-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
World calendars #1220
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
World calendars #1220
Conversation
and hopefully faster than built-in .map
I still can't tell why this is passing as a test when it fails for me in the dashboard (because of timezone conversion)
/* | ||
* wrapped dateTime2ms that: | ||
* - accepts ms numbers for backward compatibility | ||
* - inserts a dummy arg so calendar is the 3rd arg (see notes 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.
There's a lot of rearrangement in this commit, but there are two key points. This is one: calendar is always the 3rd argument to these functions - an extremely unfortunate necessity, since we need to allow different calendar systems on the same axis.
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 can't calendar be set in the closure?
Answer in #1220 (comment)
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.
you mean the setConvert
closure? Because each trace can apply its own calendar to this axis and setConvert
only happens per axis. But at least the axis internals only need to pass the first arg because it defaults to ax.calendar
.
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 knew there was a good reason. Thanks for the info!
I still think we could come up with a better pattern here. Having to remember to pass ax.calendar
in the trace module (or transform?) calc steps could potentially produce bugs in future PRs.
Let me test out a few things. Anyhow, I'm ok with the current implementation.
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.
yeah, it's a painful pattern for sure. A lot of trace types never see it though, as it's centralized in ax.makeCalcdata
which gets the trace and data attribute as args already so it can look up the calendar. But if anyone can think of a better pattern I would be happy to refactor (after a bit of a break 😅 )
But - and this is perhaps an important point - the way I have it implemented traces do NOT inherit the axis calendar, so actually you never need to pay attention to ax.calendar
, just the calendar that's specified in the trace itself. The only calendar inheritance is layout.calendar
which sets the default for everything else. That's intended for what I expect will really be the most common use case of this, people really living by one of these calendars who want all their date data and all their date axes to use that calendar.
* fn to make sure range is a couplet of valid & distinct values | ||
* now type-specific conversions for **ALL** other combinations | ||
* they're all written out, instead of being combinations of each other, for | ||
* both clarity and speed. |
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.
... and this is the other key change here: now all possible conversions between d, r, c, l, and p are generated (so you don't need to worry about whether the one you want exists), and they're simplified as much as possible, separately for each axis type.
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.
That's nice.
I've seen some users use the ax.?2?
methods in their projects (unfortunately 😏 ), not having to worry about which method is defined should make their lives easier.
out = new Array(len); | ||
for(var i = 0; i < len; i++) out[i] = func(array[i], x1, x2); | ||
return out; | ||
}; |
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 of the extra args to the axis conversion functions, statements like ax.range.map(ax.r2l)
would behave... strangely to say the least. This was actually already true for log axes, but it didn't seem to have bitten us yet. This helper fixes that and lets you pass the extra (constant) args straight through. Not super excited about its name, but I wanted to make sure people didn't use it in place of built-in map without understanding it first, that's why I didn't just call it lib.map
.
"xaxis": "x2", | ||
"yaxis": "y2", | ||
"ycalendar": "hebrew", | ||
"name": "hebrew histogram<br>jalali filtered", |
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.
and displayed against nepali dates - 3 calendars applied to one array!
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.
Nice. We should test the Chinese calendar in this mock too.
cc @n-riesco
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.
Oof what an effort 💪 Way to bulldoze through all the traces types 🚚
The new attributes look good. That said, I'm not particular happy with how intrusive world calendars are.
At the moment, world calendars are included in the Lib
module meaning that all plotly.js bundles (even custom-made bundles that don't require trace types with support with date axes e.g. geo, ternary traces). That means a lot of extra bytes for everyone. Running npm run build
on this branch shows that all our minified bundles will a little under 200K bigger . That a lot of bytes for arguably a niche feature 🍔
That said, as we are late in releasing v1.21.0
, I'm ok with this iteration.
Down the road (but hopefully soon), we should require world-calendars
in a component module. In this component module, all the calendar attributes would be declared along with the hard-coded conversion table and the special formatting logic. Then, lib/dates.js
would call Registry.getComponentMethod
to use world calendar component only if it is registered. Finally, we register the world calendar component only when trace module that support dates are themselves registered.
requiredOpts: [], | ||
otherOpts: ['dflt'], | ||
coerceFunction: function(v, propOut, dflt) { | ||
if(v && calendarList.indexOf(v) !== -1) propOut.set(v); |
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's your reasoning for adding a valType
instead of reusing enumerated
?
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.
just so I didn't have to reference the calendar list all the time. (Oh right, if we componentize, that's a second reference to world-calendars
)
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'll see what I can do here.
|
||
// for 2-digit years, the first year we map them onto | ||
var YFIRST = new Date().getFullYear() - 70; | ||
|
||
// cache world calendars, so we don't have to reinstantiate | ||
// during each date-time conversion | ||
var allCals = {}; |
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.
nice touch 👌
if(y.length === 2) { | ||
y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; | ||
} | ||
else y = Number(y); | ||
|
||
// new Date uses months from 0; subtract 1 here just so we | ||
// don't have to do it again during the validity test below | ||
m -= 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.
excellent comment 👍
// do not allow milliseconds (old) or jsdate objects (inherently | ||
// described as gregorian dates) with world calendars | ||
if(isWorldCalendar(calendar)) { | ||
logError('JS Dates and milliseconds are incompatible with world calendars', v); |
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.
nice touch
* to an ugly placeholder | ||
*/ | ||
var UNKNOWN = '##'; | ||
var d3ToWorldCalendars = { |
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.
Where did you grab the list of d3 templates? I'm (a little bit) concerned that d3 might be adding more templates in the future.
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.
https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md
Good call - we'll have to check that when we upgrade d3.
@@ -89,7 +89,8 @@ | |||
"superscript-text": "^1.0.0", | |||
"tinycolor2": "^1.3.0", | |||
"topojson-client": "^2.1.0", | |||
"webgl-context": "^2.2.0" | |||
"webgl-context": "^2.2.0", | |||
"world-calendars": "0.1.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.
Arguably wrong (depending on how much you ❤️ npm), but dependencies in libraries are commonly listed with leading ^
.
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.
👍 when we get @n-riesco 's Chinese in world-calendars
I'll change the pattern here - and probably bump it to 1.0.0 at that point since it's doing its job.
@@ -37,6 +37,9 @@ module.exports = { | |||
'jointly represent the X, Y and Z coordinates of the nth vertex.' | |||
].join(' ') | |||
}, | |||
xcalendar: surfaceAtts.xcalendar, | |||
ycalendar: surfaceAtts.ycalendar, | |||
zcalendar: surfaceAtts.zcalendar, |
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.
Hmm. Do dates actually work in mesh3d
traces?
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.
Hmm. Do dates actually work in mesh3d traces?
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 it's surface that has a problem with dates, because colorscale
and colorbar
don't do dates... you'll notice it's there but I had to explicitly include surfacecolor
. We could support this too, but another 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.
Great. Thanks for checking 👍
"xaxis": "x2", | ||
"yaxis": "y2", | ||
"ycalendar": "hebrew", | ||
"name": "hebrew histogram<br>jalali filtered", |
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.
Nice. We should test the Chinese calendar in this mock too.
cc @n-riesco
// TODO: should we do something special if the axis calendar and | ||
// the data calendar are different? Somehow display both dates with | ||
// their system names? Right now it will just display in the axis calendar | ||
// but users could add the other one as text. | ||
else d.xLabel = xLabelObj.text; |
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.
Right now it will just display in the axis calendar
Sounds to me like the correct solution.
// ideally if we want to make gantt charts or something we'd treat | ||
// the actual size (trace.x or y) as time delta but base as absolute | ||
// time. But included here for completeness. | ||
scalendar = trace.xcalendar; |
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.
Setting the size data calendar won't happen by accident. 👍 for honoring it
I think this would be easy, actually... here is the only place we directly call out to |
Sure. I'll give this a go this afternoon. |
y: { | ||
valType: 'data_array', | ||
role: 'info', |
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.
Nop. data_array
attributes get role: 'data'
during PlotSchema.get()
.
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.
oh good catch - mispaste...
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.
@@ -352,7 +353,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { | |||
|
|||
mockFigure.layout[oppAxisName] = { | |||
domain: [0, 1], | |||
range: oppAxisOpts.range.slice() | |||
range: oppAxisOpts.range.slice(), | |||
calendar: oppAxisOpts.calendar |
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.
@etpinard apologies for adding a nearly duplicate mock to test this... but since the fix is in draw
I didn't really see another way. I tried to at least put both rangesliders together on one plot but it didn't seem to like that at all.
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.
That's fine 👍
By the way, I've been wanting to add support for multiple range sliders per plot for a while now. Would be a nice feature.
break; | ||
|
||
case 'todate': | ||
var base2 = d3.time[step].utc.offset(base, -count); | ||
|
||
range0 = Lib.ms2DateTime(+d3.time[step].utc.ceil(base2)); | ||
range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2)); |
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 weird one. At first when I played with range selectors it seemed like they just worked ™️ - if only we were that lucky. They worked (to within errors I would not have been able to notice) as long as the range end is valid in the gregorian calendar and the new start is valid in the axis calendar, but there's no guarantee that that will be true in general. The fix here at least means we properly convert between range values and milliseconds.
What it doesn't get right is stepping backward by some months or years - these steps are going to be somewhat different in each calendar.
What it really doesn't get right is month or year to date. It's going to go to the beginning of the Gregorian month or year, which is generally totally different from in the other calendar. I propose to leave this as an open item for now, because it's going to have all sorts of edge cases to sort out (like in the Hebrew calendar, when the year changes at month 7, not month 1) for a niche-upon-niche feature.
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 propose to leave this as an open item for now, because it's going to have all sorts of edge cases to sort out
I agree 👍 . But let's make sure the range selector supplyDefaults doesn't allow axis.calendar !== 'gregorian'
with axis.rangeselector.buttons[i].stepmode = 'todate'
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 year/month todate (but smaller increments are still allowed) range selectors outside Gregorian calendars: 7bd501f
* | ||
* special case for world calendars: multiples of 12 are treated as years, | ||
* even for calendar systems that don't have (always or ever) 12 months/year | ||
* TODO: perhaps we need a different code for year increments to support this? |
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.
@etpinard I meant to flag this one for you - there would be something nice even with regular dates about allowing Y1
and Y5
etc instead of requiring M12
and M60
for 1 and 5 year ticks, even though for Gregorian they're equivalent. And if we allow M12
to mean a year in every calendar now it will be tough to undo later if we decide to add Y1
. On the other hand, someone actually wanting 12-month ticks in a calendar that doesn't have 12 months seems ridiculously unlikely.
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. Can you open an issue about it after this PR is merged?
1452453861917, | ||
1471240462978 | ||
"2016-01-10 14:24:21.917", | ||
"2016-08-15 01:54:22.978" | ||
] |
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.
Looks like we'll need to update the baseline.
Here's the diff on CI:
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.
- replace layoutNodes list with _module.schema object which allow multiple distinct attribute (e.g. with different description) for each nodes
- ++ allow trace and transform attributes to be filled by component modules.
"calendars" component
use only the calendars we need from 'world-calendars'
var calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar); | ||
if(isChinese) { | ||
var isIntercalary = m.charAt(m.length - 1) === 'i'; | ||
m = Number(isIntercalary ? m.substr(0, m.length - 1) : m); |
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 parseInt(m)
woudl work 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.
ah good call - I always forget about parseInt.
coptic: '2000-01-03', | ||
discworld: '2000-01-01', | ||
discworld: '2000-01-03', |
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.
bug fix or typo?
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.
see the comment change above - I decided it would be better to keep our Sundays unless and until we support the native weeks in these calendars.
@@ -284,6 +290,55 @@ describe('dates', function() { | |||
expect(Lib.dateTime2ms(expected_lastinstant, calendar)).toBe(lastInstant, calendar); | |||
}); | |||
}); | |||
|
|||
it('should contain canonical ticks sundays, ranges for all calendars', 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.
very nice test 🍻
@@ -341,6 +396,51 @@ describe('dates', function() { | |||
}); | |||
}); | |||
|
|||
describe('incrementMonth', function() { | |||
it('should include Chinese intercalary months', 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.
I should add a similar tests to world-calendars
@@ -90,7 +90,7 @@ | |||
"tinycolor2": "^1.3.0", | |||
"topojson-client": "^2.1.0", | |||
"webgl-context": "^2.2.0", | |||
"world-calendars": "0.1.0" | |||
"world-calendars": "^1.0.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.
🍻
Looks good to me 💃 after that |
]; | ||
var tick = Lib.dateTime2ms(start, 'chinese'); | ||
expected.forEach(function(v) { | ||
tick = Lib.incrementMonth(tick, 12, 'chinese'); |
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.
12? I guess incrementMonth
does some magic over leap 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.
yes exactly: #1220 (comment)
I had a quick look through the Chinese commits (I didn't go through the functions in Lib). LGTM |
@etpinard @n-riesco plots all around the world!
A few TODOs left:
world-calendars
( @n-riesco is working on this)layout.calendar
The bulk of this PR is in the two "add world calendar support" commits, but it doesn't make sense to look at the two of them separately, the first is incomplete and did some things wrong that the second completed and fixed.