Skip to content
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

Merged
merged 33 commits into from
Dec 9, 2016
Merged

World calendars #1220

merged 33 commits into from
Dec 9, 2016

Conversation

alexcjohnson
Copy link
Collaborator

@alexcjohnson alexcjohnson commented Dec 4, 2016

@etpinard @n-riesco plots all around the world!

A few TODOs left:

  • test against gl2d and gl3d plots
  • test against candlestick and OHLC
  • get Chinese into world-calendars ( @n-riesco is working on this)
  • test global 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.

/*
* wrapped dateTime2ms that:
* - accepts ms numbers for backward compatibility
* - inserts a dummy arg so calendar is the 3rd arg (see notes below).
Copy link
Collaborator Author

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.

Copy link
Contributor

@etpinard etpinard Dec 5, 2016

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)

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.
Copy link
Collaborator Author

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.

Copy link
Contributor

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;
};
Copy link
Collaborator Author

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",
Copy link
Collaborator Author

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!

Copy link
Contributor

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

@etpinard etpinard added this to the v1.21.0 milestone Dec 5, 2016
Copy link
Contributor

@etpinard etpinard left a 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);
Copy link
Contributor

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?

Copy link
Collaborator Author

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)

Copy link
Contributor

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 = {};
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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 = {
Copy link
Contributor

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.

Copy link
Collaborator Author

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"
Copy link
Contributor

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 ^ .

Copy link
Collaborator Author

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,
Copy link
Contributor

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?

Copy link
Collaborator Author

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?

yes

Copy link
Collaborator Author

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.

Copy link
Contributor

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",
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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

@alexcjohnson
Copy link
Collaborator Author

Down the road (but hopefully soon), we should require world-calendars in a component module.

I think this would be easy, actually... here is the only place we directly call out to world-calendars (though we probably need another in isWorldCalendar) I just don't know the right pattern - @etpinard if you've got a few free cycles would you want to turn this into a pluggable component?

@etpinard
Copy link
Contributor

etpinard commented Dec 5, 2016

I just don't know the right pattern - @etpinard if you've got a few free cycles would you want to turn this into a pluggable component?

Sure. I'll give this a go this afternoon.

y: {
valType: 'data_array',
role: 'info',
Copy link
Contributor

@etpinard etpinard Dec 5, 2016

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().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh good catch - mispaste...

Copy link
Collaborator Author

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
Copy link
Collaborator Author

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.

Copy link
Contributor

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));
Copy link
Collaborator Author

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.

Copy link
Contributor

@etpinard etpinard Dec 6, 2016

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'

Copy link
Collaborator Author

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?
Copy link
Collaborator Author

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.

Copy link
Contributor

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"
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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.
@etpinard etpinard mentioned this pull request Dec 6, 2016
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);
Copy link
Contributor

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

Copy link
Collaborator Author

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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug fix or typo?

Copy link
Collaborator Author

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() {
Copy link
Contributor

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() {
Copy link
Contributor

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍻

@etpinard
Copy link
Contributor

etpinard commented Dec 8, 2016

Looks good to me 💃 after that parseInt patch.

];
var tick = Lib.dateTime2ms(start, 'chinese');
expected.forEach(function(v) {
tick = Lib.incrementMonth(tick, 12, 'chinese');
Copy link
Contributor

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly: #1220 (comment)

@n-riesco
Copy link
Contributor

n-riesco commented Dec 8, 2016

I had a quick look through the Chinese commits (I didn't go through the functions in Lib). LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants