-
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
Time scale improvements #3914
Time scale improvements #3914
Conversation
0c82865
to
ccbdaf7
Compare
ccbdaf7
to
dfefaa3
Compare
Thanks @tredston I will look at reviewing this. In the meantime, could you rebase against master and squash into some smaller commits? |
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.
Overall this is a great start. Thank you for getting this to PR quality 😄
I had a few comments, questions, etc and some minor refactoring that I suggested in the comments. Please take a look and we can discuss if you have questions or concerns
src/scales/scale.time.js
Outdated
var units = Object.keys(time); | ||
var unit; | ||
|
||
for (var i = units.indexOf(minUnit); i < units.length; i++) { |
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 a while loop be slightly more clear here?
while (i < units.length && !fits)
or even maybe a do-while since we always want to loop to run at least once and we can make it clear.
do {
} while (i < units.length && !fits);
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 prefer to use a for loop when doing simple array iteration, I think having the start index, end conditions and stride all together is clearer than spreading them out.
The fits
variable would also need to be hoisted out of the loop, in which case we could write:
for (var i = units.indexOf(minUnit); i < units.length && !fits; i++)
instead?
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 think we could leave this as is. I had just wondered if we could make it a bit simpler but it's already straightforward
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 👍
name: 'month', | ||
}, | ||
month: { | ||
size: 2.628e9, |
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.
One question that came up from when I first worked on this ... what do we do for months where this is not true? ie, months like February that have less days.
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.
These sizes are used to estimate the unit and step size to use, they aren't used for calculating the tick locations. The step size is a multiple of the unit, so a unit
of month
and stepSize
of 3
give ticks which are 3 months apart. The labels are still rounded (by moment
) to the start of the unit
so they display correctly.
See e5cd916 for details.
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, ok, great 👍
src/scales/scale.time.js
Outdated
var stepSize = generationOptions.stepSize; | ||
ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceRange.min); | ||
var cur = moment(niceRange.min); | ||
while (cur.add(stepSize, generationOptions.unit).valueOf() < niceRange.max) { |
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 still need to use moment? I thought once everything was timestamps this wasn't needed? Or does this make things a lot simpler overall?
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 simplifies units which don't have a constant number of milliseconds in them eg months, years.
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, makes sense 😄
src/scales/scale.time.js
Outdated
var niceMin; | ||
var niceMax; | ||
var isoWeekday = generationOptions.isoWeekday; | ||
if (isoWeekday !== false) { |
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 think this option only applies if generationOptions.unit
is 'week'
otherwise it could be transforming data that is in seconds, or minutes.
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.
Yep, will update to only apply when generationOptions.unit
is week
.
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.
Updated.
getLabelCapacity: function(exampleTime) { | ||
var me = this; | ||
|
||
me.displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation |
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 be nice if we didn't have to set this on ourself .. could we refactor to pass this into tickFormatFunction
instead?
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.
The problem is that convertTicksToLabels
uses tickFormatFunction
as the callback for a map
so we can't really change tickFormatFunction
's message signature to include the display format.
test/scale.time.tests.js
Outdated
// scale.buildTicks(); | ||
scale.update(400, 50); | ||
var scale = createScale(mockData, config); | ||
scale.update(5000, 200); |
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 need to be this wide?
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 just needed to be wide enough to not be in danger of having ticks dropped for overlapping. Looking again this can be revised down to about 2500
before the last two ticks might overlap.
Alternatively we can have a narrower graph and update the expect
to remove the penultimate tick.
test/scale.time.tests.js
Outdated
// scale.buildTicks(); | ||
scale.update(400, 50); | ||
var scale = createScale(mockData, config); | ||
scale.update(800, 200); |
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.
same comment here about the width
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 above
test/scale.time.tests.js
Outdated
}); | ||
|
||
it('Should use the isoWeekday option', function() { | ||
var scaleID = 'myScale'; | ||
it('should not have overlapping ticks', function() { |
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.
Any reason there are two tests with the same name?
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.
One (in describe('when specifying limits'...
suite) covers the case when specifying a time.max
while the other covers the general case. Since they are in separate suites jasmine will print different messages if/when they break.
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, just wanted to make sure this wasn't an error 😄
test/scale.time.tests.js
Outdated
} | ||
}); | ||
var scale = chart.scales.xScale0; | ||
scale.update(24633, 160); |
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 seems like a giant width :/
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 essentially a regression test, the code was lifted from the example in #2249.
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, ok. that makes sense
test/scale.time.tests.js
Outdated
expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(71); | ||
expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(452); | ||
expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(71); | ||
it('should be bounded by midnights', function() { |
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.
[nit] Is the goal of this test to ensure that that when days are displayed, the last tick is at the end of the last day and similarly for the first tick?
After reading the several years ones, I think a better title might be "should be bounded by the nearest day beginnings" since I was a little unclear on what this meant at first.
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 that's the goal.
That title is much clearer, I will update.
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.
Updated.
bf77bb6
to
75feb75
Compare
Tried these out and they work well. It seems really fast now 😄 I did notice that the add buttons on https://github.com/chartjs/Chart.js/blob/master/samples/scales/time/line-time-point-data.html don't work. This was likely broken when I started on this not by your changes afterwards. Do you mind fixing it while you're at it? |
7190f04
to
8a55cce
Compare
Squashed to fewer commits and rebased onto master. Updated the code following discussions thus far. Updated the sample as requested. |
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 @tredston
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 @tredston :)
I'm not familiar with the time scale, so can't really review the overall logic, so just reviewed the code style.
That's a big PR, I didn't go in details so to sum up:
- gather local variables in function header (one per line, except for uninitialized variables)
- cache object property and array value when used more than 2 or 3 times
- cache
for
loop end condition when object property or array value - use shorter names when context is obvious
I didn't review unit test!
@@ -119,7 +119,7 @@ | |||
|
|||
document.getElementById('addData').addEventListener('click', function() { | |||
if (config.data.datasets.length > 0) { | |||
var lastTime = myLine.scales['x-axis-0'].labelMoments[0].length ? myLine.scales['x-axis-0'].labelMoments[0][myLine.scales['x-axis-0'].labelMoments[0].length - 1] : moment(); | |||
var lastTime = myLine.scales['x-axis-0'].ticksAsTimestamps.length ? moment(myLine.scales['x-axis-0'].ticksAsTimestamps[myLine.scales['x-axis-0'].ticksAsTimestamps.length - 1]) : moment(); |
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.
Could be more readable by caching myLine.scales['x-axis-0'].ticksAsTimestamps
var timestamps = myLine.scales['x-axis-0'].ticksAsTimestamps;
var lastTime = timestamps.length? moment(timestamps[timestamps.length - 1]) : moment();
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.
Done.
@@ -61,253 +69,288 @@ module.exports = function(Chart) { | |||
month: 'MMM YYYY', // Sept 2015 | |||
quarter: '[Q]Q - YYYY', // Q3 | |||
year: 'YYYY' // 2015 | |||
} | |||
}, |
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.
Why here and not at line 46?
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.
Because there were multiple authors and linting (comma-dangle
) allows it :)
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.
Of course, I mean you modified both lines, but only added comma on the second one. Anyway, that's not important!
src/scales/scale.time.js
Outdated
// Custom parsing (return an instance of moment) | ||
console.warn('options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); | ||
return timeOpts.format(label); | ||
} |
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.
No need for all these else
since you return for all if
:
if (foo) {
return 'foo';
}
if (bar) {
return 'bar';
}
return false;
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.
Done.
src/scales/scale.time.js
Outdated
} else if (label.isValid && label.isValid()) { | ||
// Moment support | ||
return label; | ||
} else if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { |
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 cache timeOpts.format
?
var format = timeOpts.format;
if (typeof format !== 'string' && format.call) {
return format(label);
}
return moment(label, format);
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.
Done.
src/scales/scale.time.js
Outdated
return label; | ||
} else if (typeof timeOpts.format !== 'string' && timeOpts.format.call) { | ||
// Custom parsing (return an instance of moment) | ||
console.warn('options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale'); |
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.
We try to reduce library size, so I would avoid to output the URL (which is quite outdated)
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.
Done.
src/scales/scale.time.js
Outdated
* @return {Number[]} ticks | ||
*/ | ||
Chart.Ticks.generators.time = function(generationOptions, dataRange) { | ||
var niceMin; |
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.
var isoWeekday = generationOptions.isoWeekday;
var niceMin, niceMax; //< uninitialized variables on the same line
and generationOptions
-> options
src/scales/scale.time.js
Outdated
} | ||
var timeGeneratorOptions = { |
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.
No need for this temporary variable:
var ticks = me.ticks = Chart.Ticks.generators.time({
maxTicks: maxTicks,
min: minTimestamp,
max: maxTimestamp,
stepSize: stepSize,
unit: unit,
isoWeekday: timeOpts.isoWeekday
}, {
min: dataMin,
max: dataMax
});
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.
Done.
src/scales/scale.time.js
Outdated
return moment(label, me.options.time.format); | ||
|
||
var tickLabelWidth = me.ctx.measureText(label).width; | ||
var cosRotation = Math.cos(helpers.toRadians(me.options.ticks.maxRotation)); |
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 cache var ticksOptions = me.options.ticks
?
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.
Done.
src/scales/scale.time.js
Outdated
@@ -8,34 +8,42 @@ module.exports = function(Chart) { | |||
|
|||
var helpers = Chart.helpers; | |||
var time = { |
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 variable name could be a bit more explicit, time
is quite a common term in this file :)
src/scales/scale.time.js
Outdated
var me = this; | ||
var epochWidth = me.max - me.min; | ||
var decimal = 0; | ||
if (epochWidth > 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.
var decimal = epochWidth? (offset - me.min) / epochWidth : 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.
Done.
b10b545
to
2f0e1cb
Compare
Thanks for the review @simonbrunel. I've implemented the requested changes around caching local variables and logic simplification. However, I'm not keen to implement:
beacuse:
var i, j; to var i = 0;
var j;
|
Thanks @tredston for the changes,
Anyway, that's minor, we can still rework that later, too bad that you removed your last commit implementing these changes ;) |
I pulled this branch to use on my own site because I was having the overlapping tick issue with some of my time scales. It seems that instead of picking better start and end values to allow for evenly placed ticks, it simply drops what would have been the second to last tick. The makes for a much larger gap between the last two ticks on the axis when displayed. It is definitely better than the current situation, but there is still an underlying issue that needs to be resolved. |
@hmcfletch is it possible to drop a test case here that uses this branch? |
Working on a deploy today, but i'll see what i can do. |
@hmcfletch were you able to make any progress on a test case for the issue you were seeing? |
Here is the data I am working with: Screenshot of Chart Chart Data {
"labels":[
"2017-02-03T00:00:00.000Z",
"2017-02-04T00:00:00.000Z",
"2017-02-06T00:00:00.000Z",
"2017-02-07T00:00:00.000Z",
"2017-02-08T00:00:00.000Z",
"2017-02-09T00:00:00.000Z",
"2017-02-10T00:00:00.000Z",
"2017-02-11T00:00:00.000Z",
"2017-02-13T00:00:00.000Z",
"2017-02-14T00:00:00.000Z",
"2017-02-15T00:00:00.000Z",
"2017-02-16T00:00:00.000Z",
"2017-02-17T00:00:00.000Z",
"2017-02-19T00:00:00.000Z",
"2017-02-20T00:00:00.000Z",
"2017-02-21T00:00:00.000Z",
"2017-02-22T00:00:00.000Z",
"2017-02-23T00:00:00.000Z",
"2017-02-24T00:00:00.000Z",
"2017-02-26T00:00:00.000Z",
"2017-02-27T00:00:00.000Z",
"2017-02-28T00:00:00.000Z",
"2017-03-01T00:00:00.000Z"
],
"datasets":[
{
"fill":false,
"borderWidth":2,
"borderColor":"rgba(145, 226, 254, 1)",
"pointBackgroundColor":"rgba(37, 198, 254, 1)",
"pointHoverBackgroundColor":"rgba(0, 237, 164, 1)",
"data":[
{
"x":"2017-02-02",
"y":168244
},
{
"x":"2017-02-03",
"y":168306
},
{
"x":"2017-02-05",
"y":168353
},
{
"x":"2017-02-06",
"y":168432
},
{
"x":"2017-02-07",
"y":168476
},
{
"x":"2017-02-08",
"y":168518
},
{
"x":"2017-02-09",
"y":168596
},
{
"x":"2017-02-10",
"y":168650
},
{
"x":"2017-02-12",
"y":168727
},
{
"x":"2017-02-13",
"y":168780
},
{
"x":"2017-02-14",
"y":168820
},
{
"x":"2017-02-15",
"y":168860
},
{
"x":"2017-02-16",
"y":168926
},
{
"x":"2017-02-18",
"y":168951
},
{
"x":"2017-02-19",
"y":169032
},
{
"x":"2017-02-20",
"y":169078
},
{
"x":"2017-02-21",
"y":169125
},
{
"x":"2017-02-22",
"y":169155
},
{
"x":"2017-02-23",
"y":169193
},
{
"x":"2017-02-25",
"y":169277
},
{
"x":"2017-02-26",
"y":169300
},
{
"x":"2017-02-27",
"y":169330
},
{
"x":"2017-02-28",
"y":169344
}
]
}
]
} Chart Options {
"responsive": true,
"legend": {
"display": false
},
"tooltips": {
"callbacks": {},
"displayColors": false
},
"scales": {
"yAxes": [
{
"type": "linear",
"ticks": {
"beginAtZero": false,
"fontColor": "#7b8994"
}
}
],
"xAxes": [
{
"ticks": {
"minRotation": 45,
"fontColor": "#7b8994"
},
"type": "time",
"time": {
"format": "YYYY-MM-DD",
"tooltipFormat": "ll HH:mm"
}
}
]
}
} |
@tredston @hmcfletch the behaviour you're seeing is happening due to https://github.com/tredston/Chart.js/blob/time-scale-improvements/src/scales/scale.time.js#L185-L189 I can't recall what that was originally designed to achieve. I think it was something to always ensure the last tick is present if the size of the axis is not a multiple of the step size. Options I see:
I personally prefer option 2. @simonbrunel thoughts? @tredston could you make those changes? |
I tried changing that if (ticks[ticks.length - 1] !== realMax) {
ticks.push(realMax);
} and it matched v2.5 |
@tredston one thing to ask as well when you're making that last change (the one in my previous comment). If you can rebase on master to fix the test file conflict this will be good to merge 😄 |
Months, quarters and years have non-constant numbers of seconds. A scale that's linear WRT milliseconds produces incorrect tick labels due to the label formatting losing precision (eg year labels lose month and day so a label of 2016-12-32 displays as 2016 instead of 2017).
531ecf3
to
7271e16
Compare
As in v2.5
7271e16
to
9e65c41
Compare
That code was meant to avoid adding the penultimate tick if it was in danger of overlapping with the last tick. Seemingly label rotation was not accounted for. Requested change applied and rebased onto master. |
Thanks @tredston @simonbrunel shall we merge this? |
Sure! |
Complete work started in https://github.com/chartjs/Chart.js/tree/time-scale-improvements (#3673) to convert
scale.time
to internally use unix timestamps instead of moments. Improves performance and reduces complexity.