-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve offset calculation for scale.offset option #4658
Conversation
src/scales/scale.time.js
Outdated
|
||
if (options.offset) { | ||
[ts.labels, ts.data].forEach(function(data) { | ||
if (!timestamps.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work as expected if ts.data
and ts.labels
both have items?
It looks what will happen is that the first iteration of the forEach
will run and timestamps.length
will be 0 so the items in ts.labels
are considered. However, the items in ts.data
will be skipped because on the second iteration, timestamps.length
may be non-zero due to the first iteration of the loop.
If this is as intended, I think there should be a comment explaining why. I can see it accidentally getting lost/changed later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is as intended. First, it processes ts.labels
to pick up timestamps of labels, which are primarily used for offset calculation because bars are usually on labels. But if there are no labels, it should add offsets based on ts.data
because we support bars with {x, y} data points as well. That is the second path.
I will add a comment on it.
src/scales/scale.time.js
Outdated
if (length) { | ||
divisor = length > 1 ? 2 : 1; | ||
if (!options.time.min) { | ||
upper = length > 1 ? interpolate(table, 'time', timestamps[1], 'pos') : 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is timestamps
guaranteed to be sorted here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is deduped and sorted at
Chart.js/src/scales/scale.time.js
Line 515 in b9afeaf
labels = arrayUnique(labels).sort(sorter); |
Chart.js/src/scales/scale.time.js
Line 521 in b9afeaf
timestamps = arrayUnique(timestamps).sort(sorter); |
b05b0f8
to
7eabbf0
Compare
src/scales/scale.time.js
Outdated
// primarily used for offset calculation because bars are usually on labels. | ||
// But if there are no labels, it should add offsets based on `ts.data` because | ||
// we support bars with {x, y} data points as well. That is the second path. | ||
[ts.labels, ts.data].forEach(function(data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would ts.data
be enough since most of the time it contains ts.labels
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@simonbrunel I was considering the case of a mixed chart with bars on labels and line with {x, y} points. If the label positions and {x, y} points are not aligned on a timeline, bar width calculation could go wrong. But, as you say, that may be an edge case, and I agree that the calculation based on ts.data
works in most cases.
Removed check for label values, and now it only uses |
@nagix the fiddles in the description don't seem to be working for me |
@nagix could you fix the fiddles so that we can review this PR? They're giving me the error below:
|
@nagix just a reminder to fix the fiddles when you get a chance. I'd love to get this change merged |
@nagix have you abandoned this PR? I'd would really like to see it get checked assuming it fixes an issue, but I can't tell what the issue is since the fiddles are broken I may close this PR due to inactivity if you're no longer interested in seeing it merged |
Closing due to inactivity |
@benmccann @etimberg @simonbrunel Sorry for not responding earlier. I fixed the fiddles and confirmed that the problems still remain. Can you reopen this if possible? |
- Offset is calulated based on the intervals between the first two data points and the last two data points (barThickness: 'flex') or the minimum interval of all data (barThickness: undefined). - Add test for both cases of barThickness: 'flex' and undefined.
As equally sized bars were introduced in 2.7.2, I made some changes to support it.
@benmccann @etimberg @simonbrunel Can you review this again? |
Documentation for these features added in 119a86f and awaiting deployment |
@@ -164,7 +164,7 @@ function computeFlexCategoryTraits(index, ruler, options) { | |||
if (prev === null) { | |||
// first data: its size is double based on the next point or, | |||
// if it's also the last data, we use the scale end extremity. | |||
prev = curr - (next === null ? ruler.end - curr : next - curr); | |||
prev = curr - (next === null ? Math.max(curr - ruler.start, ruler.end - curr) * 2 : next - curr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is getting a bit tricky to follow. I feel like we're trying to hack prev
and next
to get the desired results for start
and size
. I wonder if it would be easier if we just computed those directly like:
if (prev == null && next == null) {
start = ruler.start * (1 - percent / 2);
size = ruler.end - ruler.start;
} else if (prev == null) {
start = curr - ((next - curr) / 2) * percent;
size = ((next - curr) / 2) * percent;
} else if (next == null) {
start = curr - ((curr - prev) / 2) * percent;
size = (curr - prev) * percent;
} else {
start = curr - ((curr - prev) / 2) * percent;
size = ((next - prev) / 2) * percent;
}
Note that these formulas probably aren't at all correct. This is just to demonstrate an alternate way of laying out the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benmccann your proposal introduces lot of code duplication and actually doesn't look clearer to me since it's hard to follow all the if
/ else
. The current implementation is not a "hack" of prev / next but an adjustment when they are not defined. Comments explain pretty well what's going on so please don't reformat this part of the code.
src/scales/scale.time.js
Outdated
}); | ||
|
||
if (!barThickness) { | ||
[data, ticks].forEach(function(timestamps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a comment here:
// Calculate minInterval
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I will add this.
src/scales/scale.time.js
Outdated
) / 2; | ||
|
||
length = pos.length; | ||
if (length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be combined with the enclosing if
statement? I.e. if (options.offset && data.length)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if data
is not empty, we need to use the subset of data
, which only contains timestamps between min
and max
, so checking pos.length
is needed. But, we can move this if (length)
statement before the calculation of minInterval
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right. Maybe we could also invert the check. E.g.:
data.forEach(function(timestamp) {
if (timestamp >= min && timestamp <= max) {
pos.push(interpolate(table, 'time', timestamp, 'pos'));
}
});
length = pos.length;
if (!length) {
return;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this function needs to return an object, I will just move the check.
src/scales/scale.time.js
Outdated
|
||
length = pos.length; | ||
if (length) { | ||
// Calculate minInterval |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit nit picky, but I would actually move this comment down a line (i.e. right above [data, ticks].forEach(function(timestamps) {
) because right now it's not clear if it's referring to the entire block of code inside the if (length)
statement of just to the if (!barThickness)
section
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, agreed.
src/scales/scale.time.js
Outdated
} | ||
|
||
if (!timeOpts.min) { | ||
if (length === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if length === 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the case that there is only one data point within the range. length === 0
is already filtered out here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I missed that it was filtered out up above
src/scales/scale.time.js
Outdated
@@ -361,27 +361,55 @@ function generate(min, max, capacity, options) { | |||
* Returns the right and left offsets from edges in the form of {left, right}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify more what this offset it. What is it that gets offset and why does it need to be offset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make sense?
/**
* Returns the right and left offsets from edges in the form of {left, right}
* where each values is a relative width to the scale and ranges between 0 and 1.
* They add extra margins on the both sides by scaling down the original scale.
* Offsets are typically used for bar charts. They are calculated based on intervals
* between the data points to keep the leftmost and rightmost bars from being cut.
* Offsets are added when the `offset` option is true.
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super helpful!
To clarify:
Are these offsets for each bar or only the leftmost and rightmost bar? I.e. can you update "edges" to specify the edge of the chart or the edge of the category
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, it's "the edges of the chart".
src/scales/scale.time.js
Outdated
}); | ||
|
||
length = pos.length; | ||
if (length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could make the code slightly easier to read and remove a lot of indentation if we reversed this if statement:
if (!length) {
return {left: left, right: right};
}
You could do the same just above with the options.offset
check as well which would remove the need for two levels of indentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Will update it.
} | ||
|
||
if (!timeOpts.min) { | ||
if (length === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually I just thought of one last thing. can we move setting width
outside to occur just before the if statement? I think that would allow removing the duplicate code here and in the other if statement just below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean. width
for the left and right offset needs to be calculated separately, and the calculation depends on the length of pos
and barThickness
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right. Nevermind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, we should find a better way to fix that issue without relying on the barThickness
from the time scale.
var pos = []; | ||
var minInterval = 1; | ||
var timeOpts = options.time; | ||
var barThickness = options.barThickness; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing barThickness
from the time scale doesn't seem ideal because it correlates the bar controller and this scale, which is something we are trying to remove / avoid. Actually, the barThickness
should not be part of the scale options.
@nagix this PR will need to be rebased. There's also an outstanding comment from Simon |
Closing this for now because accessing |
The offsets of time scales are currently calculated based on tick intervals, but it causes too much margin or short of margin depending on scale configuration or datasets. The proposed code calculates the offsets based on timestamps of labels and data. It also fixes offset calculation in case that a chart has a single data point.
Current version: https://jsfiddle.net/q4fo65aa/
Proposed version: https://jsfiddle.net/sfhwd1by/
Fixes #5312