From d4bb89d788a68c78c1b6dcad76a84c4e9241858b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 14 Oct 2016 16:51:58 -0400 Subject: [PATCH 01/23] improve dateTime2ms edge cases and standardize BADNUM and FP_SAFE constants --- src/lib/constants.js | 27 ++++++ src/lib/dates.js | 131 +++++++++++++++++++---------- src/lib/index.js | 6 ++ src/plots/cartesian/axes.js | 3 +- src/plots/cartesian/constants.js | 8 -- src/plots/cartesian/set_convert.js | 6 +- 6 files changed, 125 insertions(+), 56 deletions(-) create mode 100644 src/lib/constants.js diff --git a/src/lib/constants.js b/src/lib/constants.js new file mode 100644 index 00000000000..3fcc6c333a8 --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,27 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + + +module.exports = { + /** + * Standardize all missing data in calcdata to use undefined + * never null or NaN. + * That way we can use !==undefined, or !== BADNUM, + * to test for real data + */ + BADNUM: undefined, + + /* + * Limit certain operations to well below floating point max value + * to avoid glitches: Make sure that even when you multiply it by the + * number of pixels on a giant screen it still works + */ + FP_SAFE: Number.MAX_VALUE / 10000 +}; diff --git a/src/lib/dates.js b/src/lib/dates.js index d3779e90718..ae0cb32bb1a 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -11,7 +11,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); - +var BADNUM = require('./constants').BADNUM; /** * dateTime2ms - turn a date object or string s of the form @@ -19,7 +19,13 @@ var isNumeric = require('fast-isnumeric'); * per javascript standard) * may truncate after any full field, and sss can be any length * even >3 digits, though javascript dates truncate to milliseconds - * returns false if it doesn't find a date + * returns BADNUM if it doesn't find a date + * + * Expanded to support negative years to -9999 but you must always + * give 4 digits, except for 2-digit positive years which we assume are + * near the present time. + * Note that we follow ISO 8601:2004: there *is* a year 0, which + * is 1BC/BCE, and -1===2BC etc. * * 2-digit to 4-digit year conversion, where to cut off? * from http://support.microsoft.com/kb/244664: @@ -27,10 +33,10 @@ var isNumeric = require('fast-isnumeric'); * but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')): * 1950-2049 * by Java, from http://stackoverflow.com/questions/2024273/: - * now-80 - now+20 + * now-80 - now+19 * or FileMaker Pro, from * http://www.filemaker.com/12help/html/add_view_data.4.21.html: - * now-70 - now+30 + * now-70 - now+29 * but python strptime etc, via * http://docs.python.org/py3k/library/time.html: * 1969-2068 (super forward-looking, but static, not sliding!) @@ -39,8 +45,8 @@ var isNumeric = require('fast-isnumeric'); * they can learn the hard way not to use 2-digit years, as no choice we * make now will cover all possibilities. mostly this will all be taken * care of in initial parsing, should only be an issue for hand-entered data - * currently (2012) this range is: - * 1942-2041 + * currently (2016) this range is: + * 1946-2045 */ exports.dateTime2ms = function(s) { @@ -49,63 +55,96 @@ exports.dateTime2ms = function(s) { if(s.getTime) return +s; } catch(e) { - return false; + return BADNUM; } var y, m, d, h; // split date and time parts var datetime = String(s).split(' '); - if(datetime.length > 2) return false; + if(datetime.length > 2) return BADNUM; var p = datetime[0].split('-'); // date part - if(p.length > 3 || (p.length !== 3 && datetime[1])) return false; + + var CE = true; // common era, ie positive year + if(p[0] === '') { + // first part is blank: year starts with a minus sign + CE = false; + p.splice(0, 1); + } + + if(p.length > 3 || (p.length !== 3 && datetime[1])) return BADNUM; // year if(p[0].length === 4) y = Number(p[0]); else if(p[0].length === 2) { + if(!CE) return BADNUM; var yNow = new Date().getFullYear(); y = ((Number(p[0]) - yNow + 70) % 100 + 200) % 100 + yNow - 70; } - else return false; - if(!isNumeric(y)) return false; - if(p.length === 1) return new Date(y, 0, 1).getTime(); // year only - - // month - m = Number(p[1]) - 1; // new Date() uses zero-based months - if(p[1].length > 2 || !(m >= 0 && m <= 11)) return false; - if(p.length === 2) return new Date(y, m, 1).getTime(); // year-month - - // day - d = Number(p[2]); - if(p[2].length > 2 || !(d >= 1 && d <= 31)) return false; - - // now save the date part - d = new Date(y, m, d).getTime(); - if(!datetime[1]) return d; // year-month-day - p = datetime[1].split(':'); - if(p.length > 3) return false; - - // hour - h = Number(p[0]); - if(p[0].length > 2 || !(h >= 0 && h <= 23)) return false; - d += 3600000 * h; - if(p.length === 1) return d; - - // minute - m = Number(p[1]); - if(p[1].length > 2 || !(m >= 0 && m <= 59)) return false; - d += 60000 * m; - if(p.length === 2) return d; - - // second - s = Number(p[2]); - if(!(s >= 0 && s < 60)) return false; - return d + s * 1000; + else return BADNUM; + if(!isNumeric(y)) return BADNUM; + + // javascript takes new Date(0..99,m,d) to mean 1900-1999, so + // to support years 0-99 we need to use setFullYear explicitly + var baseDate = new Date(0, 0, 1); + baseDate.setFullYear(CE ? y : -y); + if(p.length > 1) { + + // month + m = Number(p[1]) - 1; // new Date() uses zero-based months + if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM; + baseDate.setMonth(m); + + if(p.length > 2) { + + // day + d = Number(p[2]); + if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM; + baseDate.setDate(d); + + // does that date exist in this month? + if(baseDate.getDate() !== d) return BADNUM; + + if(datetime[1]) { + + p = datetime[1].split(':'); + if(p.length > 3) return BADNUM; + + // hour + h = Number(p[0]); + if(p[0].length > 2 || !(h >= 0 && h <= 23)) return BADNUM; + baseDate.setHours(h); + + // does that hour exist in this day? (Daylight time!) + // (TODO: remove this check when we move to UTC) + if(baseDate.getHours() !== h) return BADNUM; + + if(p.length > 1) { + d = baseDate.getTime(); + + // minute + m = Number(p[1]); + if(p[1].length > 2 || !(m >= 0 && m <= 59)) return BADNUM; + d += 60000 * m; + if(p.length === 2) return d; + + // second (and milliseconds) + s = Number(p[2]); + if(!(s >= 0 && s < 60)) return BADNUM; + return d + s * 1000; + } + } + } + } + return baseDate.getTime(); }; +exports.MIN_MS = exports.dateTime2ms('-9999'); +exports.MAX_MS = exports.dateTime2ms('9999'); + // is string s a date? (see above) exports.isDateTime = function(s) { - return (exports.dateTime2ms(s) !== false); + return (exports.dateTime2ms(s) !== BADNUM); }; // pad a number with zeroes, to given # of digits before the decimal point @@ -123,6 +162,8 @@ function lpad(val, digits) { exports.ms2DateTime = function(ms, r) { if(!r) r = 0; + if(ms < exports.MIN_MS || ms > exports.MAX_MS) return BADNUM; + var d = new Date(ms), s = d3.time.format('%Y-%m-%d')(d); diff --git a/src/lib/index.js b/src/lib/index.js index 3518fca5d31..597b40330a1 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -36,6 +36,8 @@ lib.dateTime2ms = datesModule.dateTime2ms; lib.isDateTime = datesModule.isDateTime; lib.ms2DateTime = datesModule.ms2DateTime; lib.parseDate = datesModule.parseDate; +lib.MIN_MS = datesModule.MIN_MS; +lib.MAX_MS = datesModule.MAX_MS; var searchModule = require('./search'); lib.findBin = searchModule.findBin; @@ -75,6 +77,10 @@ lib.error = loggersModule.error; lib.notifier = require('./notifier'); +var constantsModule = require('./constants'); +lib.BADNUM = constantsModule.BADNUM, +lib.FP_SAFE = constantsModule.FP_SAFE; + /** * swap x and y of the same attribute in container cont * specify attr with a ? in place of x/y diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 56cc0eb9ea9..b6dd24b594b 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -19,6 +19,8 @@ var Titles = require('../../components/titles'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); +var FP_SAFE = Lib.FP_SAFE; + var axes = module.exports = {}; @@ -304,7 +306,6 @@ axes.saveRangeInitial = function(gd, overwrite) { // (unless one end is overridden by tozero) // tozero: (boolean) make sure to include zero if axis is linear, // and make it a tight bound if possible -var FP_SAFE = Number.MAX_VALUE / 2; axes.expand = function(ax, data, options) { if(!(ax.autorange || ax._needsExpand) || !data) return; if(!ax._min) ax._min = []; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 81467cee968..7aec8f149d0 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -21,14 +21,6 @@ module.exports = { y: /^yaxis([2-9]|[1-9][0-9]+)?$/ }, - /** - * standardize all missing data in calcdata to use undefined - * never null or NaN. - * that way we can use !==undefined, or !== BADNUM, - * to test for real data - */ - BADNUM: undefined, - // axis match regular expression xAxisMatch: /^xaxis[0-9]*$/, yAxisMatch: /^yaxis[0-9]*$/, diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index b5e1b0258e4..58eba189716 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -13,6 +13,8 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); +var FP_SAFE = Lib.FP_SAFE; +var BADNUM = Lib.BADNUM; var constants = require('./constants'); var cleanDatum = require('./clean_datum'); @@ -128,7 +130,7 @@ module.exports = function setConvert(ax) { }; ax.l2p = function(v) { - if(!isNumeric(v)) return constants.BADNUM; + if(!isNumeric(v)) return BADNUM; // include 2 fractional digits on pixel, for PDF zooming etc return d3.round(ax._b + ax._m * v, 2); @@ -197,7 +199,7 @@ module.exports = function setConvert(ax) { } var c = ax._categories.indexOf(v); - return c === -1 ? constants.BADNUM : c; + return c === -1 ? BADNUM : c; }; ax.d2l = ax.d2c; From 9e279555701e0a16ebace209c4c8044cd6532083 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 14 Oct 2016 16:53:10 -0400 Subject: [PATCH 02/23] switch date axes to use date strings for range and tick0 and introduce new 'r' conversion mode for ranges: can be the same as 'd' (for linear and date), 'l' (for log, for now!) or 'c' (for category, this will have to stay different from 'd' forever so it's continuous) --- .../rangeselector/get_update_object.js | 10 +- src/plots/cartesian/axes.js | 130 +++++++----- src/plots/cartesian/axis_defaults.js | 9 +- src/plots/cartesian/constants.js | 7 +- src/plots/cartesian/dragbox.js | 50 ++--- src/plots/cartesian/layout_attributes.js | 17 +- src/plots/cartesian/set_convert.js | 185 +++++++++++++----- src/traces/scatter/plot.js | 4 +- test/jasmine/tests/range_selector_test.js | 140 +++++++------ 9 files changed, 346 insertions(+), 206 deletions(-) diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js index 740fce6c86b..acb9ae9e087 100644 --- a/src/components/rangeselector/get_update_object.js +++ b/src/components/rangeselector/get_update_object.js @@ -11,6 +11,8 @@ var d3 = require('d3'); +var Lib = require('../../lib'); + module.exports = function getUpdateObject(axisLayout, buttonLayout) { var axName = axisLayout._name; @@ -31,7 +33,7 @@ module.exports = function getUpdateObject(axisLayout, buttonLayout) { function getXRange(axisLayout, buttonLayout) { var currentRange = axisLayout.range; - var base = new Date(currentRange[1]); + var base = new Date(Lib.dateTime2ms(currentRange[1])); var step = buttonLayout.step, count = buttonLayout.count; @@ -40,13 +42,13 @@ function getXRange(axisLayout, buttonLayout) { switch(buttonLayout.stepmode) { case 'backward': - range0 = d3.time[step].offset(base, -count).getTime(); + range0 = Lib.ms2DateTime(d3.time[step].offset(base, -count)); break; case 'todate': - var base2 = d3.time[step].offset(base, -(count - 1)); + var base2 = d3.time[step].offset(base, -count); - range0 = d3.time[step].floor(base2).getTime(); + range0 = Lib.ms2DateTime(d3.time[step].ceil(base2)); break; } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index b6dd24b594b..5e3ef6bd29e 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -132,6 +132,19 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) { } }; +// Find the autorange for this axis +// +// assumes ax._min and ax._max have already been set by calling axes.expand +// using calcdata from all traces. These are arrays of: +// {val: calcdata value, pad: extra pixels beyond this value} +// +// Returns an array of [min, max]. These are calcdata for log and category axes +// and data for linear and date axes. +// +// TODO: we want to change log to data as well, but it's hard to do this +// maintaining backward compatibility. category will always have to use calcdata +// though, because otherwise values between categories (or outside all categories) +// would be impossible. axes.getAutoRange = function(ax) { var newRange = []; @@ -150,7 +163,12 @@ axes.getAutoRange = function(ax) { var j, minpt, maxpt, minbest, maxbest, dp, dv, mbest = 0, - axReverse = (ax.range && ax.range[1] < ax.range[0]); + axReverse = false; + + if(ax.range) { + var rng = ax.range.map(ax.r2l); + axReverse = rng[1] < rng[0]; + } // one-time setting to easily reverse the axis // when plotting from code @@ -239,11 +257,9 @@ axes.getAutoRange = function(ax) { } // maintain reversal - if(axReverse) { - newRange.reverse(); - } + if(axReverse) newRange.reverse(); - return newRange; + return newRange.map(ax.l2r || Number); }; axes.doAutoRange = function(ax) { @@ -444,10 +460,22 @@ axes.autoBin = function(data, ax, nbins, is2d) { } // piggyback off autotick code to make "nice" bin sizes - var dummyax = { - type: ax.type === 'log' ? 'linear' : ax.type, - range: [datamin, datamax] - }; + var dummyax; + if(ax.type === 'log') { + dummyax = { + type: 'linear', + range: [datamin, datamax] + }; + } + else { + dummyax = { + type: ax.type, + // conversion below would be ax.c2r but that's only different from l2r + // for log, and this is the only place (so far?) we would want c2r. + range: [datamin, datamax].map(ax.l2r) + }; + } + axes.autoTicks(dummyax, size0); var binstart = axes.tickIncrement( axes.tickFirst(dummyax), dummyax.dtick, 'reverse'), @@ -529,6 +557,8 @@ axes.autoBin = function(data, ax, nbins, is2d) { axes.calcTicks = function calcTicks(ax) { if(ax.tickmode === 'array') return arrayTicks(ax); + var rng = ax.range.map(ax.r2l); + // calculate max number of (auto) ticks to display based on plot size if(ax.tickmode === 'auto' || !ax.dtick) { var nt = ax.nticks, @@ -543,7 +573,7 @@ axes.calcTicks = function calcTicks(ax) { nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; } } - axes.autoTicks(ax, Math.abs(ax.range[1] - ax.range[0]) / nt); + axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt); // check for a forced minimum dtick if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { ax.dtick = ax._minDtick; @@ -553,8 +583,7 @@ axes.calcTicks = function calcTicks(ax) { // check for missing tick0 if(!ax.tick0) { - ax.tick0 = (ax.type === 'date') ? - new Date(2000, 0, 1).getTime() : 0; + ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0; } // now figure out rounding of tick values @@ -564,12 +593,12 @@ axes.calcTicks = function calcTicks(ax) { ax._tmin = axes.tickFirst(ax); // check for reversed axis - var axrev = (ax.range[1] < ax.range[0]); + var axrev = (rng[1] < rng[0]); // return the full set of tick vals var vals = [], // add a tiny bit so we get ticks which may have rounded out - endtick = ax.range[1] * 1.0001 - ax.range[0] * 0.0001; + endtick = rng[1] * 1.0001 - rng[0] * 0.0001; if(ax.type === 'category') { endtick = (axrev) ? Math.max(-0.5, endtick) : Math.min(ax._categories.length - 0.5, endtick); @@ -597,15 +626,15 @@ function arrayTicks(ax) { var vals = ax.tickvals, text = ax.ticktext, ticksOut = new Array(vals.length), - r0expanded = ax.range[0] * 1.0001 - ax.range[1] * 0.0001, - r1expanded = ax.range[1] * 1.0001 - ax.range[0] * 0.0001, + rng = ax.range.map(ax.r2l), + r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001, + r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001, tickMin = Math.min(r0expanded, r1expanded), tickMax = Math.max(r0expanded, r1expanded), vali, i, j = 0; - // without a text array, just format the given values as any other ticks // except with more precision to the numbers if(!Array.isArray(text)) text = []; @@ -658,7 +687,7 @@ axes.autoTicks = function(ax, roughDTick) { var base; if(ax.type === 'date') { - ax.tick0 = new Date(2000, 0, 1).getTime(); + ax.tick0 = '2000-01-01'; if(roughDTick > 15778800000) { // years if roughDTick > 6mo @@ -675,7 +704,7 @@ axes.autoTicks = function(ax, roughDTick) { // days if roughDTick > 12h ax.dtick = roundDTick(roughDTick, 86400000, roundDays); // get week ticks on sunday - ax.tick0 = new Date(2000, 0, 2).getTime(); + ax.tick0 = '2000-01-01'; } else if(roughDTick > 1800000) { // hours if roughDTick > 30m @@ -697,16 +726,19 @@ axes.autoTicks = function(ax, roughDTick) { } else if(ax.type === 'log') { ax.tick0 = 0; + var rng = ax.range.map(ax.r2l); - // only show powers of 10 - if(roughDTick > 0.7) ax.dtick = Math.ceil(roughDTick); - else if(Math.abs(ax.range[1] - ax.range[0]) < 1) { + if(roughDTick > 0.7) { + // only show powers of 10 + ax.dtick = Math.ceil(roughDTick); + } + else if(Math.abs(rng[1] - rng[0]) < 1) { // span is less than one power of 10 - var nt = 1.5 * Math.abs((ax.range[1] - ax.range[0]) / roughDTick); + var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick); // ticks on a linear scale, labeled fully - roughDTick = Math.abs(Math.pow(10, ax.range[1]) - - Math.pow(10, ax.range[0])) / nt; + roughDTick = Math.abs(Math.pow(10, rng[1]) - + Math.pow(10, rng[0])) / nt; base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10); } @@ -745,13 +777,16 @@ axes.autoTicks = function(ax, roughDTick) { // for date ticks, the last date part to show (y,m,d,H,M,S) // or an integer # digits past seconds function autoTickRound(ax) { - var dtick = ax.dtick, - maxend; + var dtick = ax.dtick; ax._tickexponent = 0; - if(!isNumeric(dtick) && typeof dtick !== 'string') dtick = 1; + if(!isNumeric(dtick) && typeof dtick !== 'string') { + dtick = 1; + } - if(ax.type === 'category') ax._tickround = null; + if(ax.type === 'category') { + ax._tickround = null; + } else if(isNumeric(dtick) || dtick.charAt(0) === 'L') { if(ax.type === 'date') { if(dtick >= 86400000) ax._tickround = 'd'; @@ -761,14 +796,13 @@ function autoTickRound(ax) { else ax._tickround = 3 - Math.round(Math.log(dtick / 2) / Math.LN10); } else { + // linear or log + var rng = ax.range.map(ax.r2d || Number); if(!isNumeric(dtick)) dtick = Number(dtick.substr(1)); // 2 digits past largest digit of dtick ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); - if(ax.type === 'log') { - maxend = Math.pow(10, Math.max(ax.range[0], ax.range[1])); - } - else maxend = Math.max(Math.abs(ax.range[0]), Math.abs(ax.range[1])); + var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); if(Math.abs(rangeexp) > 3) { @@ -824,13 +858,16 @@ axes.tickIncrement = function(x, dtick, axrev) { // calculate the first tick on an axis axes.tickFirst = function(ax) { - var axrev = ax.range[1] < ax.range[0], + var r2l = ax.r2l || Number, + rng = ax.range.map(r2l), + axrev = rng[1] < rng[0], sRound = axrev ? Math.floor : Math.ceil, // add a tiny extra bit to make sure we get ticks // that may have been rounded out - r0 = ax.range[0] * 1.0001 - ax.range[1] * 0.0001, + r0 = rng[0] * 1.0001 - rng[1] * 0.0001, dtick = ax.dtick, - tick0 = ax.tick0; + tick0 = r2l(ax.tick0); + if(isNumeric(dtick)) { var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0; @@ -916,7 +953,8 @@ axes.tickText = function(ax, x, hover) { i; if(arrayMode && Array.isArray(ax.ticktext)) { - var minDiff = Math.abs(ax.range[1] - ax.range[0]) / 10000; + var rng = ax.range.map(ax.r2l), + minDiff = Math.abs(rng[1] - rng[0]) / 10000; for(i = 0; i < ax.ticktext.length; i++) { if(Math.abs(x - ax.d2l(ax.tickvals[i])) < minDiff) break; } @@ -1106,7 +1144,7 @@ function numFormat(v, ax, fmtoverride, hover) { (isNumeric(v) ? Math.abs(v) || 1 : 1), // if not showing any exponents, don't change the exponent // from what we calculate - range: ax.showexponent === 'none' ? ax.range : [0, v || 1] + range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1] }; autoTickRound(ah); tickRound = (Number(ah._tickround) || 0) + 4; @@ -1354,8 +1392,9 @@ axes.makeClipPaths = function(gd) { // doTicks: draw ticks, grids, and tick labels // axid: 'x', 'y', 'x2' etc, // blank to do all, -// 'redraw' to force full redraw, and reset ax._r -// (stored range for use by zoom/pan) +// 'redraw' to force full redraw, and reset: +// ax._r (stored range for use by zoom/pan) +// ax._rl (stored linearized range for use by zoom/pan) // or can pass in an axis object directly axes.doTicks = function(gd, axid, skipTitle) { var fullLayout = gd._fullLayout, @@ -1393,7 +1432,10 @@ axes.doTicks = function(gd, axid, skipTitle) { return function() { if(!ax._id) return; var axDone = axes.doTicks(gd, ax._id); - if(axid === 'redraw') ax._r = ax.range.slice(); + if(axid === 'redraw') { + ax._r = ax.range.slice(); + ax._rl = ax._r.map(ax.r2l); + } return axDone; }; })); @@ -1411,9 +1453,6 @@ axes.doTicks = function(gd, axid, skipTitle) { } } - // in case a val turns into string somehow - ax.range = [+ax.range[0], +ax.range[1]]; - // set scaling to pixels ax.setScale(); @@ -1802,7 +1841,8 @@ axes.doTicks = function(gd, axid, skipTitle) { break; } } - var showZl = (ax.range[0] * ax.range[1] <= 0) && ax.zeroline && + var rng = ax.range.map(ax.r2l), + showZl = (rng[0] * rng[1] <= 0) && ax.zeroline && (ax.type === 'linear' || ax.type === '-') && gridvals.length && (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline); var zl = zlcontainer.selectAll('path.' + zcls) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 015ec9d7cd0..79ebaf70133 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -25,6 +25,7 @@ var setConvert = require('./set_convert'); var orderedCategories = require('./ordered_categories'); var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); +var constants = require('./constants'); /** @@ -96,7 +97,13 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, var autoRange = coerce('autorange', !validRange); if(autoRange) coerce('rangemode'); - var range = coerce('range', [-1, letter === 'x' ? 6 : 4]); + + var dfltRange; + if(axType === 'date') dfltRange = constants.DFLTRANGEDATE; + else if(letter === 'x') dfltRange = constants.DFLTRANGEX; + else dfltRange = constants.DFLTRANGEY; + + var range = coerce('range', dfltRange.slice()); if(range[0] === range[1]) { containerOut.range = [range[0] - 1, range[0] + 1]; } diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 7aec8f149d0..765241182a6 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -64,5 +64,10 @@ module.exports = { BENDPX: 1.5, // delay before a redraw (relayout) after smooth panning and zooming - REDRAWDELAY: 50 + REDRAWDELAY: 50, + + // last resort axis ranges for x, y, and date axes if we have no data + DFLTRANGEX: [-1, 6], + DFLTRANGEY: [-1, 4], + DFLTRANGEDATE: ['2000-01-01', '2001-01-01'], }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index d2f5271ba3d..3cdac87c2c8 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -98,11 +98,6 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { return dragger; } - function forceNumbers(axRange) { - axRange[0] = Number(axRange[0]); - axRange[1] = Number(axRange[1]); - } - var dragOptions = { element: dragger, gd: gd, @@ -213,7 +208,6 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { .attr('d', 'M0,0Z'); clearSelect(); - for(var i = 0; i < allaxes.length; i++) forceNumbers(allaxes[i].range); } function clearSelect() { @@ -367,7 +361,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { verticalAlign: vAlign }) .on('edit', function(text) { - var v = ax.type === 'category' ? ax.c2l(text) : ax.d2l(text); + var v = ax.d2r(text); if(v !== undefined) { Plotly.relayout(gd, attrStr, v); } @@ -427,10 +421,11 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { function zoomWheelOneAxis(ax, centerFraction, zoom) { if(ax.fixedrange) return; - forceNumbers(ax.range); - var axRange = ax.range, + + var axRange = ax.range.map(ax.r2l), v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; - ax.range = [v0 + (axRange[0] - v0) * zoom, v0 + (axRange[1] - v0) * zoom]; + function doZoom(v) { return ax.l2r(v0 + (v - v0) * zoom); } + ax.range = axRange.map(doZoom); } if(ew) { @@ -478,7 +473,10 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { for(var i = 0; i < axList.length; i++) { var axi = axList[i]; if(!axi.fixedrange) { - axi.range = [axi._r[0] - pix / axi._m, axi._r[1] - pix / axi._m]; + axi.range = [ + axi.l2r(axi._rl[0] - pix / axi._m), + axi.l2r(axi._rl[1] - pix / axi._m) + ]; } } } @@ -501,23 +499,29 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { 1 / (1 / Math.max(d, -0.3) + 3.222)); } - // dz: set a new value for one end (0 or 1) of an axis array ax, + // dz: set a new value for one end (0 or 1) of an axis array axArray, // and return a pixel shift for that end for the viewbox // based on pixel drag distance d // TODO: this makes (generally non-fatal) errors when you get // near floating point limits - function dz(ax, end, d) { + function dz(axArray, end, d) { var otherEnd = 1 - end, - movedi = 0; - for(var i = 0; i < ax.length; i++) { - var axi = ax[i]; + movedAx, + newLinearizedEnd; + for(var i = 0; i < axArray.length; i++) { + var axi = axArray[i]; if(axi.fixedrange) continue; - movedi = i; - axi.range[end] = axi._r[otherEnd] + - (axi._r[end] - axi._r[otherEnd]) / dZoom(d / axi._length); + movedAx = axi; + newLinearizedEnd = axi._rl[otherEnd] + + (axi._rl[end] - axi._rl[otherEnd]) / dZoom(d / axi._length); + var newEnd = axi.l2r(newLinearizedEnd); + + // if l2r comes back false or undefined, it means we've dragged off + // the end of valid ranges - so stop. + if(newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd; } - return ax[movedi]._length * (ax[movedi]._r[end] - ax[movedi].range[end]) / - (ax[movedi]._r[end] - ax[movedi]._r[otherEnd]); + return movedAx._length * (movedAx._rl[end] - newLinearizedEnd) / + (movedAx._rl[end] - movedAx._rl[otherEnd]); } if(xActive === 'w') dx = dz(xa, 0, dx); @@ -719,8 +723,10 @@ function getEndText(ax, end) { diff = Math.abs(initialVal - ax.range[1 - end]), dig; + // TODO: this should basically be ax.r2d but we're doing extra + // rounding here... can we clean up at all? if(ax.type === 'date') { - return Lib.ms2DateTime(initialVal, diff); + return initialVal; } else if(ax.type === 'log') { dig = Math.ceil(Math.max(0, -Math.log(diff) / Math.LN10)) + 3; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index b1465d5836b..7b8015f5d67 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -82,16 +82,16 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'number'}, - {valType: 'number'} + {valType: 'any'}, + {valType: 'any'} ], description: [ 'Sets the range of this axis.', 'If the axis `type` is *log*, then you must take the log of your desired range', '(e.g. to set the range from 1 to 100, set the range from 0 to 2).', - 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', - '(the number of milliseconds since January 1st, 1970). For example, to set the date range from', - 'January 1st 1970 to November 4th, 2013, set the range from 0 to 1380844800000.0' + 'If the axis `type` is *date*, it should be date strings, like date data.', + 'If the axis `type` is *category*, it should be numbers, using the scale where', + 'each category is assigned a serial number from zero in the order it appears.' ].join(' ') }, @@ -141,10 +141,9 @@ module.exports = { 'Use with `dtick`.', 'If the axis `type` is *log*, then you must take the log of your starting tick', '(e.g. to set the starting tick to 100, set the `tick0` to 2).', - 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', - '(the number of milliseconds since January 1st, 1970).', - 'For example, to set the starting tick to', - 'November 4th, 2013, set the range to 1380844800000.0.' + 'If the axis `type` is *date*, it should be a date string, like date data.', + 'If the axis `type` is *category*, it should be a number, using the scale where', + 'each category is assigned a serial number from zero in the order it appears.' ].join(' ') }, dtick: { diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 58eba189716..cd0d9b92332 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -22,15 +22,24 @@ var axisIds = require('./axis_ids'); /** - * Define the conversion functions for an axis data is used in 4 ways: + * Define the conversion functions for an axis data is used in 5 ways: * * d: data, in whatever form it's provided * c: calcdata: turned into numbers, but not linearized - * l: linearized - same as c except for log axes (and other - * mappings later?) this is used by ranges, and when we - * need to know if it's *possible* to show some data on - * this axis, without caring about the current range + * l: linearized - same as c except for log axes (and other nonlinear + * mappings later?) this is used when we need to know if it's + * *possible* to show some data on this axis, without caring about + * the current range * p: pixel value - mapped to the screen with current size and zoom + * r: ranges, tick0, and annotation positions match one of the above + * but are handled differently for different types: + * - linear and date: data format (d) + * - category: calcdata format (c), and will stay that way because + * the data format has no continuous mapping + * - log: linearized (l) format + * TODO: in v2.0 we plan to change it to data format. At that point + * shapes will work the same way as ranges, tick0, and annotations + * so they can use this conversion too. * * Creates/updates these conversion functions * also clears the autorange bounds ._min and ._max @@ -55,10 +64,39 @@ module.exports = function setConvert(ax) { return 0.5 * (r0 + r1 - 3 * clipMult * Math.abs(r0 - r1)); } - else return constants.BADNUM; + else return BADNUM; + } + + function fromLog(v) { + return Math.pow(10, v); + } + + function num(v) { + if(!isNumeric(v)) return BADNUM; + v = Number(v); + if(v < -FP_SAFE || v > FP_SAFE) return BADNUM; + return isNumeric(v) ? Number(v) : BADNUM; + } + + function cleanNum(v) { + v = cleanDatum(v); + return num(v); + } + + // normalize date format to date string, in case it starts as + // a Date object or milliseconds + function cleanDate(v) { + if(v.getTime || typeof v === 'number') { + // NOTE: if someone puts in a year as a number rather than a string, + // this will mistakenly convert it thinking it's milliseconds from 1970 + // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds + return Lib.ms2DateTime(+v); + } + else if(!Lib.isDateTime(v)) { + Lib.error('unrecognized date', v); + } + return v; } - function fromLog(v) { return Math.pow(10, v); } - function num(v) { return isNumeric(v) ? Number(v) : constants.BADNUM; } ax.c2l = (ax.type === 'log') ? toLog : num; ax.l2c = (ax.type === 'log') ? fromLog : num; @@ -68,7 +106,12 @@ module.exports = function setConvert(ax) { // set scaling to pixels ax.setScale = function(usePrivateRange) { var gs = ax._gd._fullLayout._size, - i; + axLetter = ax._id.charAt(0), + i, dflt; + + if(ax.type === 'date') dflt = constants.DFLTRANGEDATE; + else if(axLetter === 'y') dflt = constants.DFLTRANGEY; + else dflt = constants.DFLTRANGEX; // TODO cleaner way to handle this case if(!ax._categories) ax._categories = []; @@ -84,40 +127,66 @@ module.exports = function setConvert(ax) { // issue if we transform the drawn layer *and* use the new axis range to // draw the data. This allows us to construct setConvert using the pre- // interaction values of the range: - var range = (usePrivateRange && ax._r) ? ax._r : ax.range; + var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range'; + var range = ax[rangeAttr]; // make sure we have a range (linearized data values) // and that it stays away from the limits of javascript numbers - if(!range || range.length !== 2 || range[0] === range[1]) { - range = [-1, 1]; + if(!range || range.length !== 2) { + ax[rangeAttr] = range = dflt; } for(i = 0; i < 2; i++) { - if(!isNumeric(range[i])) { - range[i] = isNumeric(range[1 - i]) ? - (range[1 - i] * (i ? 10 : 0.1)) : - (i ? 1 : -1); - } + if(ax.type === 'date') { + if(!Lib.isDateTime(range[i])) { + ax[rangeAttr] = range = dflt; + break; + } - if(range[i] < -(Number.MAX_VALUE / 2)) { - range[i] = -(Number.MAX_VALUE / 2); - } - else if(range[i] > Number.MAX_VALUE / 2) { - range[i] = Number.MAX_VALUE / 2; + if(range[i] < Lib.MIN_MS) range[i] = Lib.MIN_MS; + if(range[i] > Lib.MAX_MS) range[i] = Lib.MAX_MS; + + if(range[0] === range[1]) { + // split by +/- 1 second + range[0] = ax.l2r(ax.r2l(range[0]) - 1000); + range[1] = ax.l2r(ax.r2l(range[1]) + 1000); + break; + } } + else { + if(!isNumeric(range[i])) { + if(isNumeric(range[1 - i])) { + range[i] = range[1 - i] * (i ? 10 : 0.1); + } + else { + ax[rangeAttr] = range = dflt; + break; + } + } + + if(range[i] < -FP_SAFE) range[i] = -FP_SAFE; + else if(range[i] > FP_SAFE) range[i] = FP_SAFE; + if(range[0] === range[1]) { + var inc = Math.max(1, Math.abs(range[0] * 1e-6)); + range[0] -= inc; + range[1] += inc; + } + } } - if(ax._id.charAt(0) === 'y') { + var rangeLinear = range.map(ax.r2l); + + if(axLetter === 'y') { ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; ax._length = gs.h * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (range[0] - range[1]); - ax._b = -ax._m * range[1]; + ax._m = ax._length / (rangeLinear[0] - rangeLinear[1]); + ax._b = -ax._m * rangeLinear[1]; } else { ax._offset = gs.l + ax.domain[0] * gs.w; ax._length = gs.w * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (range[1] - range[0]); - ax._b = -ax._m * range[0]; + ax._m = ax._length / (rangeLinear[1] - rangeLinear[0]); + ax._b = -ax._m * rangeLinear[0]; } if(!isFinite(ax._m) || !isFinite(ax._b)) { @@ -143,39 +212,53 @@ module.exports = function setConvert(ax) { if(['linear', 'log', '-'].indexOf(ax.type) !== -1) { ax.c2d = num; - ax.d2c = function(v) { - v = cleanDatum(v); - return isNumeric(v) ? Number(v) : constants.BADNUM; - }; - ax.d2l = function(v, clip) { - if(ax.type === 'log') return ax.c2l(ax.d2c(v), clip); - else return ax.d2c(v); - }; + ax.d2c = cleanNum; + if(ax.type === 'log') { + ax.d2l = function(v, clip) { + return ax.c2l(ax.d2c(v), clip); + }; + ax.d2r = ax.d2l; + ax.r2d = ax.l2d; + } + else { + ax.d2l = cleanNum; + ax.d2r = cleanNum; + ax.r2d = num; + } + ax.r2l = num; + ax.l2r = num; } else if(ax.type === 'date') { ax.c2d = function(v) { - return isNumeric(v) ? Lib.ms2DateTime(v) : constants.BADNUM; + return isNumeric(v) ? Lib.ms2DateTime(v) : BADNUM; }; ax.d2c = function(v) { - return (isNumeric(v)) ? Number(v) : Lib.dateTime2ms(v); + // NOTE: Changed this behavior: previously we took any numeric value + // to be a ms, even if it was a string that could be a bare year. + // Now we convert it as a date if at all possible, and only try + // as ms if that fails. + var ms_from_str = Lib.dateTime2ms(v); + if(ms_from_str === false) { + if(isNumeric(v)) return Number(v); + return BADNUM; + } + return ms_from_str; }; ax.d2l = ax.d2c; + ax.r2l = ax.d2c; + ax.l2r = ax.c2d; + ax.d2r = Lib.identity; // TODO: do we want this to validate? + ax.r2d = Lib.identity; - // check if date strings or js date objects are provided for range - // and convert to ms + // check if milliseconds or js date objects are provided for range + // or tick0 and convert to date strings if(ax.range && ax.range.length > 1) { - try { - var ar1 = ax.range.map(Lib.dateTime2ms); - if(!isNumeric(ax.range[0]) && isNumeric(ar1[0])) { - ax.range[0] = ar1[0]; - } - if(!isNumeric(ax.range[1]) && isNumeric(ar1[1])) { - ax.range[1] = ar1[1]; - } - } - catch(e) { Lib.error(e, ax.range); } + ax.range = ax.range.map(cleanDate); + } + if(ax.tick0 !== undefined) { + ax.tick0 = cleanDate(ax.tick0); } } else if(ax.type === 'category') { @@ -203,6 +286,10 @@ module.exports = function setConvert(ax) { }; ax.d2l = ax.d2c; + ax.r2l = num; + ax.l2r = num; + ax.d2r = ax.d2c; + ax.r2d = ax.c2d; } // makeCalcdata: takes an x or y array and converts it diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 8a31a683ff5..45a3b180b4e 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -494,8 +494,8 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) { var xa = plotinfo.xaxis, ya = plotinfo.yaxis, - xr = d3.extent(xa.range.map(xa.l2c)), - yr = d3.extent(ya.range.map(ya.l2c)); + xr = d3.extent(xa.range.map(xa.r2l).map(xa.l2c)), + yr = d3.extent(ya.range.map(ya.r2l).map(ya.l2c)); var trace = cdscatter[0].trace; if(!subTypes.hasMarkers(trace)) return; diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index fd8a8072549..74835037888 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -180,17 +180,23 @@ describe('range selector defaults:', function() { describe('range selector getUpdateObject:', function() { 'use strict'; - var axisLayout = { - _name: 'xaxis', - range: [ - (new Date(1948, 0, 1)).getTime(), - (new Date(2015, 10, 30)).getTime() - ] - }; - function assertRanges(update, range0, range1) { - expect(update['xaxis.range[0]']).toEqual(range0.getTime()); - expect(update['xaxis.range[1]']).toEqual(range1.getTime()); + expect(update['xaxis.range[0]']).toEqual(range0); + expect(update['xaxis.range[1]']).toEqual(range1); + } + + // buttonLayout: {step, stepmode, count} + // range0out: expected resulting range[0] (input is always '1948-01-01') + // range1: input range[1], expected to also be the output + function assertUpdateCase(buttonLayout, range0out, range1) { + var axisLayout = { + _name: 'xaxis', + range: ['1948-01-01', range1] + }; + + var update = getUpdateObject(axisLayout, buttonLayout); + + assertRanges(update, range0out, range1); } it('should return update object (1 month backward case)', function() { @@ -200,9 +206,8 @@ describe('range selector getUpdateObject:', function() { count: 1 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 9, 30), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-10-30', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-10-30 12:34:56', '2015-11-30 12:34:56'); }); it('should return update object (3 months backward case)', function() { @@ -212,9 +217,8 @@ describe('range selector getUpdateObject:', function() { count: 3 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 7, 30), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-08-30', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-08-30 12:34:56', '2015-11-30 12:34:56'); }); it('should return update object (6 months backward case)', function() { @@ -224,9 +228,8 @@ describe('range selector getUpdateObject:', function() { count: 6 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 4, 30), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-05-30', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-05-30 12:34:56', '2015-11-30 12:34:56'); }); it('should return update object (5 months to-date case)', function() { @@ -236,9 +239,9 @@ describe('range selector getUpdateObject:', function() { count: 5 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 6, 1), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-07-01', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-07-01', '2015-12-01'); + assertUpdateCase(buttonLayout, '2015-08-01', '2015-12-01 00:00:01'); }); it('should return update object (1 year to-date case)', function() { @@ -248,9 +251,9 @@ describe('range selector getUpdateObject:', function() { count: 1 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 0, 1), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-01-01', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-01-01', '2016-01-01'); + assertUpdateCase(buttonLayout, '2016-01-01', '2016-01-01 00:00:01'); }); it('should return update object (10 year to-date case)', function() { @@ -260,9 +263,9 @@ describe('range selector getUpdateObject:', function() { count: 10 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2006, 0, 1), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2006-01-01', '2015-11-30'); + assertUpdateCase(buttonLayout, '2006-01-01', '2016-01-01'); + assertUpdateCase(buttonLayout, '2007-01-01', '2016-01-01 00:00:01'); }); it('should return update object (1 year backward case)', function() { @@ -272,19 +275,23 @@ describe('range selector getUpdateObject:', function() { count: 1 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2014, 10, 30), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2014-11-30', '2015-11-30'); + assertUpdateCase(buttonLayout, '2014-11-30 12:34:56', '2015-11-30 12:34:56'); }); it('should return update object (reset case)', function() { + var axisLayout = { + _name: 'xaxis', + range: ['1948-01-01', '2015-11-30'] + }; + var buttonLayout = { step: 'all' }; var update = getUpdateObject(axisLayout, buttonLayout); - expect(update).toEqual({ 'xaxis.autorange': true }); + expect(update).toEqual({'xaxis.autorange': true}); }); it('should return update object (10 day backward case)', function() { @@ -294,9 +301,8 @@ describe('range selector getUpdateObject:', function() { count: 10 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 20), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-11-20', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-11-20 12:34:56', '2015-11-30 12:34:56'); }); it('should return update object (5 hour backward case)', function() { @@ -306,9 +312,8 @@ describe('range selector getUpdateObject:', function() { count: 5 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 29, 19), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-11-29 19', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-11-30 07:34:56', '2015-11-30 12:34:56'); }); it('should return update object (15 minute backward case)', function() { @@ -318,9 +323,8 @@ describe('range selector getUpdateObject:', function() { count: 15 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 29, 23, 45), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-11-29 23:45', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-11-30 12:19:56', '2015-11-30 12:34:56'); }); it('should return update object (10 second backward case)', function() { @@ -330,9 +334,8 @@ describe('range selector getUpdateObject:', function() { count: 10 }; - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 29, 23, 59, 50), new Date(2015, 10, 30)); + assertUpdateCase(buttonLayout, '2015-11-29 23:59:50', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-11-30 12:34:46', '2015-11-30 12:34:56'); }); it('should return update object (12 hour to-date case)', function() { @@ -342,25 +345,21 @@ describe('range selector getUpdateObject:', function() { count: 12 }; - axisLayout.range[1] = new Date(2015, 10, 30, 12).getTime(); - - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 30, 1), new Date(2015, 10, 30, 12)); + assertUpdateCase(buttonLayout, '2015-11-30', '2015-11-30 12'); + assertUpdateCase(buttonLayout, '2015-11-30 01', '2015-11-30 12:00:01'); + assertUpdateCase(buttonLayout, '2015-11-30 01', '2015-11-30 13'); }); - it('should return update object (15 minute backward case)', function() { + it('should return update object (20 minute to-date case)', function() { var buttonLayout = { step: 'minute', stepmode: 'todate', count: 20 }; - axisLayout.range[1] = new Date(2015, 10, 30, 12, 20).getTime(); - - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 30, 12, 1), new Date(2015, 10, 30, 12, 20)); + assertUpdateCase(buttonLayout, '2015-11-30 12', '2015-11-30 12:20'); + assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:20:01'); + assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:21'); }); it('should return update object (2 second to-date case)', function() { @@ -370,20 +369,15 @@ describe('range selector getUpdateObject:', function() { count: 2 }; - axisLayout.range[1] = new Date(2015, 10, 30, 12, 20, 2).getTime(); - - var update = getUpdateObject(axisLayout, buttonLayout); - - assertRanges(update, new Date(2015, 10, 30, 12, 20, 1), new Date(2015, 10, 30, 12, 20, 2)); + assertUpdateCase(buttonLayout, '2015-11-30 12:20', '2015-11-30 12:20:02'); + assertUpdateCase(buttonLayout, '2015-11-30 12:20:01', '2015-11-30 12:20:02.001'); + assertUpdateCase(buttonLayout, '2015-11-30 12:20:01', '2015-11-30 12:20:03'); }); it('should return update object with correct axis names', function() { var axisLayout = { _name: 'xaxis5', - range: [ - (new Date(1948, 0, 1)).getTime(), - (new Date(2015, 10, 30)).getTime() - ] + range: ['1948-01-01', '2015-11-30'] }; var buttonLayout = { @@ -395,8 +389,8 @@ describe('range selector getUpdateObject:', function() { var update = getUpdateObject(axisLayout, buttonLayout); expect(update).toEqual({ - 'xaxis5.range[0]': new Date(2015, 9, 30).getTime(), - 'xaxis5.range[1]': new Date(2015, 10, 30).getTime() + 'xaxis5.range[0]': '2015-10-30', + 'xaxis5.range[1]': '2015-11-30' }); }); @@ -422,9 +416,9 @@ describe('range selector interactions:', function() { expect(d3.selectAll(query).size()).toEqual(cnt); } - function checkActiveButton(activeIndex) { + function checkActiveButton(activeIndex, msg) { d3.selectAll('.button').each(function(d, i) { - expect(d.isActive).toBe(activeIndex === i); + expect(d.isActive).toBe(activeIndex === i, msg + ': button #' + i); }); } @@ -524,23 +518,23 @@ describe('range selector interactions:', function() { var buttons = d3.selectAll('.button').select('rect'); // 'all' should be active at first - checkActiveButton(buttons.size() - 1); + checkActiveButton(buttons.size() - 1, 'initial'); var update = { - 'xaxis.range[0]': (new Date(2015, 9, 30)).getTime(), - 'xaxis.range[1]': (new Date(2015, 10, 30)).getTime() + 'xaxis.range[0]': '2015-10-30', + 'xaxis.range[1]': '2015-11-30' }; Plotly.relayout(gd, update).then(function() { // '1m' should be active after the relayout - checkActiveButton(0); + checkActiveButton(0, '1m'); return Plotly.relayout(gd, 'xaxis.autorange', true); }).then(function() { // 'all' should be after an autoscale - checkActiveButton(buttons.size() - 1); + checkActiveButton(buttons.size() - 1, 'back to all'); done(); }); From 9784b3b2e70a5a975a3a07b94dcebd89d5c45f21 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 14 Oct 2016 22:28:30 -0400 Subject: [PATCH 03/23] axe parseDate - moved to streambed --- src/lib/dates.js | 184 --------------------------------- src/lib/index.js | 1 - test/jasmine/tests/lib_test.js | 33 ------ 3 files changed, 218 deletions(-) diff --git a/src/lib/dates.js b/src/lib/dates.js index ae0cb32bb1a..46beaacc8f1 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -187,187 +187,3 @@ exports.ms2DateTime = function(ms, r) { } return s; }; - -/** - * parseDate: forgiving attempt to turn any date string - * into a javascript date object - * - * first collate all the date formats we want to support, precompiled - * to d3 format objects see below for the string cleaning that happens - * before this separate out 2-digit (y) and 4-digit-year (Y) formats, - * formats with month names (b), and formats with am/pm (I) or no time (D) - * (also includes hour only, as the test is really for a colon) so we can - * cut down the number of tests we need to run for any given string - * (right now all are between 15 and 32 tests) - */ - -// TODO: this is way out of date vs. the server-side version -var timeFormats = { - // 24 hour - H: ['%H:%M:%S~%L', '%H:%M:%S', '%H:%M'], - // with am/pm - I: ['%I:%M:%S~%L%p', '%I:%M:%S%p', '%I:%M%p'], - // no colon, ie only date or date with hour (could also support eg 12h34m?) - D: ['%H', '%I%p', '%Hh'] -}; - -var dateFormats = { - Y: [ - '%Y~%m~%d', - '%Y%m%d', - '%y%m%d', // YYMMDD, has 6 digits together so will match Y, not y - '%m~%d~%Y', // MM/DD/YYYY has first precedence - '%d~%m~%Y' // then DD/MM/YYYY - ], - Yb: [ - '%b~%d~%Y', // eg nov 21 2013 - '%d~%b~%Y', // eg 21 nov 2013 - '%Y~%d~%b', // eg 2013 21 nov (or 2013 q3, after replacement) - '%Y~%b~%d' // eg 2013 nov 21 - ], - /** - * the two-digit year cases have so many potential ambiguities - * it's not even funny, but we'll try them anyway. - */ - y: [ - '%m~%d~%y', - '%d~%m~%y', - '%y~%m~%d' - ], - yb: [ - '%b~%d~%y', - '%d~%b~%y', - '%y~%d~%b', - '%y~%b~%d' - ] -}; - -// use utc formatter since we're ignoring timezone info -var formatter = d3.time.format.utc; - -/** - * ISO8601 and YYYYMMDDHHMMSS are the only ones where date and time - * are not separated by a space, so they get inserted specially here. - * Also a couple formats with no day (so time makes no sense) - */ -var dateTimeFormats = { - Y: { - H: ['%Y~%m~%dT%H:%M:%S', '%Y~%m~%dT%H:%M:%S~%L'].map(formatter), - I: [], - D: ['%Y%m%d%H%M%S', '%Y~%m', '%m~%Y'].map(formatter) - }, - Yb: {H: [], I: [], D: ['%Y~%b', '%b~%Y'].map(formatter)}, - y: {H: [], I: [], D: []}, - yb: {H: [], I: [], D: []} -}; -// all others get inserted in all possible combinations from dateFormats and timeFormats -['Y', 'Yb', 'y', 'yb'].forEach(function(dateType) { - dateFormats[dateType].forEach(function(dateFormat) { - // just a date (don't do just a time) - dateTimeFormats[dateType].D.push(formatter(dateFormat)); - ['H', 'I', 'D'].forEach(function(timeType) { - timeFormats[timeType].forEach(function(timeFormat) { - var a = dateTimeFormats[dateType][timeType]; - - // 'date time', then 'time date' - a.push(formatter(dateFormat + '~' + timeFormat)); - a.push(formatter(timeFormat + '~' + dateFormat)); - }); - }); - }); -}); - -// precompiled regexps for performance -var matchword = /[a-z]*/g, - shortenword = function(m) { return m.substr(0, 3); }, - weekdaymatch = /(mon|tue|wed|thu|fri|sat|sun|the|of|st|nd|rd|th)/g, - separatormatch = /[\s,\/\-\.\(\)]+/g, - ampmmatch = /~?([ap])~?m(~|$)/, - replaceampm = function(m, ap) { return ap + 'm '; }, - match4Y = /\d\d\d\d/, - matchMonthName = /(^|~)[a-z]{3}/, - matchAMPM = /[ap]m/, - matchcolon = /:/, - matchquarter = /q([1-4])/, - quarters = ['31~mar', '30~jun', '30~sep', '31~dec'], - replacequarter = function(m, n) { return quarters[n - 1]; }, - matchTZ = / ?([+\-]\d\d:?\d\d|Z)$/; - -function getDateType(v) { - var dateType; - dateType = (match4Y.test(v) ? 'Y' : 'y'); - dateType = dateType + (matchMonthName.test(v) ? 'b' : ''); - return dateType; -} - -function getTimeType(v) { - var timeType; - timeType = matchcolon.test(v) ? (matchAMPM.test(v) ? 'I' : 'H') : 'D'; - return timeType; -} - -exports.parseDate = function(v) { - // is it already a date? just return it - if(v.getTime) return v; - /** - * otherwise, if it's not a string, return nothing - * the case of numbers that just have years will get - * dealt with elsewhere. - */ - if(typeof v !== 'string') return false; - - // first clean up the string a bit to reduce the number of formats we have to test - v = v.toLowerCase() - /** - * cut all words down to 3 characters - this will result in - * some spurious matches, ie whenever the first three characters - * of a word match a month or weekday but that seems more likely - * to fix typos than to make dates where they shouldn't be... - * and then we can omit the long form of months from our testing - */ - .replace(matchword, shortenword) - /** - * remove weekday names, as they get overridden anyway if they're - * inconsistent also removes a few more words - * (ie "tuesday the 26th of november") - * TODO: language support? - * for months too, but these seem to be built into d3 - */ - .replace(weekdaymatch, '') - /** - * collapse all separators one ~ at a time, except : which seems - * pretty consistent for the time part use ~ instead of space or - * something since d3 can eat a space as padding on 1-digit numbers - */ - .replace(separatormatch, '~') - // in case of a.m. or p.m. (also take off any space before am/pm) - .replace(ampmmatch, replaceampm) - // turn quarters Q1-4 into dates (quarter ends) - .replace(matchquarter, replacequarter) - .trim() - // also try to ignore timezone info, at least for now - .replace(matchTZ, ''); - - // now test against the various formats that might match - var out = null, - dateType = getDateType(v), - timeType = getTimeType(v), - formatList, - len; - - formatList = dateTimeFormats[dateType][timeType]; - len = formatList.length; - - for(var i = 0; i < len; i++) { - out = formatList[i].parse(v); - if(out) break; - } - - // If not an instance of Date at this point, just return it. - if(!(out instanceof Date)) return false; - // parse() method interprets arguments with local time zone. - var tzoff = out.getTimezoneOffset(); - // In general (default) this is not what we want, so force into UTC: - out.setTime(out.getTime() + tzoff * 60 * 1000); - return out; -}; diff --git a/src/lib/index.js b/src/lib/index.js index e98b0309491..857adcbc931 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -35,7 +35,6 @@ var datesModule = require('./dates'); lib.dateTime2ms = datesModule.dateTime2ms; lib.isDateTime = datesModule.isDateTime; lib.ms2DateTime = datesModule.ms2DateTime; -lib.parseDate = datesModule.parseDate; lib.MIN_MS = datesModule.MIN_MS; lib.MAX_MS = datesModule.MAX_MS; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 2ed3fccf402..39f753ae0bc 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -12,39 +12,6 @@ var Plots = PlotlyInternal.Plots; describe('Test lib.js:', function() { 'use strict'; - describe('parseDate() should', function() { - it('return false on bad (number) input:', function() { - expect(Lib.parseDate(0)).toBe(false); - }); - it('return false on bad (string) input:', function() { - expect(Lib.parseDate('toto')).toBe(false); - }); - it('work with yyyy-mm-dd string input:', function() { - var input = '2014-12-01', - res = Lib.parseDate(input), - res0 = new Date(2014, 11, 1); - expect(res.getTime()).toEqual(res0.getTime()); - }); - it('work with mm/dd/yyyy string input:', function() { - var input = '12/01/2014', - res = Lib.parseDate(input), - res0 = new Date(2014, 11, 1); - expect(res.getTime()).toEqual(res0.getTime()); - }); - it('work with yyyy-mm-dd HH:MM:SS.sss string input:', function() { - var input = '2014-12-01 09:50:05.124', - res = Lib.parseDate(input), - res0 = new Date(2014, 11, 1, 9, 50, 5, 124); - expect(res.getTime()).toEqual(res0.getTime()); - }); - it('work with mm/dd/yyyy HH:MM:SS string input:', function() { - var input = '2014-12-01 09:50:05', - res = Lib.parseDate(input), - res0 = new Date(2014, 11, 1, 9, 50, 5); - expect(res.getTime()).toEqual(res0.getTime()); - }); - }); - describe('interp() should', function() { it('return 1.75 as Q1 of [1, 2, 3, 4, 5]:', function() { var input = [1, 2, 3, 4, 5], From 1db4448f74db0ab1a53b0179ca43778527dfa69a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 24 Oct 2016 17:02:03 -0400 Subject: [PATCH 04/23] refactor dates handling to use date strings as much as possible for axis ranges, annotation & image positions... everything that's an absolute value, not a delta (so dtick is not changed, for example) Apologies in advance to reviewers, I couldn't see a way to break this up. --- .../annotations/annotation_defaults.js | 77 +++--- src/components/annotations/attributes.js | 34 ++- src/components/annotations/calc_autorange.js | 4 +- src/components/annotations/draw.js | 40 +-- src/components/images/attributes.js | 4 +- .../rangeselector/get_update_object.js | 4 +- src/components/rangeslider/attributes.js | 12 +- src/components/rangeslider/defaults.js | 15 +- src/components/rangeslider/draw.js | 37 +-- src/components/shapes/draw.js | 28 +- src/components/shapes/helpers.js | 22 +- src/components/shapes/shape_defaults.js | 43 ++- src/lib/clean_number.js | 32 +++ src/lib/dates.js | 117 +++++--- src/lib/index.js | 4 + src/plots/cartesian/axes.js | 96 +++++-- src/plots/cartesian/axis_defaults.js | 22 +- src/plots/cartesian/clean_datum.js | 37 --- src/plots/cartesian/dragbox.js | 8 +- src/plots/cartesian/layout_attributes.js | 14 +- src/plots/cartesian/set_convert.js | 197 +++++++------ src/plots/cartesian/tick_value_defaults.js | 12 +- src/plots/plots.js | 4 +- src/traces/ohlc/transform.js | 38 ++- test/image/baselines/shapes.png | Bin 27174 -> 27190 bytes test/jasmine/tests/annotations_test.js | 26 +- test/jasmine/tests/finance_test.js | 20 +- test/jasmine/tests/lib_date_test.js | 260 ++++++++++++++++++ test/jasmine/tests/lib_test.js | 36 +++ test/jasmine/tests/range_selector_test.js | 8 +- 30 files changed, 852 insertions(+), 399 deletions(-) create mode 100644 src/lib/clean_number.js delete mode 100644 src/plots/cartesian/clean_datum.js create mode 100644 test/jasmine/tests/lib_date_test.js diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index 36acea5d457..7b5f2da9689 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -35,69 +35,56 @@ module.exports = function handleAnnotationDefaults(annIn, fullLayout) { var borderWidth = coerce('borderwidth'); var showArrow = coerce('showarrow'); - if(showArrow) { - coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine); - coerce('arrowhead'); - coerce('arrowsize'); - coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2); - coerce('ax'); - coerce('ay'); - coerce('axref'); - coerce('ayref'); - - // if you have one part of arrow length you should have both - Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); - } - coerce('text', showArrow ? ' ' : 'new text'); coerce('textangle'); Lib.coerceFont(coerce, 'font', fullLayout.font); // positioning - var axLetters = ['x', 'y']; + var axLetters = ['x', 'y'], + arrowPosDflt = [-10, -30], + tdMock = {_fullLayout: fullLayout}; for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i], - tdMock = {_fullLayout: fullLayout}; + var axLetter = axLetters[i]; // xref, yref - var axRef = Axes.coerceRef(annIn, annOut, tdMock, axLetter); - - // TODO: should be refactored in conjunction with Axes axref, ayref - var aaxRef = Axes.coerceARef(annIn, annOut, tdMock, axLetter); + var axRef = Axes.coerceRef(annIn, annOut, tdMock, axLetter, '', 'paper'); // x, y - var defaultPosition = 0.5; - if(axRef !== 'paper') { - var ax = Axes.getFromId(tdMock, axRef); - defaultPosition = ax.range[0] + defaultPosition * (ax.range[1] - ax.range[0]); - - // convert date or category strings to numbers - if(['date', 'category'].indexOf(ax.type) !== -1 && - typeof annIn[axLetter] === 'string') { - var newval; - if(ax.type === 'date') { - newval = Lib.dateTime2ms(annIn[axLetter]); - if(newval !== false) annIn[axLetter] = newval; - - if(aaxRef === axRef) { - var newvalB = Lib.dateTime2ms(annIn['a' + axLetter]); - if(newvalB !== false) annIn['a' + axLetter] = newvalB; - } - } - else if((ax._categories || []).length) { - newval = ax._categories.indexOf(annIn[axLetter]); - if(newval !== -1) annIn[axLetter] = newval; - } + Axes.coercePosition(annOut, tdMock, coerce, axRef, axLetter, 0.5); + + if(showArrow) { + var arrowPosAttr = 'a' + axLetter, + // axref, ayref + aaxRef = Axes.coerceRef(annIn, annOut, tdMock, arrowPosAttr, 'pixel'); + + // for now the arrow can only be on the same axis or specified as pixels + // TODO: sometime it might be interesting to allow it to be on *any* axis + // but that would require updates to drawing & autorange code and maybe more + if(aaxRef !== 'pixel' && aaxRef !== axRef) { + aaxRef = annOut[arrowPosAttr] = 'pixel'; } + + // ax, ay + var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4; + Axes.coercePosition(annOut, tdMock, coerce, aaxRef, arrowPosAttr, aDflt); } - coerce(axLetter, defaultPosition); // xanchor, yanchor - if(!showArrow) coerce(axLetter + 'anchor'); + else coerce(axLetter + 'anchor'); } // if you have one coordinate you should have both Lib.noneOrAll(annIn, annOut, ['x', 'y']); + if(showArrow) { + coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine); + coerce('arrowhead'); + coerce('arrowsize'); + coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2); + + // if you have one part of arrow length you should have both + Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); + } + return annOut; }; diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index cea6afb9a4a..221e0ca3918 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -132,27 +132,27 @@ module.exports = { description: 'Sets the width (in px) of annotation arrow.' }, ax: { - valType: 'number', - dflt: -10, + valType: 'any', role: 'info', description: [ 'Sets the x component of the arrow tail about the arrow head.', 'If `axref` is `pixel`, a positive (negative) ', 'component corresponds to an arrow pointing', 'from right to left (left to right).', - 'If `axref` is an axis, this is a value on that axis.' + 'If `axref` is an axis, this is an absolute value on that axis,', + 'like `x`, NOT a relative value.' ].join(' ') }, ay: { - valType: 'number', - dflt: -30, + valType: 'any', role: 'info', description: [ 'Sets the y component of the arrow tail about the arrow head.', 'If `ayref` is `pixel`, a positive (negative) ', 'component corresponds to an arrow pointing', 'from bottom to top (top to bottom).', - 'If `ayref` is an axis, this is a value on that axis.' + 'If `ayref` is an axis, this is an absolute value on that axis,', + 'like `y`, NOT a relative value.' ].join(' ') }, axref: { @@ -207,11 +207,18 @@ module.exports = { ].join(' ') }, x: { - valType: 'number', + valType: 'any', role: 'info', description: [ 'Sets the annotation\'s x position.', - 'Note that dates and categories are converted to numbers.' + 'If the axis `type` is *log*, then you must take the', + 'log of your desired range.', + 'If the axis `type` is *date*, it should be date strings,', + 'like date data, though Date objects and unix milliseconds', + 'will be accepted and converted to strings.', + 'If the axis `type` is *category*, it should be numbers,', + 'using the scale where each category is assigned a serial', + 'number from zero in the order it appears.' ].join(' ') }, xanchor: { @@ -250,11 +257,18 @@ module.exports = { ].join(' ') }, y: { - valType: 'number', + valType: 'any', role: 'info', description: [ 'Sets the annotation\'s y position.', - 'Note that dates and categories are converted to numbers.' + 'If the axis `type` is *log*, then you must take the', + 'log of your desired range.', + 'If the axis `type` is *date*, it should be date strings,', + 'like date data, though Date objects and unix milliseconds', + 'will be accepted and converted to strings.', + 'If the axis `type` is *category*, it should be numbers,', + 'using the scale where each category is assigned a serial', + 'number from zero in the order it appears.' ].join(' ') }, yanchor: { diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index ba2261f3f4c..989fc442557 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -69,14 +69,14 @@ function annAutorange(gd) { } if(xa && xa.autorange) { - Axes.expand(xa, [xa.l2c(ann.x)], { + Axes.expand(xa, [xa.l2c(xa.r2l(ann.x))], { ppadplus: rightSize, ppadminus: leftSize }); } if(ya && ya.autorange) { - Axes.expand(ya, [ya.l2c(ann.y)], { + Axes.expand(ya, [ya.l2c(ya.r2l(ann.y))], { ppadplus: bottomSize, ppadminus: topSize }); diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 2b104001477..563c4cefe7e 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -168,12 +168,19 @@ function drawOne(gd, index, opt, value) { continue; } - var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter)), - axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter)), + var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), + axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), position = optionsIn[axLetter], axTypeOld = oldPrivate['_' + axLetter + 'type']; if(optionsEdit[axLetter + 'ref'] !== undefined) { + + // TODO: include ax / ay / axref / ayref here if not 'pixel' + // or even better, move all of this machinery out of here and into + // streambed as extra attributes to a regular relayout call + // we should do this after v2.0 when it can work equivalently for + // annotations, shapes, and images. + var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto', plotSize = (axLetter === 'x' ? gs.w : gs.h), halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) / @@ -182,18 +189,11 @@ function drawOne(gd, index, opt, value) { // go to the same fraction of the axis length // whether or not these axes share a domain - // first convert to fraction of the axis - position = (position - axOld.range[0]) / - (axOld.range[1] - axOld.range[0]); - - // then convert to new data coordinates at the same fraction - position = axNew.range[0] + - position * (axNew.range[1] - axNew.range[0]); + position = axNew.fraction2r(axOld.r2fraction(position)); } else if(axOld) { // data -> paper // first convert to fraction of the axis - position = (position - axOld.range[0]) / - (axOld.range[1] - axOld.range[0]); + position = axOld.r2fraction(position); // next scale the axis to the whole plot position = axOld.domain[0] + @@ -221,8 +221,7 @@ function drawOne(gd, index, opt, value) { (axNew.domain[1] - axNew.domain[0]); // finally convert to data coordinates - position = axNew.range[0] + - position * (axNew.range[1] - axNew.range[0]); + position = axNew.fraction2r(position); } } @@ -353,20 +352,21 @@ function drawOne(gd, index, opt, value) { // outside the visible plot (as long as the axis // isn't autoranged - then we need to draw it // anyway to get its bounding box) - if(!ax.autorange && ((options[axLetter] - ax.range[0]) * - (options[axLetter] - ax.range[1]) > 0)) { + var posFraction = ax.r2fraction(options[axLetter]); + if(!ax.autorange && (posFraction < 0 || posFraction > 1)) { if(options['a' + axLetter + 'ref'] === axRef) { - if((options['a' + axLetter] - ax.range[0]) * - (options['a' + axLetter] - ax.range[1]) > 0) { + posFraction = ax.r2fraction(options['a' + axLetter]); + if(posFraction < 0 || posFraction > 1) { annotationIsOffscreen = true; } - } else { + } + else { annotationIsOffscreen = true; } if(annotationIsOffscreen) return; } - annPosPx[axLetter] = ax._offset + ax.l2p(options[axLetter]); + annPosPx[axLetter] = ax._offset + ax.l2p(ax.r2l(options[axLetter])); alignPosition = 0.5; } else { @@ -379,7 +379,7 @@ function drawOne(gd, index, opt, value) { var alignShift = 0; if(options['a' + axLetter + 'ref'] === axRef) { - annPosPx['aa' + axLetter] = ax._offset + ax.l2p(options['a' + axLetter]); + annPosPx['aa' + axLetter] = ax._offset + ax.l2p(ax.r2l(options['a' + axLetter])); } else { if(options.showarrow) { alignShift = options['a' + axLetter]; diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js index a7f35575256..7e5fc74d2e2 100644 --- a/src/components/images/attributes.js +++ b/src/components/images/attributes.js @@ -81,7 +81,7 @@ module.exports = { }, x: { - valType: 'number', + valType: 'any', role: 'info', dflt: 0, description: [ @@ -93,7 +93,7 @@ module.exports = { }, y: { - valType: 'number', + valType: 'any', role: 'info', dflt: 0, description: [ diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js index acb9ae9e087..fe6eacaa454 100644 --- a/src/components/rangeselector/get_update_object.js +++ b/src/components/rangeselector/get_update_object.js @@ -42,13 +42,13 @@ function getXRange(axisLayout, buttonLayout) { switch(buttonLayout.stepmode) { case 'backward': - range0 = Lib.ms2DateTime(d3.time[step].offset(base, -count)); + range0 = Lib.ms2DateTime(+d3.time[step].offset(base, -count)); break; case 'todate': var base2 = d3.time[step].offset(base, -count); - range0 = Lib.ms2DateTime(d3.time[step].ceil(base2)); + range0 = Lib.ms2DateTime(+d3.time[step].ceil(base2)); break; } diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index e6d58be6e06..bb25d074c40 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -34,16 +34,20 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'number'}, - {valType: 'number'} + {valType: 'any'}, + {valType: 'any'} ], description: [ 'Sets the range of the range slider.', 'If not set, defaults to the full xaxis range.', 'If the axis `type` is *log*, then you must take the', 'log of your desired range.', - 'If the axis `type` is *date*, then you must convert', - 'the date to unix time in milliseconds.' + 'If the axis `type` is *date*, it should be date strings,', + 'like date data, though Date objects and unix milliseconds', + 'will be accepted and converted to strings.', + 'If the axis `type` is *category*, it should be numbers,', + 'using the scale where each category is assigned a serial', + 'number from zero in the order it appears.' ].join(' ') }, thickness: { diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 6e745a5624c..01c07091e01 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -21,7 +21,8 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe } var containerIn = layoutIn[axName].rangeslider, - containerOut = layoutOut[axName].rangeslider = {}; + axOut = layoutOut[axName], + containerOut = axOut.rangeslider = {}; function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); @@ -35,14 +36,16 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe coerce('range'); // Expand slider range to the axis range - if(containerOut.range && !layoutOut[axName].autorange) { + if(containerOut.range && !axOut.autorange) { var outRange = containerOut.range, - axRange = layoutOut[axName].range; + axRange = axOut.range, + l2r = axOut.l2r, + r2l = axOut.r2l; - outRange[0] = Math.min(outRange[0], axRange[0]); - outRange[1] = Math.max(outRange[1], axRange[1]); + outRange[0] = l2r(Math.min(r2l(outRange[0]), r2l(axRange[0]))); + outRange[1] = l2r(Math.max(r2l(outRange[1]), r2l(axRange[1]))); } else { - layoutOut[axName]._needsExpand = true; + axOut._needsExpand = true; } if(containerOut.visible) { diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index b3ea4b1ecc0..2a92d8d3091 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -104,20 +104,10 @@ module.exports = function(gd) { rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')'); - // update inner nodes - - rangeSlider - .call(drawBg, gd, axisOpts, opts) - .call(addClipPath, gd, axisOpts, opts) - .call(drawRangePlot, gd, axisOpts, opts) - .call(drawMasks, gd, axisOpts, opts) - .call(drawSlideBox, gd, axisOpts, opts) - .call(drawGrabbers, gd, axisOpts, opts); - // update data <--> pixel coordinate conversion methods - var range0 = opts.range[0], - range1 = opts.range[1], + var range0 = axisOpts.r2l(opts.range[0]), + range1 = axisOpts.r2l(opts.range[1]), dist = range1 - range0; opts.p2d = function(v) { @@ -128,6 +118,18 @@ module.exports = function(gd) { return (v - range0) / dist * opts._width; }; + opts._rl = [range0, range1]; + + // update inner nodes + + rangeSlider + .call(drawBg, gd, axisOpts, opts) + .call(addClipPath, gd, axisOpts, opts) + .call(drawRangePlot, gd, axisOpts, opts) + .call(drawMasks, gd, axisOpts, opts) + .call(drawSlideBox, gd, axisOpts, opts) + .call(drawGrabbers, gd, axisOpts, opts); + // setup drag element setupDragElement(rangeSlider, gd, axisOpts, opts); @@ -165,8 +167,8 @@ function setupDragElement(rangeSlider, gd, axisOpts, opts) { target = event.target, startX = event.clientX, offsetX = startX - rangeSlider.node().getBoundingClientRect().left, - minVal = opts.d2p(axisOpts.range[0]), - maxVal = opts.d2p(axisOpts.range[1]); + minVal = opts.d2p(axisOpts._rl[0]), + maxVal = opts.d2p(axisOpts._rl[1]); var dragCover = dragElement.coverSlip(); @@ -227,7 +229,7 @@ function setupDragElement(rangeSlider, gd, axisOpts, opts) { function setDataRange(rangeSlider, gd, axisOpts, opts) { function clamp(v) { - return Lib.constrain(v, opts.range[0], opts.range[1]); + return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1])); } var dataMin = clamp(opts.p2d(opts._pixelMin)), @@ -244,8 +246,8 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) { return Lib.constrain(v, 0, opts._width); } - var pixelMin = clamp(opts.d2p(axisOpts.range[0])), - pixelMax = clamp(opts.d2p(axisOpts.range[1])); + var pixelMin = clamp(opts.d2p(axisOpts._rl[0])), + pixelMax = clamp(opts.d2p(axisOpts._rl[1])); rangeSlider.select('rect.' + constants.slideBoxClassName) .attr('x', pixelMin) @@ -338,6 +340,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { data: [], layout: { xaxis: { + type: axisOpts.type, domain: [0, 1], range: opts.range.slice() }, diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 7b5e67d99b7..7ba8b4168f7 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -197,18 +197,17 @@ function updateShape(gd, index, opt, value) { var axLetter = posAttr.charAt(0), axOld = Axes.getFromId(gd, - Axes.coerceRef(oldRef, {}, gd, axLetter)), + Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), axNew = Axes.getFromId(gd, - Axes.coerceRef(optionsIn, {}, gd, axLetter)), + Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), position = optionsIn[posAttr], - linearizedPosition; + rangePosition; if(optionsEdit[axLetter + 'ref'] !== undefined) { // first convert to fraction of the axis if(axOld) { - linearizedPosition = helpers.dataToLinear(axOld)(position); - position = (linearizedPosition - axOld.range[0]) / - (axOld.range[1] - axOld.range[0]); + rangePosition = helpers.shapePositionToRange(axOld)(position); + position = axOld.r2fraction(rangePosition); } else { position = (position - axNew.domain[0]) / (axNew.domain[1] - axNew.domain[0]); @@ -216,9 +215,8 @@ function updateShape(gd, index, opt, value) { if(axNew) { // then convert to new data coordinates at the same fraction - linearizedPosition = axNew.range[0] + position * - (axNew.range[1] - axNew.range[0]); - position = helpers.linearToData(axNew)(linearizedPosition); + rangePosition = axNew.fraction2r(position); + position = helpers.rangeToShapePosition(axNew)(rangePosition); } else { // or scale to the whole plot position = axOld.domain[0] + @@ -463,22 +461,22 @@ function getPathString(gd, options) { xa = Axes.getFromId(gd, options.xref), ya = Axes.getFromId(gd, options.yref), gs = gd._fullLayout._size, - x2l, + x2r, x2p, - y2l, + y2r, y2p; if(xa) { - x2l = helpers.dataToLinear(xa); - x2p = function(v) { return xa._offset + xa.l2p(x2l(v, true)); }; + x2r = helpers.shapePositionToRange(xa); + x2p = function(v) { return xa._offset + xa.l2p(xa.r2l(x2r(v, true))); }; } else { x2p = function(v) { return gs.l + gs.w * v; }; } if(ya) { - y2l = helpers.dataToLinear(ya); - y2p = function(v) { return ya._offset + ya.l2p(y2l(v, true)); }; + y2r = helpers.shapePositionToRange(ya); + y2p = function(v) { return ya._offset + ya.l2p(ya.r2l(y2r(v, true))); }; } else { y2p = function(v) { return gs.t + gs.h * (1 - v); }; diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index d4a5fc5fba9..7d816d883bc 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -14,16 +14,16 @@ // so these have to be specified in terms of the category serial numbers, // but can take fractional values. Other axis types we specify position based on // the actual data values. -// TODO: this should really be part of axes, but for now it's only used here. -// eventually annotations and axis ranges will use this too. -// what should we do, invent a new letter for "data except if it's category"? +// TODO: in V2.0 (when log axis ranges are in data units) range and shape position +// will be identical, so rangeToShapePosition and shapePositionToRange can be +// removed entirely. -exports.dataToLinear = function(ax) { - return ax.type === 'category' ? ax.c2l : ax.d2l; +exports.rangeToShapePosition = function(ax) { + return (ax.type === 'log') ? ax.r2d : function(v) { return v; }; }; -exports.linearToData = function(ax) { - return ax.type === 'category' ? ax.l2c : ax.l2d; +exports.shapePositionToRange = function(ax) { + return (ax.type === 'log') ? ax.d2r : function(v) { return v; }; }; exports.decodeDate = function(convertToPx) { @@ -42,10 +42,10 @@ exports.getDataToPixel = function(gd, axis, isVertical) { dataToPixel; if(axis) { - var d2l = exports.dataToLinear(axis); + var d2r = exports.shapePositionToRange(axis); dataToPixel = function(v) { - return axis._offset + axis.l2p(d2l(v, true)); + return axis._offset + axis.l2p(axis.r2l(d2r(v, true))); }; if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); @@ -65,8 +65,8 @@ exports.getPixelToData = function(gd, axis, isVertical) { pixelToData; if(axis) { - var l2d = exports.linearToData(axis); - pixelToData = function(p) { return l2d(axis.p2l(p - axis._offset)); }; + var r2d = exports.rangeToShapePosition(axis); + pixelToData = function(p) { return r2d(axis.l2r(axis.p2l(p - axis._offset))); }; } else if(isVertical) { pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; }; diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js index 88e4c98ba93..47bfbea7fff 100644 --- a/src/components/shapes/shape_defaults.js +++ b/src/components/shapes/shape_defaults.js @@ -39,29 +39,54 @@ module.exports = function handleShapeDefaults(shapeIn, fullLayout) { tdMock = {_fullLayout: fullLayout}; // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, tdMock, axLetter); + var axRef = Axes.coerceRef(shapeIn, shapeOut, tdMock, axLetter, '', 'paper'); if(shapeType !== 'path') { var dflt0 = 0.25, - dflt1 = 0.75; + dflt1 = 0.75, + ax, + pos2r, + r2pos; if(axRef !== 'paper') { - var ax = Axes.getFromId(tdMock, axRef), - convertFn = helpers.linearToData(ax); + ax = Axes.getFromId(tdMock, axRef); + r2pos = helpers.rangeToShapePosition(ax); + pos2r = helpers.shapePositionToRange(ax); - dflt0 = convertFn(ax.range[0] + dflt0 * (ax.range[1] - ax.range[0])); - dflt1 = convertFn(ax.range[0] + dflt1 * (ax.range[1] - ax.range[0])); + dflt0 = r2pos(ax.fraction2r(dflt0)); + dflt1 = r2pos(ax.fraction2r(dflt1)); } + else { + pos2r = r2pos = Lib.identity; + } + + // hack until V2.0 when log has regular range behavior - make it look like other + // ranges to send to coerce, then put it back after + // this is all to give reasonable default position behavior on log axes, which is + // a pretty unimportant edge case so we could just ignore this. + var attr0 = axLetter + '0', + attr1 = axLetter + '1', + in0 = shapeIn[attr0], + in1 = shapeIn[attr1]; + shapeIn[attr0] = pos2r(shapeIn[attr0], true); + shapeIn[attr1] = pos2r(shapeIn[attr1], true); // x0, x1 (and y0, y1) - coerce(axLetter + '0', dflt0); - coerce(axLetter + '1', dflt1); + Axes.coercePosition(shapeOut, tdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(shapeOut, tdMock, coerce, axRef, attr1, dflt1); + + // hack part 2 + shapeOut[attr0] = r2pos(shapeOut[attr0]); + shapeOut[attr1] = r2pos(shapeOut[attr1]); + shapeIn[attr0] = in0; + shapeIn[attr1] = in1; } } if(shapeType === 'path') { coerce('path'); - } else { + } + else { Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); } diff --git a/src/lib/clean_number.js b/src/lib/clean_number.js new file mode 100644 index 00000000000..d8736ecf93a --- /dev/null +++ b/src/lib/clean_number.js @@ -0,0 +1,32 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +var BADNUM = require('./constants').BADNUM; + +// precompile these regex's for speed +var FRONTJUNK = /^['"%,$#\s']+/; +var ENDJUNK = /['"%,$#\s']+$/; + +/** + * cleanNumber: remove common leading and trailing cruft + * Always returns either a number or BADNUM. + */ +module.exports = function cleanNumber(v) { + if(typeof v === 'string') { + v = v.replace(FRONTJUNK, '').replace(ENDJUNK, ''); + } + + if(isNumeric(v)) return Number(v); + + return BADNUM; +}; diff --git a/src/lib/dates.js b/src/lib/dates.js index 46beaacc8f1..bd2e4c8b8e5 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -12,6 +12,17 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var BADNUM = require('./constants').BADNUM; +var logError = require('./loggers').error; + +// is an object a javascript date? +exports.isJSDate = function(v) { + return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; +}; + +// The absolute limits of our date-time system +// This is a little weird: we use MIN_MS and MAX_MS in dateTime2ms +// but we use dateTime2ms to calculate them (after defining it!) +var MIN_MS, MAX_MS; /** * dateTime2ms - turn a date object or string s of the form @@ -41,7 +52,7 @@ var BADNUM = require('./constants').BADNUM; * http://docs.python.org/py3k/library/time.html: * 1969-2068 (super forward-looking, but static, not sliding!) * - * lets go with now-70 to now+30, and if anyone runs into this problem + * lets go with now-70 to now+29, and if anyone runs into this problem * they can learn the hard way not to use 2-digit years, as no choice we * make now will cover all possibilities. mostly this will all be taken * care of in initial parsing, should only be an issue for hand-entered data @@ -51,16 +62,19 @@ var BADNUM = require('./constants').BADNUM; exports.dateTime2ms = function(s) { // first check if s is a date object - try { - if(s.getTime) return +s; - } - catch(e) { + if(exports.isJSDate(s)) { + s = Number(s); + if(s >= MIN_MS && s <= MAX_MS) return s; return BADNUM; } + // otherwise only accept strings and numbers + if(typeof s !== 'string' && typeof s !== 'number') return BADNUM; var y, m, d, h; // split date and time parts - var datetime = String(s).split(' '); + // TODO: we strip leading/trailing whitespace but not other + // characters like we do for numbers - do we want to? + var datetime = String(s).trim().split(' '); if(datetime.length > 2) return BADNUM; var p = datetime[0].split('-'); // date part @@ -72,7 +86,8 @@ exports.dateTime2ms = function(s) { p.splice(0, 1); } - if(p.length > 3 || (p.length !== 3 && datetime[1])) return BADNUM; + var plen = p.length; + if(plen > 3 || (plen !== 3 && datetime[1]) || !plen) return BADNUM; // year if(p[0].length === 4) y = Number(p[0]); @@ -90,14 +105,14 @@ exports.dateTime2ms = function(s) { baseDate.setFullYear(CE ? y : -y); if(p.length > 1) { - // month + // month - may be 1 or 2 digits m = Number(p[1]) - 1; // new Date() uses zero-based months if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM; baseDate.setMonth(m); if(p.length > 2) { - // day + // day - may be 1 or 2 digits d = Number(p[2]); if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM; baseDate.setDate(d); @@ -110,9 +125,9 @@ exports.dateTime2ms = function(s) { p = datetime[1].split(':'); if(p.length > 3) return BADNUM; - // hour + // hour - may be 1 or 2 digits h = Number(p[0]); - if(p[0].length > 2 || !(h >= 0 && h <= 23)) return BADNUM; + if(p[0].length > 2 || !p[0].length || !(h >= 0 && h <= 23)) return BADNUM; baseDate.setHours(h); // does that hour exist in this day? (Daylight time!) @@ -122,13 +137,14 @@ exports.dateTime2ms = function(s) { if(p.length > 1) { d = baseDate.getTime(); - // minute + // minute - must be 2 digits m = Number(p[1]); - if(p[1].length > 2 || !(m >= 0 && m <= 59)) return BADNUM; + if(p[1].length !== 2 || !(m >= 0 && m <= 59)) return BADNUM; d += 60000 * m; if(p.length === 2) return d; - // second (and milliseconds) + // second (and milliseconds) - must have 2-digit seconds + if(p[2].split('.')[0].length !== 2) return BADNUM; s = Number(p[2]); if(!(s >= 0 && s < 60)) return BADNUM; return d + s * 1000; @@ -139,8 +155,8 @@ exports.dateTime2ms = function(s) { return baseDate.getTime(); }; -exports.MIN_MS = exports.dateTime2ms('-9999'); -exports.MAX_MS = exports.dateTime2ms('9999'); +MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999'); +MAX_MS = exports.MAX_MS = exports.dateTime2ms('9999-12-31 23:59:59.9999'); // is string s a date? (see above) exports.isDateTime = function(s) { @@ -153,37 +169,60 @@ function lpad(val, digits) { } /** - * Turn ms into string of the form YYYY-mm-dd HH:MM:SS.sss - * Crop any trailing zeros in time, but always leave full date - * (we could choose to crop '-01' from date too)... + * Turn ms into string of the form YYYY-mm-dd HH:MM:SS.ssss + * Crop any trailing zeros in time, except never stop right after hours + * (we could choose to crop '-01' from date too but for now we always + * show the whole date) * Optional range r is the data range that applies, also in ms. * If rng is big, the later parts of time will be omitted */ exports.ms2DateTime = function(ms, r) { + if(typeof ms !== 'number' || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM; + if(!r) r = 0; - if(ms < exports.MIN_MS || ms > exports.MAX_MS) return BADNUM; - - var d = new Date(ms), - s = d3.time.format('%Y-%m-%d')(d); - - if(r < 7776000000) { - // <90 days: add hours - s += ' ' + lpad(d.getHours(), 2); - if(r < 432000000) { - // <5 days: add minutes - s += ':' + lpad(d.getMinutes(), 2); - if(r < 10800000) { - // <3 hours: add seconds - s += ':' + lpad(d.getSeconds(), 2); - if(r < 300000) { - // <5 minutes: add ms - s += '.' + lpad(d.getMilliseconds(), 3); + var d = new Date(Math.floor(ms)), + dateStr = d3.time.format('%Y-%m-%d')(d), + // <90 days: add hours and minutes - never *only* add hours + h = (r < 7776000000) ? d.getHours() : 0, + m = (r < 7776000000) ? d.getMinutes() : 0, + // <3 hours: add seconds + s = (r < 10800000) ? d.getSeconds() : 0, + // <5 minutes: add ms (plus one extra digit, this is msec*10) + msec10 = (r < 300000) ? Math.round((d.getMilliseconds() + (((ms % 1) + 1) % 1)) * 10) : 0; + + // include each part that has nonzero data in or after it + if(h || m || s || msec10) { + dateStr += ' ' + lpad(h, 2) + ':' + lpad(m, 2); + if(s || msec10) { + dateStr += ':' + lpad(s, 2); + if(msec10) { + var digits = 4; + while(msec10 % 10 === 0) { + digits -= 1; + msec10 /= 10; } + dateStr += '.' + lpad(msec10, digits); } } - // strip trailing zeros - return s.replace(/([:\s]00)*\.?[0]*$/, ''); } - return s; + return dateStr; +}; + +// normalize date format to date string, in case it starts as +// a Date object or milliseconds +// optional dflt is the return value if cleaning fails +exports.cleanDate = function(v, dflt) { + if(exports.isJSDate(v) || typeof v === 'number') { + // NOTE: if someone puts in a year as a number rather than a string, + // this will mistakenly convert it thinking it's milliseconds from 1970 + // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds + v = exports.ms2DateTime(+v); + if(!v && dflt !== undefined) return dflt; + } + else if(!exports.isDateTime(v)) { + logError('unrecognized date', v); + return dflt; + } + return v; }; diff --git a/src/lib/index.js b/src/lib/index.js index 857adcbc931..49072b5809b 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -35,6 +35,8 @@ var datesModule = require('./dates'); lib.dateTime2ms = datesModule.dateTime2ms; lib.isDateTime = datesModule.isDateTime; lib.ms2DateTime = datesModule.ms2DateTime; +lib.cleanDate = datesModule.cleanDate; +lib.isJSDate = datesModule.isJSDate; lib.MIN_MS = datesModule.MIN_MS; lib.MAX_MS = datesModule.MAX_MS; @@ -82,6 +84,8 @@ lib.FP_SAFE = constantsModule.FP_SAFE; lib.filterUnique = require('./filter_unique'); +lib.cleanNumber = require('./clean_number'); + /** * swap x and y of the same attribute in container cont * specify attr with a ? in place of x/y diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 5e3ef6bd29e..ba9bdcfde10 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -38,43 +38,85 @@ axes.getFromId = axisIds.getFromId; axes.getFromTrace = axisIds.getFromTrace; -// find the list of possible axes to reference with an xref or yref attribute -// and coerce it to that list -axes.coerceRef = function(containerIn, containerOut, gd, axLetter, dflt) { - var axlist = gd._fullLayout._has('gl2d') ? [] : axes.listIds(gd, axLetter), - refAttr = axLetter + 'ref', +/* + * find the list of possible axes to reference with an xref or yref attribute + * and coerce it to that list + * + * attr: the attribute we're generating a reference for. Should end in 'x' or 'y' + * but can be prefixed, like 'ax' for annotation's arrow x + * dflt: the default to coerce to, or blank to use the first axis (falling back on + * extraOption if there is no axis) + * extraOption: aside from existing axes with this letter, what non-axis value is allowed? + * Only required if it's different from `dflt` + */ +axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) { + var axLetter = attr.charAt(attr.length - 1), + axlist = gd._fullLayout._has('gl2d') ? [] : axes.listIds(gd, axLetter), + refAttr = attr + 'ref', attrDef = {}; + if(!dflt) dflt = axlist[0] || extraOption; + if(!extraOption) extraOption = dflt; + // data-ref annotations are not supported in gl2d yet attrDef[refAttr] = { valType: 'enumerated', - values: axlist.concat(['paper']), - dflt: dflt || axlist[0] || 'paper' + values: axlist.concat(extraOption ? [extraOption] : []), + dflt: dflt }; // xref, yref return Lib.coerce(containerIn, containerOut, attrDef, refAttr); }; -// todo: duplicated per github PR 610. Should be consolidated with axes.coerceRef. -// find the list of possible axes to reference with an axref or ayref attribute -// and coerce it to that list -axes.coerceARef = function(containerIn, containerOut, gd, axLetter, dflt) { - var axlist = gd._fullLayout._has('gl2d') ? [] : axes.listIds(gd, axLetter), - refAttr = 'a' + axLetter + 'ref', - attrDef = {}; - - // data-ref annotations are not supported in gl2d yet +/* + * coerce position attributes (range-type) that can be either on axes or absolute + * (paper or pixel) referenced. The biggest complication here is that we don't know + * before looking at the axis whether the value must be a number or not (it may be + * a date string), so we can't use the regular valType='number' machinery + * + * axRef (string): the axis this position is referenced to, or: + * paper: fraction of the plot area + * pixel: pixels relative to some starting position + * attr (string): the attribute in containerOut we are coercing + * dflt (number): the default position, as a fraction or pixels. If the attribute + * is to be axis-referenced, this will be converted to an axis data value + * + * Also cleans the values, since the attribute definition itself has to say + * valType: 'any' to handle date axes. This allows us to accept: + * - for category axes: category names, and convert them here into serial numbers + * - for date axes: JS Dates or milliseconds, and convert to date strings + * - for other types: coerce them to numbers + */ +axes.coercePosition = function(containerOut, td, coerce, axRef, attr, dflt) { + var pos, + newPos; + + if(axRef === 'paper' || axRef === 'pixel') { + pos = coerce(attr, dflt); + } + else { + var ax = axes.getFromId(td, axRef); - attrDef[refAttr] = { - valType: 'enumerated', - values: axlist.concat(['pixel']), - dflt: dflt || 'pixel' || axlist[0] - }; + dflt = ax.fraction2r(dflt); + pos = coerce(attr, dflt); - // axref, ayref - return Lib.coerce(containerIn, containerOut, attrDef, refAttr); + if(ax.type === 'category') { + // if position is given as a category name, convert it to a number + if(typeof pos === 'string' && (ax._categories || []).length) { + newPos = ax._categories.indexOf(pos); + containerOut[attr] = (newPos !== -1) ? dflt : newPos; + return; + } + } + else if(ax.type === 'date') { + containerOut[attr] = Lib.cleanDate(pos); + return; + } + } + // finally make sure we have a number (unless date type already returned a string) + containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt; }; // empty out types for all axes containing these traces @@ -106,8 +148,8 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) { if(['log', 'category'].indexOf(ax.type) !== -1 || !allow) { ax._minDtick = 0; } - // null means there's nothing there yet - else if(ax._minDtick === null) { + // undefined means there's nothing there yet + else if(ax._minDtick === undefined) { ax._minDtick = newDiff; ax._forceTick0 = newFirst; } @@ -704,7 +746,9 @@ axes.autoTicks = function(ax, roughDTick) { // days if roughDTick > 12h ax.dtick = roundDTick(roughDTick, 86400000, roundDays); // get week ticks on sunday - ax.tick0 = '2000-01-01'; + // this will also move the base tick off 2000-01-01 if dtick is + // 2 or 3 days... but that's a weird enough case that we'll ignore it. + ax.tick0 = '2000-01-02'; } else if(roughDTick > 1800000) { // hours if roughDTick > 30m diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 79ebaf70133..3536bd83c5b 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -23,9 +23,7 @@ var handleTickLabelDefaults = require('./tick_label_defaults'); var handleCategoryOrderDefaults = require('./category_order_defaults'); var setConvert = require('./set_convert'); var orderedCategories = require('./ordered_categories'); -var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); -var constants = require('./constants'); /** @@ -91,23 +89,15 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, var validRange = ( (containerIn.range || []).length === 2 && - isNumeric(containerIn.range[0]) && - isNumeric(containerIn.range[1]) + isNumeric(containerOut.r2l(containerIn.range[0])) && + isNumeric(containerOut.r2l(containerIn.range[1])) ); var autoRange = coerce('autorange', !validRange); if(autoRange) coerce('rangemode'); - var dfltRange; - if(axType === 'date') dfltRange = constants.DFLTRANGEDATE; - else if(letter === 'x') dfltRange = constants.DFLTRANGEX; - else dfltRange = constants.DFLTRANGEY; - - var range = coerce('range', dfltRange.slice()); - if(range[0] === range[1]) { - containerOut.range = [range[0] - 1, range[0] + 1]; - } - Lib.noneOrAll(containerIn.range, containerOut.range, [0, 1]); + coerce('range'); + containerOut.cleanRange(); coerce('fixedrange'); @@ -279,8 +269,8 @@ function category(a) { ai; for(var i = 0; i < a.length; i += inc) { - ai = cleanDatum(a[Math.round(i)]); - if(isNumeric(ai)) curvenums++; + ai = a[Math.round(i)]; + if(Lib.cleanNumber(ai) !== Lib.BADNUM) curvenums++; else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; } diff --git a/src/plots/cartesian/clean_datum.js b/src/plots/cartesian/clean_datum.js deleted file mode 100644 index 086d412fdf3..00000000000 --- a/src/plots/cartesian/clean_datum.js +++ /dev/null @@ -1,37 +0,0 @@ -/** -* Copyright 2012-2016, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); - - -/** - * cleanDatum: removes characters - * same replace criteria used in the grid.js:scrapeCol - * but also handling dates, numbers, and NaN, null, Infinity etc - */ -module.exports = function cleanDatum(c) { - try { - if(typeof c === 'object' && c !== null && c.getTime) { - return Lib.ms2DateTime(c); - } - if(typeof c !== 'string' && !isNumeric(c)) { - return ''; - } - c = c.toString().replace(/['"%,$# ]/g, ''); - } - catch(e) { - Lib.error(e, c); - } - - return c; -}; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 3cdac87c2c8..0df91d1ec77 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -298,16 +298,16 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { function zoomAxRanges(axList, r0Fraction, r1Fraction) { var i, axi, - axRange; + axRangeLinear; for(i = 0; i < axList.length; i++) { axi = axList[i]; if(axi.fixedrange) continue; - axRange = axi.range; + axRangeLinear = axi.range.map(axi.r2l); axi.range = [ - axRange[0] + (axRange[1] - axRange[0]) * r0Fraction, - axRange[0] + (axRange[1] - axRange[0]) * r1Fraction + axi.l2r(axRangeLinear[0] + (axRangeLinear[1] - axRangeLinear[0]) * r0Fraction), + axi.l2r(axRangeLinear[0] + (axRangeLinear[1] - axRangeLinear[0]) * r1Fraction) ]; } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 7b8015f5d67..ed0201c6d92 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -87,11 +87,15 @@ module.exports = { ], description: [ 'Sets the range of this axis.', - 'If the axis `type` is *log*, then you must take the log of your desired range', - '(e.g. to set the range from 1 to 100, set the range from 0 to 2).', - 'If the axis `type` is *date*, it should be date strings, like date data.', - 'If the axis `type` is *category*, it should be numbers, using the scale where', - 'each category is assigned a serial number from zero in the order it appears.' + 'If the axis `type` is *log*, then you must take the log of your', + 'desired range (e.g. to set the range from 1 to 100,', + 'set the range from 0 to 2).', + 'If the axis `type` is *date*, it should be date strings,', + 'like date data, though Date objects and unix milliseconds', + 'will be accepted and converted to strings.', + 'If the axis `type` is *category*, it should be numbers,', + 'using the scale where each category is assigned a serial', + 'number from zero in the order it appears.' ].join(' ') }, diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index cd0d9b92332..4307ea35b67 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -17,7 +17,6 @@ var FP_SAFE = Lib.FP_SAFE; var BADNUM = Lib.BADNUM; var constants = require('./constants'); -var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); @@ -41,10 +40,10 @@ var axisIds = require('./axis_ids'); * shapes will work the same way as ranges, tick0, and annotations * so they can use this conversion too. * - * Creates/updates these conversion functions + * Creates/updates these conversion functions, as well as cleaner functions: + * ax.d2d and ax.clean2r * also clears the autorange bounds ._min and ._max - * and the autotick constraints ._minDtick, ._forceTick0, - * and looks for date ranges that aren't yet in numeric format + * and the autotick constraints ._minDtick, ._forceTick0 */ module.exports = function setConvert(ax) { @@ -78,77 +77,60 @@ module.exports = function setConvert(ax) { return isNumeric(v) ? Number(v) : BADNUM; } - function cleanNum(v) { - v = cleanDatum(v); - return num(v); - } - - // normalize date format to date string, in case it starts as - // a Date object or milliseconds - function cleanDate(v) { - if(v.getTime || typeof v === 'number') { - // NOTE: if someone puts in a year as a number rather than a string, - // this will mistakenly convert it thinking it's milliseconds from 1970 - // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds - return Lib.ms2DateTime(+v); - } - else if(!Lib.isDateTime(v)) { - Lib.error('unrecognized date', v); - } - return v; - } - ax.c2l = (ax.type === 'log') ? toLog : num; ax.l2c = (ax.type === 'log') ? fromLog : num; ax.l2d = function(v) { return ax.c2d(ax.l2c(v)); }; ax.p2d = function(v) { return ax.l2d(ax.p2l(v)); }; - // set scaling to pixels - ax.setScale = function(usePrivateRange) { - var gs = ax._gd._fullLayout._size, - axLetter = ax._id.charAt(0), + /* + * fn to make sure range is a couplet of valid & distinct values + * keep numbers away from the limits of floating point numbers, + * and dates away from the ends of our date system (+/- 9999 years) + * + * optional param rangeAttr: operate on a different attribute, like + * ax._r, rather than ax.range + */ + ax.cleanRange = function(rangeAttr) { + if(!rangeAttr) rangeAttr = 'range'; + var range = ax[rangeAttr], + axLetter = (ax._id || 'x').charAt(0), i, dflt; if(ax.type === 'date') dflt = constants.DFLTRANGEDATE; else if(axLetter === 'y') dflt = constants.DFLTRANGEY; else dflt = constants.DFLTRANGEX; - // TODO cleaner way to handle this case - if(!ax._categories) ax._categories = []; + // make sure we don't later mutate the defaults + dflt = dflt.slice(); - // make sure we have a domain (pull it in from the axis - // this one is overlaying if necessary) - if(ax.overlaying) { - var ax2 = axisIds.getFromId(ax._gd, ax.overlaying); - ax.domain = ax2.domain; + if(!range || range.length !== 2) { + ax[rangeAttr] = dflt; + return; } - // While transitions are occuring, occurring, we get a double-transform - // issue if we transform the drawn layer *and* use the new axis range to - // draw the data. This allows us to construct setConvert using the pre- - // interaction values of the range: - var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range'; - var range = ax[rangeAttr]; - - // make sure we have a range (linearized data values) - // and that it stays away from the limits of javascript numbers - if(!range || range.length !== 2) { - ax[rangeAttr] = range = dflt; + if(ax.type === 'date') { + // check if milliseconds or js date objects are provided for range + // and convert to date strings + range[0] = Lib.cleanDate(range[0]); + range[1] = Lib.cleanDate(range[1]); } + for(i = 0; i < 2; i++) { if(ax.type === 'date') { if(!Lib.isDateTime(range[i])) { - ax[rangeAttr] = range = dflt; + ax[rangeAttr] = dflt; break; } if(range[i] < Lib.MIN_MS) range[i] = Lib.MIN_MS; if(range[i] > Lib.MAX_MS) range[i] = Lib.MAX_MS; - if(range[0] === range[1]) { + if(ax.r2l(range[0]) === ax.r2l(range[1])) { // split by +/- 1 second - range[0] = ax.l2r(ax.r2l(range[0]) - 1000); - range[1] = ax.l2r(ax.r2l(range[1]) + 1000); + var linCenter = Lib.constrain(ax.r2l(range[0]), + Lib.MIN_MS + 1000, Lib.MAX_MS - 1000); + range[0] = ax.l2r(linCenter - 1000); + range[1] = ax.l2r(linCenter + 1000); break; } } @@ -158,7 +140,7 @@ module.exports = function setConvert(ax) { range[i] = range[1 - i] * (i ? 10 : 0.1); } else { - ax[rangeAttr] = range = dflt; + ax[rangeAttr] = dflt; break; } } @@ -167,26 +149,65 @@ module.exports = function setConvert(ax) { else if(range[i] > FP_SAFE) range[i] = FP_SAFE; if(range[0] === range[1]) { + // somewhat arbitrary: split by 1 or 1ppm, whichever is bigger var inc = Math.max(1, Math.abs(range[0] * 1e-6)); range[0] -= inc; range[1] += inc; } } } + }; - var rangeLinear = range.map(ax.r2l); + // find the range value at the specified (linear) fraction of the axis + ax.fraction2r = function(v) { + var rl0 = ax.r2l(ax.range[0]), + rl1 = ax.r2l(ax.range[1]); + return ax.l2r(rl0 + v * (rl1 - rl0)); + }; + + // find the fraction of the range at the specified range value + ax.r2fraction = function(v) { + var rl0 = ax.r2l(ax.range[0]), + rl1 = ax.r2l(ax.range[1]); + return (ax.r2l(v) - rl0) / (rl1 - rl0); + }; + + // set scaling to pixels + ax.setScale = function(usePrivateRange) { + var gs = ax._gd._fullLayout._size, + axLetter = ax._id.charAt(0); + + // TODO cleaner way to handle this case + if(!ax._categories) ax._categories = []; + + // make sure we have a domain (pull it in from the axis + // this one is overlaying if necessary) + if(ax.overlaying) { + var ax2 = axisIds.getFromId(ax._gd, ax.overlaying); + ax.domain = ax2.domain; + } + + // While transitions are occuring, occurring, we get a double-transform + // issue if we transform the drawn layer *and* use the new axis range to + // draw the data. This allows us to construct setConvert using the pre- + // interaction values of the range: + var rangeAttr = (usePrivateRange && ax._r) ? '_r' : 'range'; + ax.cleanRange(rangeAttr); + + var rl0 = ax.r2l(ax[rangeAttr][0]), + rl1 = ax.r2l(ax[rangeAttr][1]); if(axLetter === 'y') { ax._offset = gs.t + (1 - ax.domain[1]) * gs.h; ax._length = gs.h * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (rangeLinear[0] - rangeLinear[1]); - ax._b = -ax._m * rangeLinear[1]; + ax._m = ax._length / (rl0 - rl1); + ax._b = -ax._m * rl1; } else { ax._offset = gs.l + ax.domain[0] * gs.w; ax._length = gs.w * (ax.domain[1] - ax.domain[0]); - ax._m = ax._length / (rangeLinear[1] - rangeLinear[0]); - ax._b = -ax._m * rangeLinear[0]; + ax._m = ax._length / (rl1 - rl0); + ax._b = -ax._m * rl0; } if(!isFinite(ax._m) || !isFinite(ax._b)) { @@ -212,7 +233,7 @@ module.exports = function setConvert(ax) { if(['linear', 'log', '-'].indexOf(ax.type) !== -1) { ax.c2d = num; - ax.d2c = cleanNum; + ax.d2c = Lib.cleanNumber; if(ax.type === 'log') { ax.d2l = function(v, clip) { return ax.c2l(ax.d2c(v), clip); @@ -221,45 +242,43 @@ module.exports = function setConvert(ax) { ax.r2d = ax.l2d; } else { - ax.d2l = cleanNum; - ax.d2r = cleanNum; + ax.d2l = Lib.cleanNumber; + ax.d2r = Lib.cleanNumber; ax.r2d = num; } ax.r2l = num; ax.l2r = num; } else if(ax.type === 'date') { - ax.c2d = function(v) { - return isNumeric(v) ? Lib.ms2DateTime(v) : BADNUM; - }; + ax.c2d = Lib.ms2DateTime; ax.d2c = function(v) { // NOTE: Changed this behavior: previously we took any numeric value // to be a ms, even if it was a string that could be a bare year. // Now we convert it as a date if at all possible, and only try // as ms if that fails. - var ms_from_str = Lib.dateTime2ms(v); - if(ms_from_str === false) { - if(isNumeric(v)) return Number(v); - return BADNUM; + var ms = Lib.dateTime2ms(v); + if(ms === BADNUM) { + if(isNumeric(v)) ms = Number(v); + else return BADNUM; } - return ms_from_str; + return Lib.constrain(ms, Lib.MIN_MS, Lib.MAX_MS); }; ax.d2l = ax.d2c; ax.r2l = ax.d2c; ax.l2r = ax.c2d; - ax.d2r = Lib.identity; // TODO: do we want this to validate? + ax.d2r = Lib.identity; ax.r2d = Lib.identity; - - // check if milliseconds or js date objects are provided for range - // or tick0 and convert to date strings - if(ax.range && ax.range.length > 1) { - ax.range = ax.range.map(cleanDate); - } - if(ax.tick0 !== undefined) { - ax.tick0 = cleanDate(ax.tick0); - } + ax.cleanr = function(v) { + /* + * If v is already a date string this is a noop, but running it + * through d2c and back validates the value: + * normalizes Date objects, milliseconds, and out-of-bounds dates + * so we always end up with either a clean date string or BADNUM + */ + return ax.c2d(ax.d2c(v)); + }; } else if(ax.type === 'category') { @@ -295,29 +314,29 @@ module.exports = function setConvert(ax) { // makeCalcdata: takes an x or y array and converts it // to a position on the axis object "ax" // inputs: - // tdc - a data object from td.data - // axletter - a string, either 'x' or 'y', for which item + // trace - a data object from td.data + // axLetter - a string, either 'x' or 'y', for which item // to convert (TODO: is this now always the same as // the first letter of ax._id?) // in case the expected data isn't there, make a list of // integers based on the opposite data - ax.makeCalcdata = function(tdc, axletter) { + ax.makeCalcdata = function(trace, axLetter) { var arrayIn, arrayOut, i; - if(axletter in tdc) { - arrayIn = tdc[axletter]; + if(axLetter in trace) { + arrayIn = trace[axLetter]; arrayOut = new Array(arrayIn.length); for(i = 0; i < arrayIn.length; i++) arrayOut[i] = ax.d2c(arrayIn[i]); } else { - var v0 = ((axletter + '0') in tdc) ? - ax.d2c(tdc[axletter + '0']) : 0, - dv = (tdc['d' + axletter]) ? - Number(tdc['d' + axletter]) : 1; + var v0 = ((axLetter + '0') in trace) ? + ax.d2c(trace[axLetter + '0']) : 0, + dv = (trace['d' + axLetter]) ? + Number(trace['d' + axLetter]) : 1; // the opposing data, for size if we have x and dx etc - arrayIn = tdc[{x: 'y', y: 'x'}[axletter]]; + arrayIn = trace[{x: 'y', y: 'x'}[axLetter]]; arrayOut = new Array(arrayIn.length); for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0 + i * dv; @@ -332,6 +351,6 @@ module.exports = function setConvert(ax) { ax._max = []; // and for bar charts and box plots: reset forced minimum tick spacing - ax._minDtick = null; - ax._forceTick0 = null; + delete ax._minDtick; + delete ax._forceTick0; }; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index b51994b5ce7..504667a6efd 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -10,6 +10,7 @@ 'use strict'; var isNumeric = require('fast-isnumeric'); +var Lib = require('../../lib'); module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { @@ -28,8 +29,17 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe if(tickmode === 'auto') coerce('nticks'); else if(tickmode === 'linear') { - coerce('tick0'); + var tick0 = coerce('tick0'); coerce('dtick'); + + // tick0 can have different valType for different axis types, so + // validate that now. Also for dates, change milliseconds to date strings + if(axType === 'date') { + containerOut.tick0 = Lib.cleanDate(tick0, '2000-01-01'); + } + else if(!isNumeric(tick0)) { + containerOut.tick0 = 0; + } } else { var tickvals = coerce('tickvals'); diff --git a/src/plots/plots.js b/src/plots/plots.js index c504faa0c37..7718f739c83 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1211,9 +1211,7 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { // convert native dates to date strings... // mostly for external users exporting to plotly - if(d && d.getTime) { - return Lib.ms2DateTime(d); - } + if(Lib.isJSDate(d)) return Lib.ms2DateTime(+d); return d; } diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js index f7e0b959818..2c580e5a7a3 100644 --- a/src/traces/ohlc/transform.js +++ b/src/traces/ohlc/transform.js @@ -133,18 +133,28 @@ exports.calcTransform = function calcTransform(gd, trace, opts) { y = [], textOut = []; - var getXItem = trace._fullInput.x ? - function(i) { return xa.d2c(trace.x[i]); } : - function(i) { return i; }; - - var getTextItem = Array.isArray(textIn) ? - function(i) { return textIn[i] || ''; } : - function() { return textIn; }; - - var appendX = function(i) { - var v = getXItem(i); - x.push(v - tickWidth, v, v, v, v, v + tickWidth, null); - }; + var appendX; + if(trace._fullInput.x) { + appendX = function(i) { + var xi = trace.x[i], + xcalc = xa.d2c(xi); + + x.push( + xa.c2d(xcalc - tickWidth), + xi, xi, xi, xi, + xa.c2d(xcalc + tickWidth), + null); + }; + } + else { + appendX = function(i) { + x.push( + i - tickWidth, + i, i, i, i, + i + tickWidth, + null); + }; + } var appendY = function(o, h, l, c) { y.push(o, o, h, l, c, c, null); @@ -160,6 +170,10 @@ exports.calcTransform = function calcTransform(gd, trace, opts) { hasY = hasAll || hoverParts.indexOf('y') !== -1, hasText = hasAll || hoverParts.indexOf('text') !== -1; + var getTextItem = Array.isArray(textIn) ? + function(i) { return textIn[i] || ''; } : + function() { return textIn; }; + var appendText = function(i, o, h, l, c) { var t = []; diff --git a/test/image/baselines/shapes.png b/test/image/baselines/shapes.png index e92907c9e06a1fbee73f549c2bc47f95b8a958cb..51456a8d92d3ad646bacf9b9cd6ef3643af0d012 100644 GIT binary patch literal 27190 zcmeFZRa9JU)-4Q#yg&%SAq4jXO>n0mxD(ui2M_KR91 z(Or0^eTlkvVH9#t&;VM>eWv+Tor^#vF_y~vFFf%8(0#>GyvPXy9o^-Y$lmhhs;d$2 zx{zh-Xw&Mo(6*p8Au%y=6bdpr2nhuz011@}3HaEFhEj(^C8AP*`JZ1?2Ot^K{`()1 zP}*@p0au}^(SKj}uUnv^d;jb6-?upahJ@11JLQ7<9}l7e%|7|pLmw{)a6rYOYR@j9 z{g3A&A^)QI=g|IfTOZm0q!TP|aTI_SUT)~yBF~Bzf1SurTeed{l_8s?}7cVulsM9`2Qa!!np#+b44p$Zo##xUEG`# z@RE~16^-K7EA73*V<2l>zN`96^-AADPRPEul6+bJJ5qEI6+i0Xy!3o~eqNbs+V{z= z2VJoMIzq;8KrHpcf5~fqp=@Bk!d9lQgYXsHgqg7(d&{iuVk6^~pQHDGlDe}g@AZxQ zhJ?}Y_4YY>@b}IZ{~s1YAxIdTYZ9MTbaZs)YhjG^HY?j(TZfJeBqSueb2XM$U?NuC zAJ-yZUcPiR?XJF{xAsph)WsR|=q`pI^O zv5zrO+qrthxjkso%v`l^i<3!v#f}L+ZO~s>PKH5f*ix~`Zd3dlf8 z0V>7^1xR>=D1Q1%A*{RJ`;D11s`q?vP$H!5!g^-NlQ+=#$dCY^p&X=>mb8sd=uMa` z?598?;?pPDcbv56mkcTDe*5$AmdTHQ&Pc1~`5NO}hQGepX1JbTI?Skr{ zbS|s!p)bgHZ$E#8mLoJWMMh+$lcT_2TREHy3fNvp$Z$EXNnnw1&<(w}uvMiCKoSd( za9rWolEHf$_|6EPP-Z(k+1j}M@k@7i_}yNCJ^J@X3@5Q4z^2eNdVvMq-EM`--#tW@ z8_;q;5ybNJ86fC;ZqxsrKJJxZ&^IJxRCE+Nl!YJEQc0lfXXNAW3?}831(?!?qsawp zeQ(^Ri#6o2UU8{_1za!qP7ax_q3?xrFvH{CXQcuYm_{Wmuy%uI#K-6Mjfje#F;Z?e z&B^|DF!E6aE-0SR5m6W-k@vvZ*s#5~`b_k4b(mMa!y$`Chb`8H=_(*^(wbAN13mdT zIh!QNVrr32jfM4cYZKh{+N3}yi0d~$q2qA< zZ7Sm(sFNK?{5oV9t1?w{rT9m-x70;Wzj6*p)XJIg$~p{m_uWzY{NXD~<7^%dLJ3U8 zqr&&n`GOvsdic6L45Wj?AXM;4j$~C;T|LirZ^r5laX#PZn7e-?5efcUYdiO$Os_#R zjmIv160ac{hhLVFzmSFiJ?|AVstl^u)UQL`PkMvr#;Y#F1bdd$F)ZgAZuO>K=BwVU z(%~VP49YU7yhi9@Ux5t^{;bE5M?m#)WFwxt=_@4R?xDq`=UEvruLcsH#TgYWOj0J= zhVG_TD?hbq*5W-MgW1Y4Pr_-|4}OzA+ZyVHrJo=ME%o#Db|o*!1%B|8yhG;D4&uj> z4HWJn@S_PpirBHQ6#YUpGjr;%UA3cSd3kuy8V1oOD229P_eO-8_nZc5&wlKdPS$)g zZz>i*1VYpl2viqUPG`>2il8Ec|w3s-9*x) zJ7b*kjPwWh9*5fAZ0xy3q&ZFSWUL3e8t)Kb1;&P8>#xK1TdZV91&V3n^b!M@*frjD z$1M!0uW@7;fYl=8>JQEsI*`({0(B}0D+Dx}AI#Sw4qJ}Y_4V};HEtyfZ#V@x?Z;I~ zMDS_nQs#7_UItc#aD4th0Z#^mDg|{IODYfrKbhY4s51s2$p#YEPvW#RH6$@hu&^9i zg^_Os!@doB9d5b|w+{T;Zx{BiA6n^(#K6XvxmcU(l1IQ6cu-K0PrI;1Kdt@>8NuJp zmrt_pIVCGItdrP-I(#@R5Zc_fo0$=hlBrc|iFAj!mCYCHHPpZ2F5XUVuKhHwU#3?# zwrqDfE^_0h+80f3KK@sW3oPewsYQ#V?X|C{FNE9KnT@jI#9W~qaJ+fugNdFDLIe1< z__+8qO{`Ax{&f%cH#Qx27tD9Z~;MNQP}ntk&nPrz#V7p(XkAb@>J@ zc{$*od0p+&Th^ZW9}V(*&hFO32(WIIS7Bm#X&L?Co{jN^=CEF)SQte*B%{r%WE8h= z=QnN4Q~>f5afDU4zjldMh1>0miMAialzI*J)3?`f^M?xEcWNbUQO!RP?jFN~!M|G% z;c|*K7t7ub($CW}!wDtAq0FEgL%@%FDDq`puTwbX!%$wq;20(V&hZLkv#vjI$5cQDr1d_Ge~@Pe{-~#i@h>61<5Q{EJ5r-Ycitw z%nH58KNy9%R~(@~uZYA*;D@`v-n+3wcqs`hj?QN{VLF||bNvg9(%)(`f?dAjYtt6@ z^sdy|XfamycLqKI%P7D!2cod?$#rx@Q$D(CFaN})^dolG=EYr0HBXV95kn)->-|Pb zD}HPE)#+}Iq6!4EuUfRdC@z{cbPBN*>S8-WQ@@`#?g~vf!H+CFcsnH-5Nu2+L>6S{ zg->%4c~_~OblCs<=fQ$jigLF?ar;2OV|u@-vOp>M%_vkNtnF>h>F0M=&EhEQy~Fpg z?S%&U#IMKxJx&4XrPVb22XCdu(7Gl1N-ElmBPt#;{jQ87lt`x38K1wNsk$SzUa6;7 z4f(?2X2N0KtLb|tN#1q|)0{4Ys=psW)Q#|Olbs(f3I5LRS-K3xB0Zb4%!G+PT*qt; zrK~UhNqxz`m28@RTnSS#oEgkd8Dectxc1hDwA7S0kM14vC8OyK$@-2XN2OEGVuasi z+H4JL@?ppx4kU-a#l+K6>5d#1j`+F}o#Ry|DGWwN$@AJv$E(}#TF?0UgP1l^t()c( ze6&d}2`?P9(q$dB{(m4O#5zu2h5Z>Nt!5G0>**+Xo! z10NN2r_fI6k71VDhj}w4UY=#de;gYq)+pg}SZ@7rGR|32h%d+c+}t3|HP!id`UhO6 zS>v#TXA*F|(dn z>h*+K%jRyzPS>c?uEFMDdoroQF(-0f*gzc;P7~C9H|W83d>VQy*70z6RHyv=SMHIO;q~=(>O0d=XPVPUVJP8Q@B| zjD9CHe>j*LVRwmx^+|+8SmBenRj4KKtCvV&kx{Qci|jq$Eq)Kji*FE)crF^qvd}8& z?k6(nd1X`BqsodNTU4%(*K0@F4N)r;pd2VxuZ?&Cm1?w9&ZwnB7FF~RLqf)TgM8Zg z1bZj&=4i_CZg0<>cxF0a8RG2R^42?y!n<9?G*hpH)Zr?~zJ0SI`@G^c3(Hik)#O<} zW9BEoP)GXvPWmV-G9PZYN3hDl&xI~#O^dZEGVEIpc-Tdn+8a_z^o8>a(}gGn-S)?{ zjC}JC78=4debnj1Q+XW>txqj=CKZFV+hbkW82I+vdwm;qnK=x@&_SUTN-1|JkCCu}_!M&2vY{E1) zRFN^d=O3?kX_hX}G=*=hGwew{TkM-*1$4Ektqg~)a3yJhD+-3&$n|EcZZ#TfT(%;V_7-qj3kuv}@y z{I;kH+WeUDOOA{GP?tq@=o*c4M&t@05&6b{*k9yY+o`&K>KZ6$I!g7*2gR}X14wb~#jt)t{Y-NWS@j#o94mPX;k4}{;#iou^k3m=TYGyV zMuN95`2HZd?$3^23nb*2%ymtjtoI52qP_T84E~&lc+;05@{-cBVya|?eY_58Sn|%a z_XCL#>C;9byP~=AbsP zo}^`!4!^r%SH)^(9nI?`L$Cf5MT`&V^xlSqb(YcFZV2NZ@fZH^Vcrqw(zd%+i^lj@ zsswOK*}f8eJBu{A>ks_bon#Xg({+AS=cU*!mn6bowX{ICg`eQ&b|I}5CZe|Oz;bGC0d>5Ig}r%nG6?5WZ9j)x;V$snu}9S0Me|LQBg%gl)$ z*Df}_G0U44{Fe_>8GeFJH4I8#YK-ZhzZ`{2eS4Kp>31dem~rK}Bw*Lv?AMGUL>MXr z((`0f^S|#;C%QN;HM?3jmSrOpv88V5O*1Sax!YD;2&dmTDS`Isy5-cB#hCCY?k<+} z?gaNYuUu5*01|!{0|WoFFV7+!U&5Q&*OA|L#UbUdt1cGR@bKv+mW$a|&x*p3Ffx&@ z0mtilD%-camaXTyocrj(z0YdpP^3X3F$M@hN8S!_V(3 z$MWqtmS1Q3HGVZw7PDWrVM>Da2fH?8B+m%nrzZe$^&X3(Fe2Me6zP_5!oJ!YI)fxg^kV*+=u z;th$W=ABS(#HiLnzV3Ogg+ca({iF&L+eBiSWGY8?+YA1&7W_Y~PkUbZY%;M9%)!n& ze9Qwc>{e!B9fnH=h)eiE-HHe|FPi6F8*B6ajE`apqRIJGUY0Fz_P18KtJsvXsJ?hr zAr-lRi%JrkLAnvR#Ch6aDIRkoXlAI-v^yME$r>rj#5Fxd$90x;wF+$p9S57oL_Mt* zJ9_V>;WQ+4_}z?UmjWc`qZHxPd}B`cIU{7Ku$$cIdJ^wuG`+G@tSsa5jt2CL9yG## zAm50w*qH}&3UKp{Rrgd<@Da|8TN7!S5r)T zr>B^PNWyB*$^spAJha0rOB)-tZw?wVl|-&LCRi3^(MUl-TQ|lcQ~(a1Xjt*v`$|Qq zB&z=-s+nb?+Bb=R<;Balpz}Sb7O(51rvN+B{Pf;La2D9Bk%($b-ETw;j7u@y8HNP; zD^I=DWJ`^KW8a*T>~b%1YrfI%Zepy#8*o=XV1tw7mR-9c0hp0-_jRKFK&ymy|oaOg(UrmiV+e`N~3 z8~oXP(;M1+db=gNFr{W;I~~!bxd$W=S z8+ZEnhCbX@&U0$-w;mh^SsVE7pBS{bxfZi&S9apkUM@N-CtQp9keFc!>f~!E%{Vi| zw}zda{I&TJ@LY|CqOC$LZZg9UY%$-$ACaCQ&BVZPGyjjd@!_Uh0m1XxTG=gqk^N(~ zE069fd>ksr&PWptwe1UOKAr|C$!qS#rj+*{rN95EcRgsZ`|Fylb?15(FL2aaoJqA1RFwT*X$G6JL968Jyhe0G@w2}4%tFl2n3-uU zBLj`;R}ae>AWGk}al|IOzfM6J)X3i;)BnDS!fPk{)g`XWld5cYJ0&2yQfZk!nDRBe z8jfExw)7M`P1`7uj3U~dE=h8>noS=cyzwGSnlmhPhb3RAmaXYnZc`(xR<21BjUZ0a zKg~WL^07Wjo_sDi{&rOigB=;4riww|=Jb?FA!}zO06+e?-26I@`f<95`+c0di`F{C z@L1{ttIyFS$~T|V5DOpfih0r#_0pM z6s`jG5e_By%ZeWi!V?8JkJ}-XmS5-NMddJcN%1b>C3IUnmvk2t204=+S(;y{l<|4& z^7>dbpfAK!{y@rczM6KqYoyE*Zgap?;sb9Zohn_&LJS(+e>k{kN9-~ic=pR8ESv_d zjns5{2JKnLaD${Y_s)9k)|cE@rn}r9q$t12N0ZE!VcU;4A29C0Ed2(8RMsOj#sbX6 z{jXNT{ zM%L4Sxt-%K(xT=j^Gj5K5PC#ot^&J+Pib6+zp zB4oT)B-pGmdz(OgAmUsu8VSbf(bi1qm->Q#Bi8yzp0lLsrd&|{$~WS`TXEn%NOSKh zo;`ez(~ejB;V4DA*4VfQ|)moJB-a<@h&5!*4)O{Cj_e1?MgOJ4x)?kj@>q@2-@4TOUp;wh>vDIp$l;lR>5T}Z`IM{nW$uY#HSFb=+ z?Rc?2*SsDrIMwFOb&!oq_48S5VWyw%ks=v{m!iaz;I%u0a!0KW|6mjzLT1VH{jj3I z{q3L7U_^b?_wkN}>muoAwQe^sul%vz>kx%@}&oD6)QsN;)`$M}txDYSr7vNmiGAKl&L{s4QxC>mBnHN8$ z{F6?PYuxz$HNzM#W&eWQ`a_Eh0C~r|Bk;M+e#dTb@0-e7(Sm%;Q=je@dkd{k1_EHJy-mC&p%joTI|yN<%uk1u$-Re3%LXb)zRcr>1B&G7wl88b44ccU4L@I6e(m+*@=W{Z?A@28E6f+3Df#lG$>^ z*UQDsiL8S50vB%H_~1dW{E)#7!Z%FckoAYy7L)7+*kjNUv*{P9_xl;nSyYzpWRjn! zz-=&NC#ub5RYK!l!TRqZrQ<*f&nNpA>-_Y&FLuwWP{f%UHA>r@9*6Xdb5&zCD7KiNDl>WDlI=`6Y2h161FY=fopCaR>Bk`YaWbQ>~V{&bv z=FEAV&%HBBZnt!pDV`=3hS4M_a@Qo3oxD2w%-QD!_m($Db8!z+AmQt|p1_kotQi$) z$Bdor6osA(2crhb-fy%c?(lrm+c+j(?4ZZeIXp`Z0wtpp5D@Hy5F38(4xJNCqs;;b zB-0jL`w0O!Q{d`P5Vz$B*|r2-akMRZ>>pH_l0-%BqadF77FY0FySpExgZF)QQ{I#c z?8E9ALl_0skS(F%$w4LzT3i2Z!1pB)p={LTimIuCX&6bHh0Urt6uN%A@h3ZCg@(~@ z*U|-TlS!aXnE4r&m31hjK+g3VB9@g7 zk2b;#VnIm+qZT@$8@f|AqtMaP=5mRDH)5nB@*KDS>hJ79e7@rDP$Bxt3ffNU2NmB> z%T)XU@LhjDw-OD7=zM_2Fa`Fr{U(u4np0X~@bY9^^g)MJw^jzj4?XrHYPc{g{dDBu zl{+?nV(UGXFo$0}kzFe&M(9*#qD9VJZCD%m6gZ03&~gJSdQd8l-7Z~x?OGU;qp~Q= z?sR6%Vcc0;>LFI@TtbSG4u?xUNsM54_W~QQp+_n z5v35I253U~cR5%DCT|zP>|W}r-C(4Jx5b&#!;xKK*ua^hh?to%@=x-ih!>sD*BUP zPc^M1`t=10U6VIm`3#Uwt}>Njb3mIclkf7B+lk9b|8Ol zR=)$qZTFb&=~QOWz9^gAG^)OyCaTPIX=!hfYA)G{<1jb0$nOp@BDqj+=ZuFtFUX+% z_XsI4aM%?qPwV$5ko(HV^Zb2_ke~b&?1#Z)H=k#Z<~Btd!#f$Uj1@0p$obQ2Z_wz| zuV4&Whnv$ucK3TJ?c_r>Wnqn@R(SMe82E>w+njkSpQFjd%~{FMTC6VbDa-2MV877S zy5O*;rlw|76-t1|?W~|zit;O9HOlqBcaK|duqsU0q-2sJ1h7VD!ux=3G)iPV%8EK0 zsP);-xNen62%ItwgO{@e2>L;Xjq5Mc$JV<@mga$hSAfS@pImV|?3cleMlghL(<2mL*1lY~nAv54l)y=m77t`P#%t1mDxq z)TUD=-aOfcZ#dX-wl%}yI`hrBGLIn~o}!Qd#GTLdxKt-Ur31SA2CdU}Dqm|e2tD#n zdFo*lUPbHa8BFGjjdP-k<~MFun*$R^3vZMeZN!QdxT`!p=s+ybkJ=iH@?v_ui9;{f z(wsF!*9nBrhu{&zOJOAxUdL>00;|Z1-rU#il!wO-?e0OkZtXe^i%#+rmM-VhT6?@5 z*WVDAM@CWsx9gM-W9}=yjDoO8>wb;$Uh7g$pD3ip&#U6xEVe~{oCrx*IgRKAB@q$9 zUCu(o7z$xCMtn^vYqJC>gYHkIg#%-#m4U$S*%LvA`RC9C+K)Nw+3efab6cz&nM&{) z+5E@30_X7`b7f|=NxSkuz-rUI^5ZXL0<`^7OwlehN7Nngaqri7)6Q#h*`|aqMaY_6 z@BAN*9$*)7P}usmUa@?kSH?Go9ft-$V<1ur1QbD-?@)&DLX#(#S_8JoRi~`mC()WP zqAk)1pwydp39}5!<(`Mx1Hyt-K=%MYAJ3#LYnF7qBMWu#-L%+XDv?f{%#4r|oZHP) zdc}5>k&gX}OGd3wXYVV_i-7+?xeeSZSf2yTPr;ql~)Bs+hZu#@UYFjau57b&? zeBacfozm%ox3nGdw@WhyezTIM(=MN5+uZFB*_yjE^~c)%>XgGiAH(q^9{YLz;IVT> z+wYT+_iZ&2`{sG-)eayb`L6pynG-!035o)o0w3T6w`O?lLW-I1DtZ&N40R5g;~?Qk zCmI3iK5OI6h<(XdKCSK++K0hx&BnQj<%3hSF(TVSxX+7|oiD;g^-I$wGi?@q6>I&a zkCa4gqeaVECUD>LTAiorD;L;CcM%HfEHyb7;wLr$+;GO(o>p!=mu!u?)9E{3gbN`N zQGpA`FaEHR@#dMm$84J1?`n0VRe3^om(gKp#P%T>iTc+5)UB3Tq_js_QQr5U@IDj!M#ldkDPvON^1cef?(BD1C)6fdlWFL6ZIp3l$)HuyagVUK zOBA`}{rC-2s~A zQwi3EZW+&qT|uIi8qNkQyxb{XDCOU!3{J*dJ)QeXSqYUz79q5t+eMvCg!n(5Qf(<9 zxX}nmuINlDe9)H{IT=0lhDFjWoy-!oohk+T-7MI+z|5h1KM7lzEHcb6gfl(%{alY) z4H^umRqnhzZJY=NiQx}?dtX{y#wCe8IeG?A!^_Gd%6OMOy5jxrthr`zm#Ui|j=Qj< zAHuE!^K{>>8l!X%0HAP-F552auV%@9Y{Ng*HJ>~ZwTf5Ty=`V!4L)E&9N_Q5;3qZ2 zRx`U;*5j>B1$m8EZvm2Xr5<`nP7PWMib0Bq(bAof3bj^6vAWsTVuVqfAgkg-xgDiN@lD5Cux2>V-<=u4=sC zLbwnqK5nIT^h}Pv@b$2e&hI7li|g0Q&yYXB+u}cCqQ_p10rOi=NUX!90`gYI3=i=K zzdg6Mi_48=DlPX1B4T;=4sb>P1KW(Z8gGfSh4eY)TbVYm%W0sjypbNgFZ&}oGl>2& zRn)(|y1JTcJ?irO`ix(|>W`to`m6zB+DdUspi7YPMtB$w5a6FY@85WxLM%(7Xv@9a zXg*IW^71p1SLM4M%ts$d7fUR8Dj!*eTJWje-oi)gO#5Ec=`ATOZM()qY0WbGFKydw z@(#P(o3K{pCH#CDn$Sw&QEQFJnIV8#W}u)t-inQbx5 zJaZXk!+g_MrrTBzD%0iBjPE_yXs|Z`v><9v_1s^(s|BX=Kq{1Oc+?w4CRUfM`JMT; zEU~7u{3@HAo;47btiQ84JBsOe?aFXrLaE1R&aODy`qH`8u6%+?IR85mIY7xx0!7~R z3bi+66@dDKO4C-D^|~}6&9&y$Kuf$Js!VH3RbD&Ts}m0;p3qCrtd3BytXNB z+cRXmKLFqKKNi|NMInrU%N5Scl5*?QGZ4c*e8Hi*pxmCiq@3#R+YQ5mkANq7hXw2M zv$Q1zqcjTLSh=mW67^{PP&gHf-V6k3Uhi34V)DC$a-z-s?4# zYgw$xLVIfxe$cQYkmR1ZVsN18xt>mbbY-pUubL#OTYtn+NHPff(BbDhI$Tl8jw<8B zY}roXz=}|HJ2me4@-&(98z~2W`%I1jDKKutzl~ce;D-T>3N`%OpYUZgSaTH54m9X4 z;zh#1jo@C(!LI*^^>AiK(V1+$7Tk9=g!pWLxuVD$z{W5Tka2@cSGUldw^A&3C*;}B zi*1Y+z~wM6qa#7zR0udEMlqpuC-H7Eee|I1eb|fLiTrMoTYYfJ;pxoKbL0!Mtrw z>frK4uO*Gf#cdVw)?O0vt;W{tL)UkV@RpMUmMGBT;VKSryhPaayil&h_ZA!nEZw9?2D9U@tQZ3$Jz& z<3pfO_Gg#se>YkxK)q^$!a|&VrABysF!kaGgDA|wFIs(mk9Q@hf_@KeCqau5!NJN| z#nGn){{}#B*YwQHBZ;5YlX2>As$?;?0R;t}?pMScnSGcvAP#cvL`LHq+^mB)wJ_pP z5&8?T~2n&rje3T9#gB^5HV393VCwvoix{>gpe7$_`b=1K2jRoj>cYA6w>{ zk1Q0b*6yC)uO|ozAM3wYOgk}J8R95!-ep?)+=V3o09;p~vb9$|$mZO>TxlE>zhZp4 z@tw{OqGICw#df(>XT=BpjkD}^=9g}SADQcv>SLwcp^&oo1@ecB_;!+F) z{i2SGt}4)gG^L{$7|I{of9((+)y0O-pV6D$TQw)3=(8-jB3pMSe4WeMe2h>`yK?217pAl(I!ZWE z_vZARjA|)!zMm3O_XPsHe9>76jDwdUJxmk0(vxS^@?%|f&1TYSsW(% z{ycjjxLMgse&Opm{?6<-&K&a+`QUmW5S+s_zwZG@^hxAD;xq#w$r+@GMXCxc)vglX z274F0III;{US599+OUzq;5(|OcRq_Lc(x{w?|L>ajrMChlDT}08>(NEmJ$yBB6N2) z>2-V3_k<(~3XY!S+kD*z+X7DXB#>UGR}2%BSot;HB`J6$BUPDr(h0a9?)xlh`MOps zUA$VdKWMq+U;rtEOEe|RPFJ_0NPoI*^o)vsTiN>OEu6`j&RtTv*^Ev3qL~KtU6CvL z;<2n!{qQoH?^jc&xWor46>5NnFG39$OpXdODhgTg&0>_|9W!3~Fqp>}r)+Y#WzcLf zMN~F$P-@xNMz*6i`G_F#85CxRVZ!X}%G+S~jAg%@1Brr{2%915J6T~^@a9sLY1ktj z&xo6Q9hCe=Q(ID|^eUXYt-7+s!fVrf20lPEZQncWzth|vR%BE1DOtZ+yi1}NQ5K?G z-vkofvU_vB8V)XD=CjSX_*F>DPw9Xv^$hR>07V7FR?~yUr}Er;J&+8=wvhVb zLO+fZrWMulSJsIaklKe-!_42^Ug@GLBnFrA8xwhk@XL+|1OKZo%Cj+}i*Z+C>A^YP z6e>es%P9NCLX~Z>C=ifE8B!D(r)B`f*TDvI)o-n;o?8zixWM;a1U4N%}r z5vO&2cV5F81|}mUnz(q(A^-(*fXe)7B9Ct4{81Q0prGk4U%>0OuMKuqu_RBL6FJUz z?+L-`M(v%+p&^{@@7+QnB$&HN6pkLh0Zb!*ffA-?h-{t@&^^ep~U}hwvNqc>z zSf6U@hb*Jr)aT*S+7#QmDGv>bpG5^wsLQR}9iYpm&rJ!QuUy3RZ-rqGtnf)U80l#R zAqrx2W^Q`~xlZH6eH$8^4!!@Vf_ZRKLv6T_Ar)ym~i77`Rgrd2I8nwi|UrBQJ%3DVG?{?;!Jwq1g$K!FeKtCK@ ze1Fz+6sWpIsxGTrE8om0aVvaG9t~~VH|x3+NVxP)-U@xhr~d*L~hZlLab?5^0X>6g?!FSyB#~~ znR&hvSb$@a1&(bsmK7M2i&0RLboPDArhT&M`b)BMI3Yf@f!|J!TgexGozGTg?gNSD zf(<5&l)_$TpR1?-rn+;ee+JwbRoK&&g3xd$o0!@5rOSibudN!5OvMSK6T|~h8(;j( zyGH}@SEIt~B@k~We3}lMwkekUSHJ@Bs{+JQh^uS2F~qqht)^8bM%bw6-DnwfTFanZ z@7^VCz5#n#D8HKAWUWgH$<_d%CDWW%-7nAjE5};5ezfbBfUSw<`qPh>CX98^q{FWa zSf3@)8H$^``Ry)Q4%|Ud1Il_ zf$slpoWjIZtZr5wqu_*`;gPcZlNzQNlu1Y^l+jeNiw3mVWV_U?0dR-m6#i$(>jJ33~Iw+&Yyyvw80zznii9#%s^;Gx{#Z^Pdu5mVQ*Z;xggd8yec z!5G9@jKV4Dd1;}zl!@D21Iyk=Wq`O>5Qt5`=tOS+h*3hMAMH5x-|cv0IqzhB+k@v^ z37l$kb=<{7Or>#VG`Sc5uBs9vc9viG;THbtVR1cL`BzgRFO*&Rho7I>75U7AphL^@ z)yFVH&juMR(z>b2DE~rv_O{wzx3BuO-_IY121MX+_BnOIb=kMDYi#GLCteJb4W5R0 zBycYUho$Jxwdh@}`1=F7qjDe}4&`#GXc|9mZL~k-ok?V~S1<6&X{N#) zHyukt7VrOxV`-y0-72$P2J=FYhSmd8OkF{w@{W zwMv0*aE{Y~7F6f%1b0B74zM3|B8D(+QsfWk#nNH{-6Oxce|sOb75e+1)fXsS$6Opu zN!)qOKykweAW-UaSC30Xu~viTG=7EHw{kwQTpcs zAhJu4dzx8W1e7CJ|2DFw_F@%J+{CCd5#azRaJTO^y+oYD9PhqYy`eo^q-GFs(%Z1s z2{X?x(6nFJ^nLZJC~)0&*bZxK-qw{3}sAWHkS+)c7tYh=ER4r>5@dC zU1E4(cq!|aHDq45@%=)>0|6+@-TbC}y+>&Z?%I`~4!QrKP4I{E2f!}&3aC%R~Yv1-Ij^+Yw8P<~p z-{Ql1SQD3S&A>wt=~fj8CC1}GlQ8~wpvdU^N}c;xrVghcQ3H2z-lDkzg6mQ4!N@jy z02K}l4muk?efqTeh?yxd_g8gNn!hyg^&T^A0~4FzG9s4?D57S`q>6zDM<-UeX$N}!CP6>PAakl1379U~?d7W8C)eC3^_J!ZyZ@i4K{5lm9patj; z4dcqBTF_&yL6MLzK=MUg_jg-Trzpx+t$j}Sa@j2OVpLABj@gyQz2~nlT^)7Vg~vcE zIBFxqKMhvW?q9vrI8u)*qy^OA{<)R^>X`l|^XSQn|A~P9=Q)o91-%4RohbT1smSLT z;9gIjIUg7{_!s>Yz8o*&#KwTXB_Tzn!l4Vi5s(i9(}JChVso<`fZp{dKobsjmS%%2 zs*I~_!W6)q_c0IEaXNS|kSre~r-aA8WhXEV0>z?A)xCzb9VLXvQb7aTssA7Yz2ayp zB@1+b$TkmH^tfmKc3J_ow)#dZ%WmRVXhZ105+ik!XS+Da6Bxq<>i^x=R^fVI!+&Z4{`sJPG@}2vFa7_CmVQtwt_L9VQP=RpxqpvTkSPq2lufHHz!N=nM~VNfJQ776)#>fS)<cCT!jZ~ZRaezF| z1=3i}$wFmlnnODz2wOxOP<>{*?a!{F#{-#axq8;@x9&-0(J}yM%hXv<0}RofnH`ta zL^sRoZA6(y^}Gb_eFpT6*j~< zI#uC+XH$3&i1GC90D{uRhS8F|tA+XASiG z*1Rp(A3vQ8Iz@poNT{Kk_phh7JJh@tru>rzXGkio4T(6-2*lsF9cfLD4NgE@S zBjCEbGo$C=>em4@&278>rVjfAKw$(h;8I|6{C25GBtjmQw4a9v)`8vv7kiW4nX*AS zVAH4>K4Oz)cy5sZ4po1a4C$j6qLIjA zLN?(=if2%CQer6`0-~k=^`KEYW11V{5nZRidLH?a+vV=x8d?VV4a;67;?(Z|EsS($ za&9pOjyIFFvZylu<_~mL(;^%h2NYVVuVX-y)me$rT`@s7Ys2E25U6Mf<8CX>VTRrB z1QYDB7D4z7=VqbOdYWnbC{XYIqM-xmx=@O4BBFN%ocITKyOhUX+`wlT>hz!?y4~yT zO!l@J)X)^|HeijTV_yO-vCgNsdPCj6%HOG00|aFoVz(rkBs9ftV{2;{*j0BWaeZVC zzgrIE^J-9GWbfsJHJB3NG%B{ElvHQZx;OR|v!F;>YtOLx`e*fdfDCxR|qKCZJ+qVr$yK z+pg^rrjFRG25{&$fNSrLTo)bt_<4yK6o%RbH3M32PY11mj?`i*a|#?8PZSGrD~rUf zn^mG4J!1=XHxv%Lc`jj~PW`uls!Wf!F!IjlPiJ7lGaSynMIhHCj}w;#8b)K!bR8@( zfp`c4DvTCi%tgomU8{ErV>a6id;8U|s*`A8v8uxKt*_BY#&9vJX4IkW!R~GyOR$DO z#ABQ3Pez4L0H1_EFgnUEj8zr?+!@r6g@1?<-j+6+OzQIMUdab2mqw4G(YFDiYQm;1 zh4pbOUSZ&LSr*b}`XM0eeAXiciQz*z3&~}dYdulmPrTTox0UdgQPGFR2q$w3Qgj?1 zO4J@zr^lsx_uguT0y8{;O?2Z*k~=sH`;h5$pnO&y$K+LQWm4(i6? z1$Z`rdozpg{I;`ix3hamEYnxhlT$}^0A3PI{D0cU)COwzt6EaD7Tjy#)@1OgL{`1s zym%!i07bSQ|H2#@4wvk#-UxkEJJJGLEn69MiKGDVC+%Yl_)}&U78mi&R0k-~_D7Cv zlIb4haSmww1W>xaXv~fR&`{3{g~NGEVS6?BI%LUX{hN}D5I&j3fcX&-)~J#+i&tU< zncOZO;1>?(IXM57xd5ItI2v%rF7m$u{{5XXvCTk^1Zk-sdj9XPmdOTQOM|iW{VmnM zU!TVY5I_I)uKx_&pdIun?Evji{lQ3^6qfZjfD5nIGS9ah1*)UJU&HrKMQc2!2+)J$ zK$@C~@RUmTo3Gkc?w2+S#bcwZvx4+@>@&r*WxD}nt6TnibZc)E8fEFRqB%D%- zB$FmxdUFrYtUBQN&71cR3(ew8`a1kst=At^{pMB#p`}+aOVW-A?8>FtCS~x!65;Fc z_$$5-H;gFjeKF8H+4yuVTiAGRSPgVh%TvsDWdRnBLCnmcyYKDY6X~%~Q;y>p!hK7v zLXJXgBqT}7YY$w*s97lBYAy3oHM;hVvh{-Q?pS;1H%I0o5*I(CUIw{Q9!r0?4hyLM}QC}{zsgus4xTh zAC{zwM9s20E2NYgxiY=K_^6sgfnbW`gpz@d<38f%Y$4u#j%_ugvFjByRjvG1J@Qm8 zIGkB?U)W!4bFG_@-fi8okd8s+HB7Pdq80-iIyAshHy@E15q&Bm-%n*OLTfQ&ZqhBR z!4+qkV%E09PGVYsg`S5Wh?SN{FPaFhx(;+%F|uBK4VHPU^=N(5jDH|Vj$y{i&WhoU z1@t%R)UNIN*0sv#XuGb8_zV`y=0~I*Y@X~6C|jvr$y8N^OAwDIpOjaY3JY130<$o#05gu5$NI6v)pSqo_H5$Z%c=rh=r?zKS)H0(_wRWp z)v#gIK6{1SV~KhDq669Hl#52Cu|vd^9a9PEn~?A^ET7t>ZM*}q)Jm!^E6kum8B7ry2V zB%C>hxIMAsEiXU*G;28TtrB6RG+EBg8O3S5g_xV|k29UoWSh|Wkt5I(i7hz%PVLuQ z{(!XC9FCUl)Zwv}4?V|zKD1Ik*{ZqGov4G}Q@S;V2uEyHvO(-@c}B?7xL99pl4iiG zz$oiFx@)V*Ax3mix<)@G2fP8fI^-9~Uj1 zHvve!y}=O?b*I3h{1)baHu0ouq@Rj#eO{k~!GMI_oP>Z%^ z`g^ML-5z)HzoBLxQ())QaPJ!8y^l6)UBi@k+{X+Oem*CKzjZHy=V^$Zl8X&0tUPk` ze}p{p%*5HlO}6xySD0{Zs|sl^P*;3Ne$hxgzR4opW42#fyCOq7^{9y$a>R{B7h`8+(@p}WI_>h%@{nI`krva+KT~3;ms!< zy6G7=OhxWoyu)~AE^?Fsb0s&>u(OY-5}(4S9G?^uj5&1Bb{kJ<%(OPmm=uL&9$xz}D^m7lP_w#|1CaD%GYQ#H@{ z4lBfk!x)q%`CQ)1?5hpM9!E+RRVCbj@)mu10bh$q?@(gh`=E!?|D)X3{*}O`bg6@= ziVV~b*VO8bY|>kbSreDgHbwNL=VzyxI6fP29@yZ260gYyxS>H8{z|=z} zTNQPbZjsJjMzWY!09Ux*DP}%dWQ8}HhveCGght}FFmjdrfKZIZpk3KcYiOxj1A1U!?87%canP<0s z>~Pb{RFwI3PQ==!Zr8{yRVq!x#KG3cbF=; ztt6{WBoAG2xVgMIBcRnZ(>Q| z@}iB)hicOsM#CG2wwXanXAhP5WzwVzXxT20Q*VF18H1c~DpZqWLr7MQ8(b86xZEJE zA8LXY8vUqU-sjEx*|z7Dgr3Vge|g^>f6Ix9b8h>yZJeFi&cbgwOdYIFz10c<^h+kf zIe7?E6r$MycS^kgn3`!gk*Ql(YtsjFzvxQW*vDU)!gR)hu(FJgVXfGpx%&J!)!uhp z`=vkbe{!xs*A$)embZ$nDH4mF+v^7<_QoH|rWXAjZEALpOlXOuRzcjivyhToQN#?v z+AXnn8ee9s#{IPG$o-NX6g_Pb_0q(U|CB8AFtRL0GH%bl?+d(mlL5!2ZX@t8M~_rH z_?+2EYsf+SP$F(K*85q`vQI~h7su6Hf({9Z*G$1i#E|A?0P3E>^%a9_*VhVq!=nKo z9U?;Wn6I_O&Ee^G-yv=N&QaO+4|siha5%46sBgNTM+oc9$EZ?MBAclds@-N zohb*CD__f`BDF^3Q04YvVN^Gr{y+^m98~BzWdXd2kn)A~JS?cl5z6-nn$#$BvN47B z%0P3GjoC$7>US{Hfw(tsQqg!%V6cUp=|8;MXWtc`y{c>1*J@~CWhHG?m__EUMB~@N zr|oy~ewSk9l^|nD({4&MtGEUY8rKD+3Sk(5y%t<6>24El{gZ=nn@8KmPeCRx-0*yE zeYyftgUhPe*DTp7k2gNZxRLE$G6%cEaFINgdFw)TKC*6z^sD?`LzQz_p3{NI^W<>M zj9f@H9Y(N6IFl(tBY0e=Hs?a!qBU|-D=J=-wk48m<0marj)3Wd*Dz{sGHNw=Q7CBk zCcAQmpRnV7Y9CHQ;<3#Ad$`-K29)>hRcXJ&+qFHleQE<4JhRiJ*0YbbGh&&gOCS%j z%BNE6Sq@_@-o$D{QzGa+pH{Gg@z1Y_GC(MqxKSojrkx&Sm*?{WBRiN+c9X_&UUCR( z>FBjuyD5^whh|ID?a*v0csaZgy1uCIj+XNMWSHXw!(P+%JUontkEG+6`+CgEG*XDC zoe+*AWn%z!5gMdcyc=3Q!?6Mk5&;ICZkFfLx6C7bTG#O@ye=JD);*~VqOWOzu+tK(*bl@pYdhbi z|A#!|CxK3mvfIz>3XMJt_XLhrz8c={bX(GLUlN{&byw#3;YQV!&(BbWlxk6HXQi%T zFR)zVZrA-q-@fTkTOxy7lynxanYaq1b~y-bz-tw~lw2f7ts*sFsQW0-wqNuzwbb1^s(C9D*driRF76m+AuA7 zg5&oEXRZlM2RlrG|GtOh!8>D7Uk(i$QSKpN`hzHI zF*~aoTUlh}hopI(q^6Q0A(1BVp*_l&Y0&P#v%kFYyO@TL&3xlJWMzdHriTg)aOefl z!qp{~zwMY*n3>g$ew0snYrb?w|9QN9wem?kBoqwkp$(+BHPe*Dt_K6ggP;L0j{x4rHBQI3@SUuvo zW}qDx%Ws=neJ`~A$9IjgUOAMms#+8RkN-!FCS!v0_l53UmC5hEoFG2t5xxv8-+)hx z+l~5l0df8j@9K8>SPFn17)bj^mN^55mBZ{_hprYQQ!fc^^lDrqD#s&ZM%aK)AvPvz z>NeI-68Ju7RH;u3V-rop9ZsHXhW==2z?S9J5@SYF6tw z+Y5>sKs4V1FFtK&^Lt=l#>7Ce8ZhD7F88G}nGeu7PDiF@CIUSxbE{h$lUxdbU8(=> z-Zx3S5$Io$=C!~eYn&Mrk{MGM+7dA2`mwObzMTE@smw zrT80?kM}qB-x)12O&p|ojsOjcfyy*-pM4`8r!q6qmt3bXhw}a=sg3@QS88ZkDF#)V zd~w8;w36f#q38tOSI=C8z3X6lnWS#$HD{w&G<3zMeNI3~v5yPuuvtF7!6EN|D2%Om zJ~g#Qthp2W3LYDa-8_g^tZY%%sCK}6uB0xHKx%}5kpk>CGaglc69mZ!!0`#XqVqi^ z+&AyqX+fj#45qYCqr#gdk9tA$kd-Td?}0zO>B1JfDojrcNRl=$m;8W9oUb@!7@-i* zJec!eAAj546w`Xp-@nB@O_c8uN(3okxvKR>qc5Ew{RKkTPgQVRp1bbv?ilIkFRys4 z=RxVvO)0R{Riqb?Co&VAP$wz-g)qVt?mcIf&i>tFo8;W)! z{c+vfiV`t2dnsz;$sP0+ug|lK${NO1QVJ$q4M`;>+ZRL^0>CL_cE+u_jAI3Uye%A1 z971Z)C{?0P{{ltEfg*UR_f)rof;KIDDgf#=?e`zLx#O2xOf8CL14_k{H%m73$D*GVJ%)* zZ~n3~A^+=v7G9MbGS^#)AbqvNKx6GMR>s|}5c=cVr}ciJ&hTiJZjZPhk>kB^MV(k` zq}Zlx6OcAA>L47Uk@)L$On zvXxzJQAs}`gnsSWg=CTm?q*%Js~tUayn_Mjdo7pl+e~jq?8Z^*>>x0ayw#HaUQ9qx zP~)lm_?u_@&th-Y#;{q=-gA((ILAURIbrSkb$y2No~m4#snAiaXae4% z!?59oW`9;Iz<4mh2k5WX28ocQ)8wQ*`r1M!ugck6hjBk5k?8njae+NstG>6fl`nQltBv7uZA}gwirnKarZMFbxKb%>#@b zG;eu^3-oZ}xG&(XEDI8I-^PbT!2(q{UZ=-PyHr&4bK%>PuxzJ#pq5kbE+y(eXZ+h6YvcfP zrX%@YfJ}^|aDk9{9kF_ z^?S{EERF`bNvs7L#=x^miMWx4LhyjYsD3We<|M}M4dXM?;u|ZFrTDg}OH%xk4COZg zMZ6HuFFT>iuY$TY$dhpzF3{AWcAmlt{H!Q=;dYdZ7%s(m$`YKWP(+BlhhRA7M!IAZ z*q07#O^+fi=)L>s{$^q7f9$Gx#+ z8xN)B6fx7_yP*Io;UEOzkv?o<;$q{=xm||GI(F?P{Bn8|)pP(Qy<~QM(8R~_lrPZU|k7J+&52`C#1-T~Lz^n0O zFh2~TvSsEKD8Ahjw}wL;g-Aw~NWrl1IMgghU#3_W9V=+bn0KubCbv}#*O!Aax{lGv zvyaSQduN7aF5zieut*})IHP6;4)8PGW;A&#(sP@!SxzL>c-u$sd3ZA@yc-|;Pgn)rEAutP2>=?X$aqwjfayQ@Z>r~yZs#9gxU+cHLeI0a+=))f> zVV9zUZlzW!n!Rdy{d3vz4WQ^36sRz`4OsSdOxG3|MS}`NGz@fi^DwaBFV}z>oiduT zBf=@XyQxnC4t6*mkSm1}M&P5R>0pLoJ)8I;G`2oTTn?};4s1sL6%M_E($MyBv4NIK zm*hu(gBylTe!GGUEWc2lIo%y?aO?cyuKPxZ0T{a)Q!HMQJb(|1yA-6+*RiKVMn*jW z-4UQpmEPH=7@WT`C+3NO1NlOa@=-fF5^@$HE|Uv}zOgK`sH-$|%ZM3?L@G=?2HDws zL$`4awL)T!Kbz&gr&{He}Y_93M$Ua{Gik4v2T)TC3Ymkvte6fi8E;9^}Y=B}y|2T9_0Fol02U zzXMV?T~+RbZXSs7m9A*I>lk@bSXGZvT50kq$+=vUXj23noi<)!N<{uSuvhRZs7FTy z;w*)rp)stb3Q;krS73x&X&+A%jw&C>#Rjx6Xrl_JM*=V$O3+Wj3A`sIJXrv$*{-{5 zu}Cxpt*WxX<;EX%cyCillfOlo5BmO(H3H5(3B0~u>7%nI)G3RDI1BVi^9Fox$rE5 ztef&lofr!9)v95E{+& zfx?B$bWa0bJ)OD;R0%JT(a)3@EGgj8-p%LL^nAG=Wq@ZbnLMW^7=YSxa7kkkgMwYfipOhfR{@m+y*o*n4BWsvYc+8#?+ zeWB_4rkXNj3l9XQLzw~$`+l`+nFFN)F@!OaTwiPt>UV~BQJk~5YNG`8*%T|NftHqA zqfEwXk-^eAN$jr)V^&%)bp=M3Nw1Xyl_4SkB4jc}9)-!ZeyWEcSkpox!WDS8hwC>7 z9;}4_(X>5k&eyo-{rbSN*zp>0{N}y|^Kt5)*d-j@lt-V|RBj_sLPbM(IHsHn$~+}^ zsXeN#mHPeEQ1wpZZ<}`quTZT?98j_O0EUQS>p_3JG=1UeyWC4W=1)}n4UI;H-myjq zZ2~S8%8>g8C!dqHl>yqe2XIBWwH83>YXnpR-v}zT6PF?8-WDpjVTKY$^Lqs5cj$|0 zKz0TynmZ()COKNvdi=x^`#a?stP9|g4=Dg6$iX7WMuWZ~CpvK|wKU7_!|NH|3Jtd#}1w23b2o^ZQ zVD?WWl>b@{o&wEt@{fNlkRT_o)MMuSdpisPT;uYe&0yKVWD`Y5y1%zWVZaS3|87PW zqC*IF9*+F?_fUYg|85rCjK>fpoRt3aLI@Q5znc-P(I^b_OAZKSY0T^*vTEb{`L1RV|z?uDEzR2>cu5ex?hAC3A9 zc;z)M(knPPFq|AzT+`KXF9XF@OX{ZE2rT9cmqV|Pqk{@kl~4-}d<(CMSCLbO{#9qn zsV%gm3~7>0Ubo_5yrN`^mW4n}+rI<>oroZM2v;6emdPOuG+TR|&Xh>FSzR6-K27h}Q4vhf;4?KrTBZlywj{;CO z9LEc8K?pn!_&*-maLW|``8V+VWds~D%<}6r=LU8=qxvHq7!lBInSI$w?L!Tx5atS>@<$KOqt{rL*;UO|qT%zru$cn-J} z@qfMaUzh&ZrT>l6|2*jbJ^264rT>v(|C>wy-+MiMeC1c9;Zy2x$H5m~sk6RyjVyd6 zc!-pn=WiSIB7nfpR)wW~7?;*cop{gyq!+jP0XXMENGF#FqbtgV!w>pP7v-#9pztP) z-pL_El|fmP*&aP}8B#IJ7headA{REY7w&yL?$ z(Yz%^IF&|BBS@h9Q(^A+fXhM-L56*~`nD~^%KD^|2QTcCnpXX8rplO zR300>0}pGnc0;*r1b7goU6VM=0#JU`Qx=24khQ!7!QcE23e^nh{Q{a$g2f{xXNp^O9M#hLvO)WNr2oy7Ou66Z>f18_m@5p9#2+?{0b;4bE^o7hJhA<^W>j0vz|!ugsdK<<8oLDO-oh8CQTt# zd42&CSLyz2-K=XqSt|8EKFnd&VJJshETEgDw9SDfINe?Ib!b?a{YocVe0)5Zh2^=^ z?u6E10zQa3L{`%zAP`By52$w$wbg7FxVX4DHoHk}d`{Fr<>>);+xM3sTwIO3swTvw zAjw)^F|&;rjb{0`0f89(E#*-rvTWGA_&2`wmNop^f;iaAm8fCymI*HWu1DFSvMEVP z!Jj`P;NjzE@Z7nd@8qcENn4EMqXE(9tan5Jf=F^`W`tKn`^Euw^L;#zjg5^zZQx5# zX*lU~0DapNA_q~jv+3rNjdc+lY_v%rwBfv^J3B#Q6#G1{)|82aKn;E%3#%j|=EDzB z@9fSL_NcI16zL5KZvEnYe`7KKgF81bZ@9v^<9q)*P}ne$k|@Z>?|X|4P%}@NF?v$M zgv4WpYGZdL+qXj*C-`}ySk4usgE*UGkD6ntq3y_ADzHQAKdD;4zUII!Z~7yZ(nSZo zq7|vuzyeVSI`92_xW82))ibrFft*ldzb1QqVMt_%>V_YTPfgE>avgD^(#XcdB(ID0 z%3?_DUO9bC<}H$nWT~O&1tm5-dbDqau1^dM8_k;eS~Oqwti;;m^7WxJ6Atoj+<1xB zD;CCBENJ^P4bLgdgmhs7;KX2?AHI!T*hV6F3lH_Rct2ZfBk$=-2qIUTR@+%u08u$) zufy+I)$P9G7%9@A|FkufQ<`E-6y&|NsZdoaiBL-gG$3hTP(OejxY(Cn9UUEQXrWB# zQ3XnA677ef7A92i!oWt1iN&Q1ozK~R2t(>a?X?VsVTF98r4<#WOyjX>kD)HS8_B(p zrsAO{SPt|H9vg`2m687N46~X~b;9LzV_>+(S{oLU7>`8rbrhQb=Oa20x`m~h+^g}U z*bNOyNpw&emP35=j4$nH?&iY04nIr`X^ov*bs7m)4ymL9f>CMQj>kG=Zw_qI5CRZP zcPGA0HMwzB(+}!4a#iwr2x3zFmQuE*I5~}6(lG`pr!%zu8j1UAP{PyRZj~qQ|D$iY z&T^E_Vz`M>uZ|zlym~+UVt-aL_l5Jp9P#n-@mGz)NDtAs>26<;pq{_G=DvGUpkR{y z40=JT31wzn?&$z!QAG8o%g&?rg~+;l3WV;uddfjPKkZH^-HR%Ka!Jj2OrK6;oLJg$ z1RZou6PjykDRn3LCn7>>aY|>t3kHi!ASS-zr$$`a%)zhJNEpRL$KrauFz==6Td&}I zcj)ujakAcDX|>LI_v6OA^i3V zwEvObSM53itI21|nMk1u^7f?_c-P2ZdItnU{P}68Tq=-sJrpY5sn~p2G{8#Zilp*7 zr8?Up2n#T59xgmY=DVD8W~Le~fo|L{km>COP;v95*{C8FSpV7Pkd{hbuWNJvw??Pe-ByK!b0FF!c{=`!bvfqB&v1A~6%Rs?$)$mRN{yd|yDO@A|-Be>DOk z68Q8Ly>xjDvKkc@rs1db>;*|cBI_FgC#Hgk**Bgr-?%ueZ=H7Gm(zpP$?)bf@2Q)2 z8HXJ&rp+aPf6tdqUU=A|R6K1WP%T1eqTG-_N)MHyxGVUQaq@VKtS=!U@r8iOgqG8O z1!|Ov8X|xPrm76rQjriLgFx44)vB7poH<8W1%7mULQK@p70e18=q^RJqc7G(jX>ll zxhYyOI|fo^c7C`0BDD-Ycv;T&#jrIv;-C}AbdbhNXIacMc+l|iaS!FYhr-A9iEp#^ zx+X|$C2ZE1{VX_TZ1Yyn}uL~6SV_D z(4Mb%^FSaH_wLWT#U#$ZYbuN35}vTJgFp_gc(ClDxXQczIDI=?20XB>&idje110Ca zX?`wIOgaQdTp=S8DpSJjJzFIc@&ZI2+t|@)kt?YzFn3~XKm6MMKr;pH{;b9^8V)oZ zb9eE9<;??zG*(n6x1_JapVI)c8b{rR3qFxI>fuo>LXoe(%`Q(=;(ol{uak`J<1d1p zo~el52T=OlsR>u$JQ6B}zp|a1lGETYEsfn=b!sY`5pmuldR=a4kf=EH;aDvN734z) z`ngg?L*-6ylDm5v0LzYUW+^00o%*^*llT3_Xqi3rtFIn?kIZEaJ(Jyy)fPIGtR$`o z@K<`y?coq~Ipkk>!zycbAqLzUeIzJcQttYC-D>mL^UQk{Ow#rP>k&`%~FL4So8=@Wzho9id74ZauNJ}4)Bw8k{Z#GUrf#_egpxoN^E7OW~U^U@LJD! zx3<^Kc45L*G|McZk!Pj*FO9QMDNc{YPoqjY^&fMN{lDvC@%O3~Xydp~&dP{M>|Yp6 zPexm4KR}{cc9?I=G^yOJ$BVz@*2s0AslMvM(O_!G8ZPB)(&e^OH9~t1BC{Vz({V^$ zKAe%eR|11ci;pbRwb6_$b7@63+y(D-pDEl& zn9nH=*?)~k^J;j+o?|-x5NHQJu!`~6&cT8ye-Pe&_wE|=*fr@zzPLO*@;cQra@_C6 zl}*)x{cdJGz6+}wk7yz9vo3Eg=!U2alrZ;1QTCA9HRUMw&AnES{>*)ZWPg1=Y1FTn zcw4p;2J^g4#*oj9cAB3ovahuomk-=b;6(M0M%}!{|D7SIoq?&@Bz`zu(11g(j6;Sm zD%lenYX8V9%6xj!5scNiLM;uC1L-dfiP+hrc&AtGyYay=W9cheNF5lodG;F}bH|~X zufQWKdgwz5UGZHr+tXT@#SDG!o#s3avyR3PZH|tvhB#_YTo(V_x`^EZU-k9|B_SHkt4ccKeWRbEeXx;!n1a3UEk?ZN`1wMfw5n$fej+nC{$+M z#G+CwV%s4iSI_P6xMvWP*cg@_OmuzFj(Za;RMYv;Zjqi8x;G~i|R zS8=?*P0w?^9o2Jv^dB5*ZRHMGr75{Xq4do5)OgkNKI_@xXZeLf%0}n?b|77hmA>P8 zr&ZwCNboB@bG{q*o5JskwNL$wh2R9Vs0W)jt1D9$Jy(uMKiCZoD>2is$koieZC%Cs zaihgtLg6R1gr<)$PdDc?u53GNKWy_)!y;cBDQxKr67tjj;`=C;5y_zDu=LaN&pxNO zof^a>fS%UbP|G7cB4wz*+}{8R21O^aU;k2n6N^A||0PG5C+vj`$7Pk!on;^Bw~8)I zfxUU`3`9puYYbGH(cuey`h^n7^L2~fvu7OBUnCEEwQ*8)9DJs(&m>njywG#+vzS_| zJfJN6$H^S;1RNY3#x1`waX2o z^$lZ*4LI{>zxCb^*4wXY=8Ey6W7!BZi0##Kk&94Vi^(;8y!N#eAzg^nTo}c8U%9-? zc|$G#iX%70zN@oyD4<_^h)OdGq05)Q@<3v}mLZSF>ZX z=qFKaON<#$khi46p}r}V=Rvrt?28eZXwqpgJr%2)d$ZWQ$uJ|$t9d{9LNMg9i8KP! z9O)40b~_hg?$|;^J?Y`AvbSz2ZFJ-(PZ-uXc%sZ8%)b@o&30?9XgkI}d+{mtN#*xJ zd2VmH0AfPI!j{Cntr%D}L7Dd9<-7}}+3(~WZ-@xRq*GYU`%FwOUw{V^=SRAYuBb~b zmsTny;qQ_+ir1GS_}s2j_XM z4Z21hGoh@0Yv54xKS$Gu<_6=ye;ojRM!|%#CLh9)G?WnhA&aG~dE6c$cBEk>WL_Oz$32w(YrrbZSjlA&}n~hwf)v zYAl9LI>Ib*s6yldpMC<}sV0s@U80mu0KQI6`k$_A{ZNBg%$px{{kQBg#`h8dfq~0+ zr-Rx_FS=CEg1H~07giH*^MOvf2&F1E!P7Mfvz~!xX0j;IR2V%vTK}~dJ?)0Z$#P6m zqFYt5Mnus0PC{Ud4S=KE%vhn=ds*sCrqao3>MWJAtiN$M>kN@cp(&)*vv|92O{ZFI zyMMNqg3u{~umB6H< zU4!Oi^p%g$NiVgqVCgwK=s(1_$qIKiery@NkA+#{?aE;Q-~F0udb!@mN=vCB}jvfW7R?O4DD3A zyPkqkY>!H8t?f6rwozntAnPz2lWSZ)hHY;frJV-}P&~Rsq{%!ywMV3B52KhF@6JMM z$M3)UU(Jq9e(ZR`J2ut&BZq|l`J2`V>C@gB?uqiEc4Xd-<$_8L`$6H$F-`8R#W1vR zA@bIZRO`N)^X9G8-n-Tm+bn;>rD zI4w;U=1>|OJRdB^6;to~luD|&9%`LTJa8M%r=|0Ubc^1zPS4_+Nz0&g3)s=yN_Kb< zWPwiq_T+>~Df{byFFoCbL4PQ99$aNG+_sCd%iVU+eZ7+ZdGm`rT&<4&{&sSDYJYz|xeBc7EJggQrheqQPI#Ai$N;#w0XAauIJpG5iGAsVrJQapNpYhg<>J~<5@ zs=r5cy*uvlvONUsYcr^O(Y1p-;Mr189O@$Ej^~A2c=hQfx8`_U+sOE)s?J32yNKD_ zs1{9MMEaoR2;MmVOckdWvUF$LqlL`4EQjCU^fD|WnJbxG2%=uPmK@#Cug8Q zTGdhG;K~s+uJk0&gGEM?7gc+$cey{{MyI1VE972nq}iF=T^%PV7u44iIh@dg!fwvC z{kJSO1s>IWb=bvb>Z}8!%qL_X4R27RMW`_$4|~j-INU}GYHFZNuMI{#+pVQtTCIuQ z_SuilZOZy1LZmEjLU&AEtcK zaOtugH~;Kx$lx3dWD1jd-CnSAC2@R!@OJ_6)LKo_fg7x}d%~LZXBV|ZwHg>~Vv*b0aqoAo> z83g=jIr~jkoy&efNkv5_L%`|f$0f0|-AR=f8aN_yW{b}IGmNC0g_-W?$HyC#zjoWg z5jyqYPr6C%-VWa}UVTtLQp>JRX*zBLnI5oVT6hTEiyhJFL^Xe6$ZlDO2;WQ!sH8gK z-KsqAN!Jzv{S0S-%q_TWlEb)~Jz1YX!}^mxX8Rs;mu%<#B`>{jqB=F3&?9Vkh*Urd zjYWKH{j1@xY%gpk1EO-Di@64rG4SV5;-PbWVLs4X(*_~MNJ8VVS*?j z5}nKXxdxS*%5O(=^c9UNDR45XiaW=0x+4*2#$LytQu!^SJ*N~Qqx3q9aS(}jSnCDh z+iFNy%Xg;HDkL%(LwL#R<{ZLsXp~v-z!PZE=ev`uC&rspj+Pkow&XJ5Zk@tC_VcWR z%RmJ4m^RsRjGy6W)*;?tiQd71%+x&cu(J++(n=t+<`q;7l_L)=VvjFg}v9;W5` z5%Yc|UoM&7u>ig1cIsNOOqYCjkoT~Y@Qt%cUp%9%yga9a?uStxWSC9jXGAJ^FSKxC zF6orl7Ib(fhE&T~UB)`Ovla+YcXDC(>USARjAEs@VD^?P2M@iudlX1)nyB9r4QG)Wd(9xj4P;_ijgTY(m_9m~iqlTc>* z74>js*{%J{<0&`cl+C=Lz_i;|R-G1)tQ%2}J)P4#M}%n=cLPcITtc~8Ndz8hFmG^R zU|`>xJhZnx5JO9X)jpAM^s~018!`~!63))G0>m*4wEuKsf3N{eP{yXcS7APlV|HsW z{Hpz|@dEYxB?V7Db6v944LUnAqmDl}0|SFE^fd-D{7Oc&szZyc*_k*|0F-ER60*^{ z(l6GEwdkBl0N)=8Z{wNf_gKRqc4ywrEc+9L6iFc*db!}C5)y$+ubd5(j5|oC*C*?J z1g;`EFIH`RtY<6RbOY4u^d2hceg%!le`v zlio8;I?g?9Y=SOlY)pva`YYBJ40ZR?L%&26y_=bU{T(+9Y&7Udwm71s(x34-#CR86_l60f(JSQ`16m_p&F7$R*n}0 zU>rSu2n0boXk}bkl*nPS&YY@EtK&R7*0SI&O zqW3K;4U(*~Ddvc~yRzdNO%njE1fgN?pZP~_wjA(%J!-@Wa(TGBj;w(mdDRwLsrw>H zL`+8eBBIKUF^9O)svOVMp3}(8Hha|d0g?4Cs^~F5B1hF%hgxh+n)lB96;?4E92{(7V#{PWkRP=fROtT5 zrpI#GWj_8^}S z_Q+kjlHr0dJp6;bhNg^VhL>DrVN&t3mKOJB0g?7cowc;T&u!I%5P_$LuM2^*RA2j- zAVqOovew5357=VI{pDi$%~a*PV0XeQC0_j0x5zCsdJ9ouCVBSrDddQDy|mg^(}VAS z*oJyb!JGKMP=?2uf~fjlb?^5vmv^>4oU=R49T;Ge3*_e-s{t<$AJ>pEh?Ui^!d+~1 zFP$)_tkaZlWnc{mbJ}w24hoqj0y=jqQbttK-%2Mbygq z*1MDb zcwmAuK4h5B5u@1MYK_mMw_=6>zOJrrU${g-TC!HP^(3Z>@Ht+_(0#}D+{`mH?7WO2 zTEwfgteWnVEa4p0eSKR}7YxWQOLu=1$tu=ql+T9AXdCHx2V!#=a1#*&7Y94N*xF&R zsp-Q?82j15$izNyXTI&vlWN^rtmW=bC)bN1K%`c`*BaF>4d%;k9KL*tWs}Z7CzVR* z*p%Y;h7kyzqK4{kpux~KoMknA{Zh`X%dKHa{=#5+Ib}l+D<*nnIFE~XFUmln=N$#A zDOPV!U&{K*5-n_IH6btl=4N|-SpSRvE1?^)q0j03&kfslEudspM=r>tS&%Q^oycFp*R&z>{Ti!HeCvf>B5R{k-ksU8#hMJF^j7jk#C z8fCRpnNkTG_;`T3!>X#6>3v-A*gmz5}uMz(33}&v+YOz#CIG7B~%Yb9auYUN@EKQorOKKZd2ottgKWo_>~-o zzN%5DJO1G>J|GGgXF0>w(z`Oa@zHEmdNs;ibUyF0NX5Z1TzShh<)`6`r0^293j~h8 z47@s5c?y^Lg#Pv=2~H;O8ai_Cwt`hOTufxDl=RzCUe@7aH}a0!*en0=dO@sXbAw22 z!`njR`>WZbF!nq0D)VOF_{r2!vpPmQT)v+KcBdFy;{I6o>zJ|!6I7YC`UCW|UM+1G zzsHDCH*)!BK_dCbF+3bPuAV|?JdV2vEXe$E%f*?fYG4!RE~n6 zd?yM^FU9k7tsi&pTeuZvd)=&j1|b zhlZ*m!$eO>Y1gZ|BB@9s%u3vjtkJplDRv=zb~M

^YxpKJiH#{KwPwox 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 diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 2da994bf5f7..69937c44042 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -4,6 +4,7 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var Dates = require('@src/lib/dates'); +var Axes = require('@src/plots/cartesian/axes'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); @@ -23,17 +24,26 @@ describe('Test annotations', function() { expect(annotationDefaults.annotations[0].ayref).toEqual('pixel'); }); - it('should convert ax/ay date coordinates to milliseconds if tail is in axis terms and axis is a date', function() { - var annotationOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }}; - annotationOut._has = Plots._hasPlotType.bind(annotationOut); - - var annotationIn = { - annotations: [{ showarrow: true, axref: 'x', ayref: 'y', x: '2008-07-01', ax: '2004-07-01', y: 0, ay: 50}] + it('should convert ax/ay date coordinates to date string if tail is in milliseconds and axis is a date', function() { + var layoutOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }}; + layoutOut._has = Plots._hasPlotType.bind(layoutOut); + Axes.setConvert(layoutOut.xaxis); + + var layoutIn = { + annotations: [{ + showarrow: true, + axref: 'x', + ayref: 'y', + x: '2008-07-01', + ax: Dates.dateTime2ms('2004-07-01'), + y: 0, + ay: 50 + }] }; - Annotations.supplyLayoutDefaults(annotationIn, annotationOut); + Annotations.supplyLayoutDefaults(layoutIn, layoutOut); - expect(annotationIn.annotations[0].ax).toEqual(Dates.dateTime2ms('2004-07-01')); + expect(layoutOut.annotations[0].ax).toEqual('2004-07-01'); }); }); }); diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index d0aa516eade..0394f2b572a 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -323,10 +323,6 @@ describe('finance charts calc transforms:', function() { return gd.calcdata.map(calcDatatoTrace); } - function ms2DateTime(v) { - return typeof v === 'number' ? Lib.ms2DateTime(v) : null; - } - it('should fill when *x* is not present', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', @@ -442,7 +438,7 @@ describe('finance charts calc transforms:', function() { expect(out.length).toEqual(4); - expect(out[0].x.map(ms2DateTime)).toEqual([ + expect(out[0].x).toEqual([ '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null, '2016-09-05 22:48', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06', '2016-09-06 01:12', null, '2016-09-09 22:48', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10', '2016-09-10 01:12', null @@ -452,7 +448,7 @@ describe('finance charts calc transforms:', function() { 33.05, 33.05, 33.25, 32.75, 33.1, 33.1, null, 33.5, 33.5, 34.62, 32.87, 33.7, 33.7, null ]); - expect(out[1].x.map(ms2DateTime)).toEqual([ + expect(out[1].x).toEqual([ '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null, '2016-09-04 22:48', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05', '2016-09-05 01:12', null, @@ -493,15 +489,15 @@ describe('finance charts calc transforms:', function() { var out = _calc([trace0]); expect(out[0].name).toEqual('trace 0 - increasing'); - expect(out[0].x.map(ms2DateTime)).toEqual([ - '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null, + expect(out[0].x).toEqual([ + '2016-08-31 22:48', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01', '2016-09-01 01:12', null ]); expect(out[0].y).toEqual([ 33.01, 33.01, 34.2, 31.7, 34.1, 34.1, null, ]); expect(out[1].name).toEqual('trace 0 - decreasing'); - expect(out[1].x.map(ms2DateTime)).toEqual([ + expect(out[1].x).toEqual([ '2016-09-01 22:48', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02', '2016-09-02 01:12', null, '2016-09-02 22:48', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03', '2016-09-03 01:12', null ]); @@ -511,15 +507,15 @@ describe('finance charts calc transforms:', function() { ]); expect(out[2].name).toEqual('trace 0 - increasing'); - expect(out[2].x.map(ms2DateTime)).toEqual([ - '2016-09-03 23:59:59.999', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', null + expect(out[2].x).toEqual([ + '2016-09-03 23:59:59.9999', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04', '2016-09-04 00:00:00.0001', null ]); expect(out[2].y).toEqual([ 32.06, 32.06, 34.25, 31.62, 33.18, 33.18, null ]); expect(out[3].name).toEqual('trace 0 - decreasing'); - expect(out[3].x.map(ms2DateTime)).toEqual([]); + expect(out[3].x).toEqual([]); expect(out[3].y).toEqual([]); }); diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js new file mode 100644 index 00000000000..f4771c539a4 --- /dev/null +++ b/test/jasmine/tests/lib_date_test.js @@ -0,0 +1,260 @@ +var isNumeric = require('fast-isnumeric'); +var Lib = require('@src/lib'); + +describe('dates', function() { + 'use strict'; + + var d1c = new Date(2000, 0, 1, 1, 0, 0, 600); + // first-century years must be set separately as Date constructor maps 2-digit years + // to near the present, but we accept them as part of 4-digit years + d1c.setFullYear(13); + + var thisYear = new Date().getFullYear(), + thisYear_2 = thisYear % 100, + nowMinus70 = thisYear - 70, + nowMinus70_2 = nowMinus70 % 100, + nowPlus29 = thisYear + 29, + nowPlus29_2 = nowPlus29 % 100; + + describe('dateTime2ms', function() { + it('should accept valid date strings', function() { + + [ + ['2016', new Date(2016, 0, 1)], + ['2016-05', new Date(2016, 4, 1)], + // leap year, and whitespace + ['\r\n\t 2016-02-29\r\n\t ', new Date(2016, 1, 29)], + ['9814-08-23', new Date(9814, 7, 23)], + ['1564-03-14 12', new Date(1564, 2, 14, 12)], + ['0122-04-08 08:22', new Date(122, 3, 8, 8, 22)], + ['-0098-11-19 23:59:59', new Date(-98, 10, 19, 23, 59, 59)], + ['-9730-12-01 12:34:56.789', new Date(-9730, 11, 1, 12, 34, 56, 789)], + // first century, also allow month, day, and hour to be 1-digit, and not all + // three digits of milliseconds + ['0013-1-1 1:00:00.6', d1c], + // we support more than 4 digits too, though Date objects don't. More than that + // and we hit the precision limit of js numbers unless we're close to the epoch. + // It won't break though. + ['0013-1-1 1:00:00.6001', +d1c + 0.1], + + // 2-digit years get mapped to now-70 -> now+29 + [thisYear_2 + '-05', new Date(thisYear, 4, 1)], + [nowMinus70_2 + '-10-18', new Date(nowMinus70, 9, 18)], + [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)] + ].forEach(function(v) { + expect(Lib.dateTime2ms(v[0])).toBe(+v[1], v[0]); + }); + }); + + it('should accept 4-digit and 2-digit numbers', function() { + // not sure if we really *want* this behavior, but it's what we have. + // especially since the number 0 is *not* allowed it seems pretty unlikely + // to cause problems for people using milliseconds as dates, since the only + // values to get mistaken are between 1 and 10 seconds before and after + // the epoch, and between 10 and 99 milliseconds after the epoch + // (note that millisecond numbers are not handled by dateTime2ms directly, + // but in ax.d2c if dateTime2ms fails.) + [ + 1000, 9999, -1000, -9999 + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBe(+(new Date(v, 0, 1)), v); + }); + + [ + [10, 2010], + [nowPlus29_2, nowPlus29], + [nowMinus70_2, nowMinus70], + [99, 1999] + ].forEach(function(v) { + expect(Lib.dateTime2ms(v[0])).toBe(+(new Date(v[1], 0, 1)), v[0]); + }); + }); + + it('should accept Date objects within year +/-9999', function() { + [ + new Date(), + new Date(-9999, 0, 1), + new Date(9999, 11, 31, 23, 59, 59, 999), + new Date(-1, 0, 1), + new Date(323, 11, 30), + new Date(-456, 1, 2), + d1c, + new Date(2015, 8, 7, 23, 34, 45, 567) + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBe(+v); + }); + }); + + it('should not accept Date objects beyond our limits', function() { + [ + new Date(10000, 0, 1), + new Date(-10000, 11, 31, 23, 59, 59, 999) + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBeUndefined(v); + }); + }); + + it('should not accept invalid strings or other objects', function() { + [ + '', 0, 1, 9, -1, -10, -99, 100, 999, -100, -999, 10000, -10000, + 1.2, -1.2, 2015.1, -1023.4, NaN, null, undefined, Infinity, -Infinity, + {}, {1: '2014-01-01'}, [], [2016], ['2015-11-23'], + '123-01-01', '-756-01-01', // 3-digit year + '10000-01-01', '-10000-01-01', // 5-digit year + '2015-00-01', '2015-13-01', '2015-001-01', // bad month + '2015-01-00', '2015-01-32', '2015-02-29', '2015-04-31', '2015-01-001', // bad day (incl non-leap year) + '2015-01-01 24:00', '2015-01-01 -1:00', '2015-01-01 001:00', // bad hour + '2015-01-01 12:60', '2015-01-01 12:-1', '2015-01-01 12:001', '2015-01-01 12:1', // bad minute + '2015-01-01 12:00:60', '2015-01-01 12:00:-1', '2015-01-01 12:00:001', '2015-01-01 12:00:1' // bad second + ].forEach(function(v) { + expect(Lib.dateTime2ms(v)).toBeUndefined(v); + }); + }); + }); + + describe('ms2DateTime', function() { + it('should report the minimum fields with nonzero values, except minutes', function() { + [ + '2016-01-01', // we'll never report less than this bcs month and day are never zero + '2016-01-01 01:00', // we won't report hours without minutes + '2016-01-01 01:01', + '2016-01-01 01:01:01', + '2016-01-01 01:01:01.1', + '2016-01-01 01:01:01.01', + '2016-01-01 01:01:01.001', + '2016-01-01 01:01:01.0001' + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); + }); + }); + + it('should accept Date objects within year +/-9999', function() { + [ + '-9999-01-01', + '-9999-01-01 00:00:00.0001', + '9999-12-31 23:59:59.9999', + '0123-01-01', + '0042-01-01', + '-0016-01-01', + '-0016-01-01 12:34:56.7891', + '-0456-07-23 16:22' + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v))).toBe(v); + }); + }); + + it('should not accept Date objects beyond our limits or other objects', function() { + [ + +(new Date(10000, 0, 1)), + +(new Date(-10000, 11, 31, 23, 59, 59, 999)), + '', + '2016-01-01', + '0', + [], [0], {}, {1: 2} + ].forEach(function(v) { + expect(Lib.ms2DateTime(v)).toBeUndefined(v); + }); + }); + + it('should drop the right pieces if rounding is specified', function() { + [ + ['2016-01-01 00:00:00.0001', 0, '2016-01-01 00:00:00.0001'], + ['2016-01-01 00:00:00.0001', 299999, '2016-01-01 00:00:00.0001'], + ['2016-01-01 00:00:00.0001', 300000, '2016-01-01'], + ['2016-01-01 00:00:00.0001', 7776000000, '2016-01-01'], + ['2016-01-01 12:34:56.7891', 0, '2016-01-01 12:34:56.7891'], + ['2016-01-01 12:34:56.7891', 299999, '2016-01-01 12:34:56.7891'], + ['2016-01-01 12:34:56.7891', 300000, '2016-01-01 12:34:56'], + ['2016-01-01 12:34:56.7891', 10799999, '2016-01-01 12:34:56'], + ['2016-01-01 12:34:56.7891', 10800000, '2016-01-01 12:34'], + ['2016-01-01 12:34:56.7891', 7775999999, '2016-01-01 12:34'], + ['2016-01-01 12:34:56.7891', 7776000000, '2016-01-01'], + ['2016-01-01 12:34:56.7891', 1e300, '2016-01-01'] + ].forEach(function(v) { + expect(Lib.ms2DateTime(Lib.dateTime2ms(v[0]), v[1])).toBe(v[2], v); + }); + }); + }); + + describe('cleanDate', function() { + it('should convert any number or js Date within range to a date string', function() { + [ + new Date(0), + new Date(2000), + new Date(2000, 0, 1), + new Date(), + new Date(-9999, 0, 1), + new Date(9999, 11, 31, 23, 59, 59, 999) + ].forEach(function(v) { + expect(typeof Lib.ms2DateTime(+v)).toBe('string'); + expect(Lib.cleanDate(v)).toBe(Lib.ms2DateTime(+v)); + expect(Lib.cleanDate(+v)).toBe(Lib.ms2DateTime(+v)); + expect(Lib.cleanDate(v, '2000-01-01')).toBe(Lib.ms2DateTime(+v)); + }); + }); + + it('should fail numbers & js Dates out of range, and other bad objects', function() { + [ + new Date(-20000, 0, 1), + new Date(20000, 0, 1), + new Date('fail'), + undefined, null, NaN, + [], {}, [0], {1: 2}, '', + '2001-02-29' // not a leap year + ].forEach(function(v) { + expect(Lib.cleanDate(v)).toBeUndefined(); + if(!isNumeric(+v)) expect(Lib.cleanDate(+v)).toBeUndefined(); + expect(Lib.cleanDate(v, '2000-01-01')).toBe('2000-01-01'); + }); + }); + + it('should not alter valid date strings, even to truncate them', function() { + [ + '2000', + '2000-01', + '2000-01-01', + '2000-01-01 00', + '2000-01-01 00:00', + '2000-01-01 00:00:00', + '2000-01-01 00:00:00.0', + '2000-01-01 00:00:00.00', + '2000-01-01 00:00:00.000', + '2000-01-01 00:00:00.0000', + '9999-12-31 23:59:59.9999', + '-9999-01-01 00:00:00.0000', + '99-01-01', + '00-01-01' + ].forEach(function(v) { + expect(Lib.cleanDate(v)).toBe(v); + }); + }); + }); + + describe('isJSDate', function() { + it('should return true for any Date object but not the equivalent numbers', function() { + [ + new Date(), + new Date(0), + new Date(-9900, 1, 2, 3, 4, 5, 6), + new Date(9900, 1, 2, 3, 4, 5, 6), + new Date(-20000, 0, 1), new Date(20000, 0, 1), // outside our range, still true + new Date('fail') // `Invalid Date` is still a Date + ].forEach(function(v) { + expect(Lib.isJSDate(v)).toBe(true); + expect(Lib.isJSDate(+v)).toBe(false); + }); + }); + + it('should return false for anything thats not explicitly a JS Date', function() { + [ + 0, NaN, null, undefined, '', {}, [], [0], [2016, 0, 1], + '2016-01-01', '2016-01-01 12:34:56', '2016-01-01 12:34:56.789', + 'Thu Oct 20 2016 15:35:14 GMT-0400 (EDT)', + // getting really close to a hack of our test... we look for getTime to be a function + {getTime: 4} + ].forEach(function(v) { + expect(Lib.isJSDate(v)).toBe(false); + }); + }); + }); +}); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 39f753ae0bc..d252ff9b8de 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1399,6 +1399,42 @@ describe('Test lib.js:', function() { }).toThrowError('Separator string required for formatting!'); }); }); + + describe('cleanNumber', function() { + it('should return finite numbers untouched', function() { + [ + 0, 1, 2, 1234.567, + -1, -100, -999.999, + Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON, + -Number.MAX_VALUE, -Number.MIN_VALUE, -Number.EPSILON + ].forEach(function(v) { + expect(Lib.cleanNumber(v)).toBe(v); + }); + }); + + it('should accept number strings with arbitrary cruft on the outside', function() { + [ + ['0', 0], + ['1', 1], + ['1.23', 1.23], + ['-100.001', -100.001], + [' $4.325 #%\t', 4.325], + [' " #1" ', 1], + [' \'\n \r -9.2e7 \t\' ', -9.2e7] + ].forEach(function(v) { + expect(Lib.cleanNumber(v[0])).toBe(v[1], v[0]); + }); + }); + + it('should not accept other objects or cruft in the middle', function() { + [ + NaN, Infinity, -Infinity, null, undefined, new Date(), '', + ' ', '\t', '2 2', '2%2', '2$2', {1: 2}, [1], ['1'], {}, [] + ].forEach(function(v) { + expect(Lib.cleanNumber(v)).toBeUndefined(v); + }); + }); + }); }); describe('Queue', function() { diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 74835037888..cae2373e79b 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -312,7 +312,7 @@ describe('range selector getUpdateObject:', function() { count: 5 }; - assertUpdateCase(buttonLayout, '2015-11-29 19', '2015-11-30'); + assertUpdateCase(buttonLayout, '2015-11-29 19:00', '2015-11-30'); assertUpdateCase(buttonLayout, '2015-11-30 07:34:56', '2015-11-30 12:34:56'); }); @@ -346,8 +346,8 @@ describe('range selector getUpdateObject:', function() { }; assertUpdateCase(buttonLayout, '2015-11-30', '2015-11-30 12'); - assertUpdateCase(buttonLayout, '2015-11-30 01', '2015-11-30 12:00:01'); - assertUpdateCase(buttonLayout, '2015-11-30 01', '2015-11-30 13'); + assertUpdateCase(buttonLayout, '2015-11-30 01:00', '2015-11-30 12:00:01'); + assertUpdateCase(buttonLayout, '2015-11-30 01:00', '2015-11-30 13'); }); it('should return update object (20 minute to-date case)', function() { @@ -357,7 +357,7 @@ describe('range selector getUpdateObject:', function() { count: 20 }; - assertUpdateCase(buttonLayout, '2015-11-30 12', '2015-11-30 12:20'); + assertUpdateCase(buttonLayout, '2015-11-30 12:00', '2015-11-30 12:20'); assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:20:01'); assertUpdateCase(buttonLayout, '2015-11-30 12:01', '2015-11-30 12:21'); }); From c045bc57c842c1b95ab9a6815df53689b3923579 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 24 Oct 2016 23:37:52 -0400 Subject: [PATCH 05/23] wider acceptance range for mapbox plot size test so it works locally (for me alexcjohnson anyway) --- test/jasmine/tests/mapbox_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 5d29754ba3f..7455baa40e7 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -449,7 +449,7 @@ describe('mapbox plots', function() { var divStyle = mapInfo.div.style; ['left', 'top', 'width', 'height'].forEach(function(p, i) { - expect(parseFloat(divStyle[p])).toBeWithin(dims[i], 5); + expect(parseFloat(divStyle[p])).toBeWithin(dims[i], 8); }); } From 3dd294fa8f482fb833deab16b1e2f930aff07ef1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 24 Oct 2016 23:38:55 -0400 Subject: [PATCH 06/23] fix for range slider tests with new axis range machinery --- test/jasmine/tests/range_slider_test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 60b7ef88444..464c5a49776 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,5 +1,6 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); +var setConvert = require('@src/plots/cartesian/set_convert'); var RangeSlider = require('@src/components/rangeslider'); var constants = require('@src/components/rangeslider/constants'); @@ -473,7 +474,7 @@ describe('the range slider', function() { it('should expand the rangeslider range to axis range', function() { var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } }, yaxis: {}}, - layoutOut = { xaxis: { range: [1, 10]}, yaxis: {}}, + layoutOut = { xaxis: { range: [1, 10], type: 'linear'}, yaxis: {}}, axName = 'xaxis', counterAxes = ['yaxis'], expected = { @@ -491,10 +492,14 @@ describe('the range slider', function() { }, yaxis: { fixedrange: true } }; + setConvert(layoutOut.xaxis); RangeSlider.handleDefaults(layoutIn, layoutOut, axName, counterAxes); - expect(layoutOut).toEqual(expected); + // don't compare the whole layout, because we had to run setConvert which + // attaches all sorts of other stuff to xaxis + expect(layoutOut.xaxis.rangeslider).toEqual(expected.xaxis.rangeslider); + expect(layoutOut.yaxis).toEqual(expected.yaxis); }); it('should set _needsExpand when an axis range is set', function() { From 451ee2493556702aa1aed1eaadcf34c7bc5bf201 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 25 Oct 2016 14:33:46 -0400 Subject: [PATCH 07/23] fix date axis ranges and date interactions in gl2d --- src/plots/gl2d/camera.js | 59 ++++++++++++++++++++------------------- src/plots/gl2d/scene2d.js | 34 +++++++++++++++------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/plots/gl2d/camera.js b/src/plots/gl2d/camera.js index 50fd45eaa78..80f56fefccf 100644 --- a/src/plots/gl2d/camera.js +++ b/src/plots/gl2d/camera.js @@ -38,8 +38,7 @@ function createCamera(scene) { } result.mouseListener = mouseChange(element, function(buttons, x, y) { - var xrange = scene.xaxis.range, - yrange = scene.yaxis.range, + var dataBox = scene.calcDataBox(), viewBox = plot.viewBox; var lastX = result.lastPos[0], @@ -51,14 +50,15 @@ function createCamera(scene) { // mouseChange gives y about top; convert to about bottom y = (viewBox[3] - viewBox[1]) - y; - function updateRange(range, start, end) { + function updateRange(i0, start, end) { var range0 = Math.min(start, end), range1 = Math.max(start, end); if(range0 !== range1) { - range[0] = range0; - range[1] = range1; - result.dataBox = range; + dataBox[i0] = range0; + dataBox[i0 + 2] = range1; + result.dataBox = dataBox; + scene.setRanges(dataBox); } else { scene.selectBox.selectBox = [0, 0, 1, 1]; @@ -70,11 +70,11 @@ function createCamera(scene) { case 'zoom': if(buttons) { var dataX = x / - (viewBox[2] - viewBox[0]) * (xrange[1] - xrange[0]) + - xrange[0]; + (viewBox[2] - viewBox[0]) * (dataBox[2] - dataBox[0]) + + dataBox[0]; var dataY = y / - (viewBox[3] - viewBox[1]) * (yrange[1] - yrange[0]) + - yrange[0]; + (viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + + dataBox[1]; if(!result.boxEnabled) { result.boxStart[0] = dataX; @@ -87,8 +87,8 @@ function createCamera(scene) { result.boxEnabled = true; } else if(result.boxEnabled) { - updateRange(xrange, result.boxStart[0], result.boxEnd[0]); - updateRange(yrange, result.boxStart[1], result.boxEnd[1]); + updateRange(0, result.boxStart[0], result.boxEnd[0]); + updateRange(1, result.boxStart[1], result.boxEnd[1]); unSetAutoRange(); result.boxEnabled = false; } @@ -98,15 +98,17 @@ function createCamera(scene) { result.boxEnabled = false; if(buttons) { - var dx = (lastX - x) * (xrange[1] - xrange[0]) / + var dx = (lastX - x) * (dataBox[2] - dataBox[0]) / (plot.viewBox[2] - plot.viewBox[0]); - var dy = (lastY - y) * (yrange[1] - yrange[0]) / + var dy = (lastY - y) * (dataBox[3] - dataBox[1]) / (plot.viewBox[3] - plot.viewBox[1]); - xrange[0] += dx; - xrange[1] += dx; - yrange[0] += dy; - yrange[1] += dy; + dataBox[0] += dx; + dataBox[2] += dx; + dataBox[1] += dy; + dataBox[3] += dy; + + scene.setRanges(dataBox); result.lastInputTime = Date.now(); unSetAutoRange(); @@ -120,8 +122,7 @@ function createCamera(scene) { }); result.wheelListener = mouseWheel(element, function(dx, dy) { - var xrange = scene.xaxis.range, - yrange = scene.yaxis.range, + var dataBox = scene.calcDataBox(), viewBox = plot.viewBox; var lastX = result.lastPos[0], @@ -135,16 +136,18 @@ function createCamera(scene) { var scale = Math.exp(0.1 * dy / (viewBox[3] - viewBox[1])); var cx = lastX / - (viewBox[2] - viewBox[0]) * (xrange[1] - xrange[0]) + - xrange[0]; + (viewBox[2] - viewBox[0]) * (dataBox[2] - dataBox[0]) + + dataBox[0]; var cy = lastY / - (viewBox[3] - viewBox[1]) * (yrange[1] - yrange[0]) + - yrange[0]; + (viewBox[3] - viewBox[1]) * (dataBox[3] - dataBox[1]) + + dataBox[1]; + + dataBox[0] = (dataBox[0] - cx) * scale + cx; + dataBox[2] = (dataBox[2] - cx) * scale + cx; + dataBox[1] = (dataBox[1] - cy) * scale + cy; + dataBox[3] = (dataBox[3] - cy) * scale + cy; - xrange[0] = (xrange[0] - cx) * scale + cx; - xrange[1] = (xrange[1] - cx) * scale + cx; - yrange[0] = (yrange[0] - cy) * scale + cy; - yrange[1] = (yrange[1] - cy) * scale + cy; + scene.setRanges(dataBox); result.lastInputTime = Date.now(); unSetAutoRange(); diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 6b0c8262fe6..583d94def83 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -311,14 +311,9 @@ var relayoutCallback = function(scene) { }; proto.cameraChanged = function() { - var camera = this.camera, - xrange = this.xaxis.range, - yrange = this.yaxis.range; + var camera = this.camera; - this.glplot.setDataBox([ - xrange[0], yrange[0], - xrange[1], yrange[1] - ]); + this.glplot.setDataBox(this.calcDataBox()); var nextTicks = this.computeTickMarks(); var curTicks = this.glplotOptions.ticks; @@ -416,9 +411,7 @@ proto.plot = function(fullData, calcData, fullLayout) { options.ticks = this.computeTickMarks(); - var xrange = this.xaxis.range; - var yrange = this.yaxis.range; - options.dataBox = [xrange[0], yrange[0], xrange[1], yrange[1]]; + options.dataBox = this.calcDataBox(); options.merge(fullLayout); glplot.update(options); @@ -427,6 +420,27 @@ proto.plot = function(fullData, calcData, fullLayout) { this.glplot.draw(); }; +proto.calcDataBox = function() { + var xaxis = this.xaxis, + yaxis = this.yaxis, + xrange = xaxis.range, + yrange = yaxis.range, + xr2l = xaxis.r2l, + yr2l = yaxis.r2l; + + return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])]; +}; + +proto.setRanges = function(dataBox) { + var xaxis = this.xaxis, + yaxis = this.yaxis, + xl2r = xaxis.l2r, + yl2r = yaxis.l2r; + + xaxis.range = [xl2r(dataBox[0]), xl2r(dataBox[2])]; + yaxis.range = [yl2r(dataBox[1]), yl2r(dataBox[3])]; +}; + proto.updateTraces = function(fullData, calcData) { var traceIds = Object.keys(this.traces); var i, j, fullTrace; From 51a05636d8716f0aa9d193c4c68a865035ef67ec Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 25 Oct 2016 21:33:13 -0400 Subject: [PATCH 08/23] lib/constants -> constants/numerical --- src/{lib/constants.js => constants/numerical.js} | 0 src/lib/clean_number.js | 2 +- src/lib/dates.js | 2 +- src/lib/index.js | 4 ---- src/plots/cartesian/axes.js | 2 +- src/plots/cartesian/axis_autotype.js | 3 ++- src/plots/cartesian/set_convert.js | 5 +++-- src/traces/scatter/line_points.js | 5 ++--- 8 files changed, 10 insertions(+), 13 deletions(-) rename src/{lib/constants.js => constants/numerical.js} (100%) diff --git a/src/lib/constants.js b/src/constants/numerical.js similarity index 100% rename from src/lib/constants.js rename to src/constants/numerical.js diff --git a/src/lib/clean_number.js b/src/lib/clean_number.js index d8736ecf93a..dbedfe2e2a6 100644 --- a/src/lib/clean_number.js +++ b/src/lib/clean_number.js @@ -11,7 +11,7 @@ var isNumeric = require('fast-isnumeric'); -var BADNUM = require('./constants').BADNUM; +var BADNUM = require('../constants/numerical').BADNUM; // precompile these regex's for speed var FRONTJUNK = /^['"%,$#\s']+/; diff --git a/src/lib/dates.js b/src/lib/dates.js index bd2e4c8b8e5..49778767642 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -11,7 +11,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); -var BADNUM = require('./constants').BADNUM; +var BADNUM = require('../constants/numerical').BADNUM; var logError = require('./loggers').error; // is an object a javascript date? diff --git a/src/lib/index.js b/src/lib/index.js index b9aee5c8956..adca268a068 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -78,10 +78,6 @@ lib.error = loggersModule.error; lib.notifier = require('./notifier'); -var constantsModule = require('./constants'); -lib.BADNUM = constantsModule.BADNUM, -lib.FP_SAFE = constantsModule.FP_SAFE; - lib.filterUnique = require('./filter_unique'); lib.cleanNumber = require('./clean_number'); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 09111436b37..97c166dc6be 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -19,7 +19,7 @@ var Titles = require('../../components/titles'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); -var FP_SAFE = Lib.FP_SAFE; +var FP_SAFE = require('../../constants/numerical').FP_SAFE; var axes = module.exports = {}; diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js index 96468ec8735..656ff1e0003 100644 --- a/src/plots/cartesian/axis_autotype.js +++ b/src/plots/cartesian/axis_autotype.js @@ -12,6 +12,7 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); +var BADNUM = require('../../constants/numerical').BADNUM; module.exports = function autoType(array) { if(moreDates(array)) return 'date'; @@ -64,7 +65,7 @@ function category(a) { for(var i = 0; i < a.length; i += inc) { ai = a[Math.round(i)]; - if(Lib.cleanNumber(ai) !== Lib.BADNUM) curvenums++; + if(Lib.cleanNumber(ai) !== BADNUM) curvenums++; else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; } diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 4307ea35b67..4ff5ef380ad 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -13,8 +13,9 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); -var FP_SAFE = Lib.FP_SAFE; -var BADNUM = Lib.BADNUM; +var numConstants = require('../../constants/numerical'); +var FP_SAFE = numConstants.FP_SAFE; +var BADNUM = numConstants.BADNUM; var constants = require('./constants'); var axisIds = require('./axis_ids'); diff --git a/src/traces/scatter/line_points.js b/src/traces/scatter/line_points.js index 390242a1fd7..4fc789180dc 100644 --- a/src/traces/scatter/line_points.js +++ b/src/traces/scatter/line_points.js @@ -9,7 +9,7 @@ 'use strict'; -var Axes = require('../../plots/cartesian/axes'); +var BADNUM = require('../../constants/numerical').BADNUM; module.exports = function linePoints(d, opts) { @@ -20,7 +20,6 @@ module.exports = function linePoints(d, opts) { baseTolerance = opts.baseTolerance, linear = opts.linear, segments = [], - badnum = Axes.BADNUM, minTolerance = 0.2, // fraction of tolerance "so close we don't even consider it a new point" pts = new Array(d.length), pti = 0, @@ -57,7 +56,7 @@ module.exports = function linePoints(d, opts) { function getPt(index) { var x = xa.c2p(d[index].x), y = ya.c2p(d[index].y); - if(x === badnum || y === badnum) return false; + if(x === BADNUM || y === BADNUM) return false; return [x, y]; } From 5ccd0832beae3eda13d6968198a89a6c27d7f540 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 25 Oct 2016 22:16:27 -0400 Subject: [PATCH 09/23] td -> gd once and for all except for image server compatibility, we leave td in plotTile --- src/components/annotations/annotation_defaults.js | 10 +++++----- src/components/drawing/index.js | 4 ++-- src/components/images/defaults.js | 4 ++-- src/components/shapes/shape_defaults.js | 10 +++++----- src/lib/index.js | 2 +- src/lib/svg_text_utils.js | 4 ++-- src/plot_api/to_image.js | 2 +- src/plots/cartesian/axes.js | 4 ++-- src/plots/cartesian/axis_autotype.js | 2 +- src/plots/cartesian/layout_attributes.js | 4 ++-- src/plots/cartesian/set_convert.js | 2 +- src/snapshot/cloneplot.js | 11 ++++++----- src/snapshot/toimage.js | 2 +- test/jasmine/tests/snapshot_test.js | 3 ++- 14 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index 7b5f2da9689..e414483f669 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -42,20 +42,20 @@ module.exports = function handleAnnotationDefaults(annIn, fullLayout) { // positioning var axLetters = ['x', 'y'], arrowPosDflt = [-10, -30], - tdMock = {_fullLayout: fullLayout}; + gdMock = {_fullLayout: fullLayout}; for(var i = 0; i < 2; i++) { var axLetter = axLetters[i]; // xref, yref - var axRef = Axes.coerceRef(annIn, annOut, tdMock, axLetter, '', 'paper'); + var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); // x, y - Axes.coercePosition(annOut, tdMock, coerce, axRef, axLetter, 0.5); + Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); if(showArrow) { var arrowPosAttr = 'a' + axLetter, // axref, ayref - aaxRef = Axes.coerceRef(annIn, annOut, tdMock, arrowPosAttr, 'pixel'); + aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel'); // for now the arrow can only be on the same axis or specified as pixels // TODO: sometime it might be interesting to allow it to be on *any* axis @@ -66,7 +66,7 @@ module.exports = function handleAnnotationDefaults(annIn, fullLayout) { // ax, ay var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4; - Axes.coercePosition(annOut, tdMock, coerce, aaxRef, arrowPosAttr, aDflt); + Axes.coercePosition(annOut, gdMock, coerce, aaxRef, arrowPosAttr, aDflt); } // xanchor, yanchor diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index b4f1c5581e5..b0491af52b8 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -75,14 +75,14 @@ drawing.getPx = function(s, styleAttr) { return Number(s.style(styleAttr).replace(/px$/, '')); }; -drawing.crispRound = function(td, lineWidth, dflt) { +drawing.crispRound = function(gd, lineWidth, dflt) { // for lines that disable antialiasing we want to // make sure the width is an integer, and at least 1 if it's nonzero if(!lineWidth || !isNumeric(lineWidth)) return dflt || 0; // but not for static plots - these don't get antialiased anyway. - if(td._context.staticPlot) return lineWidth; + if(gd._context.staticPlot) return lineWidth; if(lineWidth < 1) return 1; return Math.round(lineWidth); diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js index 1be89b4f05a..bbe21337a0a 100644 --- a/src/components/images/defaults.js +++ b/src/components/images/defaults.js @@ -53,11 +53,11 @@ function imageDefaults(imageIn, imageOut, fullLayout) { coerce('opacity'); for(var i = 0; i < 2; i++) { - var tdMock = { _fullLayout: fullLayout }, + var gdMock = { _fullLayout: fullLayout }, axLetter = ['x', 'y'][i]; // 'paper' is the fallback axref - Axes.coerceRef(imageIn, imageOut, tdMock, axLetter, 'paper'); + Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper'); } return imageOut; diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js index 47bfbea7fff..189507f86b9 100644 --- a/src/components/shapes/shape_defaults.js +++ b/src/components/shapes/shape_defaults.js @@ -36,10 +36,10 @@ module.exports = function handleShapeDefaults(shapeIn, fullLayout) { var axLetters = ['x', 'y']; for(var i = 0; i < 2; i++) { var axLetter = axLetters[i], - tdMock = {_fullLayout: fullLayout}; + gdMock = {_fullLayout: fullLayout}; // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, tdMock, axLetter, '', 'paper'); + var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper'); if(shapeType !== 'path') { var dflt0 = 0.25, @@ -49,7 +49,7 @@ module.exports = function handleShapeDefaults(shapeIn, fullLayout) { r2pos; if(axRef !== 'paper') { - ax = Axes.getFromId(tdMock, axRef); + ax = Axes.getFromId(gdMock, axRef); r2pos = helpers.rangeToShapePosition(ax); pos2r = helpers.shapePositionToRange(ax); @@ -72,8 +72,8 @@ module.exports = function handleShapeDefaults(shapeIn, fullLayout) { shapeIn[attr1] = pos2r(shapeIn[attr1], true); // x0, x1 (and y0, y1) - Axes.coercePosition(shapeOut, tdMock, coerce, axRef, attr0, dflt0); - Axes.coercePosition(shapeOut, tdMock, coerce, axRef, attr1, dflt1); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); // hack part 2 shapeOut[attr0] = r2pos(shapeOut[attr0]); diff --git a/src/lib/index.js b/src/lib/index.js index adca268a068..fd37fea2199 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -257,7 +257,7 @@ lib.smooth = function(arrayIn, FWHM) { * as long as its returns are not promises (ie have no .then) * includes one argument arg to send to all functions... * this is mainly just to prevent us having to make wrapper functions - * when the only purpose of the wrapper is to reference gd / td + * when the only purpose of the wrapper is to reference gd * and a final step to be executed at the end * TODO: if there's an error and everything is sync, * this doesn't happen yet because we want to make sure diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 28b152085a8..2d229357f25 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -111,8 +111,8 @@ exports.convertToTspans = function(_context, _callback) { } if(tex) { - var td = Lib.getPlotDiv(that.node()); - ((td && td._promises) || []).push(new Promise(function(resolve) { + var gd = Lib.getPlotDiv(that.node()); + ((gd && gd._promises) || []).push(new Promise(function(resolve) { that.style({visibility: 'hidden'}); var config = {fontSize: parseInt(that.style('font-size'), 10)}; diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 2f910af0b00..f86bb929395 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -52,7 +52,7 @@ function toImage(gd, opts) { // first clone the GD so we can operate in a clean environment var clone = clonePlot(gd, {format: 'png', height: opts.height, width: opts.width}); - var clonedGd = clone.td; + var clonedGd = clone.gd; // put the cloned div somewhere off screen before attaching to DOM clonedGd.style.position = 'absolute'; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 97c166dc6be..9cd9c034e8b 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -89,7 +89,7 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption * - for date axes: JS Dates or milliseconds, and convert to date strings * - for other types: coerce them to numbers */ -axes.coercePosition = function(containerOut, td, coerce, axRef, attr, dflt) { +axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { var pos, newPos; @@ -97,7 +97,7 @@ axes.coercePosition = function(containerOut, td, coerce, axRef, attr, dflt) { pos = coerce(attr, dflt); } else { - var ax = axes.getFromId(td, axRef); + var ax = axes.getFromId(gd, axRef); dflt = ax.fraction2r(dflt); pos = coerce(attr, dflt); diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js index 656ff1e0003..00139887d28 100644 --- a/src/plots/cartesian/axis_autotype.js +++ b/src/plots/cartesian/axis_autotype.js @@ -54,7 +54,7 @@ function moreDates(a) { return (dcnt > ncnt * 2); } -// are the (x,y)-values in td.data mostly text? +// are the (x,y)-values in gd.data mostly text? // require twice as many categories as numbers function category(a) { // test at most 1000 points diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index ed0201c6d92..9dda505b52e 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -40,8 +40,8 @@ module.exports = { type: { valType: 'enumerated', // '-' means we haven't yet run autotype or couldn't find any data - // it gets turned into linear in td._fullLayout but not copied back - // to td.data like the others are. + // it gets turned into linear in gd._fullLayout but not copied back + // to gd.data like the others are. values: ['-', 'linear', 'log', 'date', 'category'], dflt: '-', role: 'info', diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 4ff5ef380ad..e61b2623e0b 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -315,7 +315,7 @@ module.exports = function setConvert(ax) { // makeCalcdata: takes an x or y array and converts it // to a position on the axis object "ax" // inputs: - // trace - a data object from td.data + // trace - a data object from gd.data // axLetter - a string, either 'x' or 'y', for which item // to convert (TODO: is this now always the same as // the first letter of ax._id?) diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js index d8f076b0d7f..25a389306df 100644 --- a/src/snapshot/cloneplot.js +++ b/src/snapshot/cloneplot.js @@ -124,11 +124,12 @@ module.exports = function clonePlot(graphObj, options) { } } - var td = document.createElement('div'); - if(options.tileClass) td.className = options.tileClass; + var gd = document.createElement('div'); + if(options.tileClass) gd.className = options.tileClass; var plotTile = { - td: td, + gd: gd, + td: gd, // for external (image server) compatibility layout: newLayout, data: newData, config: { @@ -148,8 +149,8 @@ module.exports = function clonePlot(graphObj, options) { plotTile.config.setBackground = options.setBackground || 'opaque'; } - // attaching the default Layout the td, so you can grab it later - plotTile.td.defaultLayout = cloneLayoutOverride(options.tileClass); + // attaching the default Layout the gd, so you can grab it later + plotTile.gd.defaultLayout = cloneLayoutOverride(options.tileClass); return plotTile; }; diff --git a/src/snapshot/toimage.js b/src/snapshot/toimage.js index 9fc8eb75b5a..76386f4ac83 100644 --- a/src/snapshot/toimage.js +++ b/src/snapshot/toimage.js @@ -30,7 +30,7 @@ function toImage(gd, opts) { var ev = new EventEmitter(); var clone = clonePlot(gd, {format: 'png'}); - var clonedGd = clone.td; + var clonedGd = clone.gd; // put the cloned div somewhere off screen before attaching to DOM clonedGd.style.position = 'absolute'; diff --git a/test/jasmine/tests/snapshot_test.js b/test/jasmine/tests/snapshot_test.js index d2050628f43..579a3ae58eb 100644 --- a/test/jasmine/tests/snapshot_test.js +++ b/test/jasmine/tests/snapshot_test.js @@ -86,7 +86,8 @@ describe('Plotly.Snapshot', function() { var themeTile = Plotly.Snapshot.clone(dummyGraphObj, themeOptions); expect(themeTile.layout.height).toEqual(THEMETILE_DEFAULT_LAYOUT.height); expect(themeTile.layout.width).toEqual(THEMETILE_DEFAULT_LAYOUT.width); - expect(themeTile.td.defaultLayout).toEqual(THEMETILE_DEFAULT_LAYOUT); + expect(themeTile.gd.defaultLayout).toEqual(THEMETILE_DEFAULT_LAYOUT); + expect(themeTile.gd).toBe(themeTile.td); // image server compatibility expect(themeTile.config).toEqual(config); }); From e63ea3b44babf8c9188c5ea3ed74d1ea4d273f34 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 25 Oct 2016 23:07:05 -0400 Subject: [PATCH 10/23] test and fix shape default positioning --- src/components/shapes/shape_defaults.js | 3 -- test/jasmine/tests/shapes_test.js | 42 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js index 189507f86b9..8494e62275c 100644 --- a/src/components/shapes/shape_defaults.js +++ b/src/components/shapes/shape_defaults.js @@ -52,9 +52,6 @@ module.exports = function handleShapeDefaults(shapeIn, fullLayout) { ax = Axes.getFromId(gdMock, axRef); r2pos = helpers.rangeToShapePosition(ax); pos2r = helpers.shapePositionToRange(ax); - - dflt0 = r2pos(ax.fraction2r(dflt0)); - dflt1 = r2pos(ax.fraction2r(dflt1)); } else { pos2r = r2pos = Lib.identity; diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index b2058b03bc4..b616d05c260 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -1,14 +1,56 @@ var helpers = require('@src/components/shapes/helpers'); var constants = require('@src/components/shapes/constants'); +var handleShapeDefaults = require('@src/components/shapes/shape_defaults'); var Plotly = require('@lib/index'); var PlotlyInternal = require('@src/plotly'); var Lib = require('@src/lib'); var Axes = PlotlyInternal.Axes; +var Plots = require('@src/plots/plots'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var customMatchers = require('../assets/custom_matchers'); + + +describe('shape supplyDefaults', function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + it('should provide the right defaults on all axis types', function() { + var fullLayout = { + xaxis: {type: 'linear', range: [0, 20]}, + yaxis: {type: 'log', range: [1, 5]}, + xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']}, + yaxis2: {type: 'category', range: [-0.5, 7.5]} + }; + fullLayout._has = Plots._hasPlotType.bind(fullLayout); + Axes.setConvert(fullLayout.xaxis); + Axes.setConvert(fullLayout.yaxis); + Axes.setConvert(fullLayout.xaxis2); + Axes.setConvert(fullLayout.yaxis2); + + var shape1In = {type: 'rect'}, + shape1Out = handleShapeDefaults(shape1In, fullLayout), + shape2In = {type: 'circle', xref: 'x2', yref: 'y2'}, + shape2Out = handleShapeDefaults(shape2In, fullLayout); + + // default positions are 1/4 and 3/4 of the full range of that axis + expect(shape1Out.x0).toBe(5); + expect(shape1Out.x1).toBe(15); + // shapes use data values for log axes (like everyone will in V2.0) + expect(shape1Out.y0).toBeWithin(100, 0.001); + expect(shape1Out.y1).toBeWithin(10000, 0.001); + // date strings also interpolate + expect(shape2Out.x0).toBe('2006-06-06'); + expect(shape2Out.x1).toBe('2006-06-08'); + // categories must use serial numbers to get continuous values + expect(shape2Out.y0).toBeWithin(1.5, 0.001); + expect(shape2Out.y1).toBeWithin(5.5, 0.001); + }); +}); describe('Test shapes:', function() { From 2e9dbada96cb0cc879e67c7d8143313306e45f44 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 28 Oct 2016 11:47:22 -0400 Subject: [PATCH 11/23] fix date and log axis manual tick0/dtick handling --- src/plots/cartesian/layout_attributes.js | 21 ++++-- src/plots/cartesian/tick_value_defaults.js | 39 +++++++++-- test/jasmine/tests/axes_test.js | 79 ++++++++++++++++++++++ 3 files changed, 129 insertions(+), 10 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 9dda505b52e..60573e5df55 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -137,14 +137,15 @@ module.exports = { ].join(' ') }, tick0: { - valType: 'number', + valType: 'any', dflt: 0, role: 'style', description: [ 'Sets the placement of the first tick on this axis.', 'Use with `dtick`.', 'If the axis `type` is *log*, then you must take the log of your starting tick', - '(e.g. to set the starting tick to 100, set the `tick0` to 2).', + '(e.g. to set the starting tick to 100, set the `tick0` to 2)', + 'except when `dtick`=*L* (see `dtick` for more info).', 'If the axis `type` is *date*, it should be a date string, like date data.', 'If the axis `type` is *category*, it should be a number, using the scale where', 'each category is assigned a serial number from zero in the order it appears.' @@ -152,19 +153,27 @@ module.exports = { }, dtick: { valType: 'any', - dflt: 1, role: 'style', description: [ - 'Sets the step in-between ticks on this axis', - 'Use with `tick0`.', + 'Sets the step in-between ticks on this axis. Use with `tick0`.', + 'Must be a positive number, or special strings available to *log* and *date* axes.', 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', 'is the tick number. For example,', 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', + '*log* has several special values; *L*, where `f` is a positive number,', + 'gives ticks linearly spaced in value (but not position).', + 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', + 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', + '`tick0` is ignored for *D1* and *D2*.', 'If the axis `type` is *date*, then you must convert the time to milliseconds.', 'For example, to set the interval between ticks to one day,', - 'set `dtick` to 86400000.0.' + 'set `dtick` to 86400000.0.', + '*date* also has special values *M* gives ticks spaced by a number of months.', + '`n` must be a positive integer.', + 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', + 'To set ticks every 4 years, set `dtick` to *M48*' ].join(' ') }, tickvals: { diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 504667a6efd..694c3c46b19 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -22,22 +22,53 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe } if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array'; - else if(containerIn.dtick && isNumeric(containerIn.dtick)) { + else if(containerIn.dtick) { tickmodeDefault = 'linear'; } var tickmode = coerce('tickmode', tickmodeDefault); if(tickmode === 'auto') coerce('nticks'); else if(tickmode === 'linear') { - var tick0 = coerce('tick0'); - coerce('dtick'); + // dtick is usually a positive number, but there are some + // special strings available for log or date axes + // default is 1 day for dates, otherwise 1 + var dtickDflt = (axType === 'date') ? 86400000 : 1; + var dtick = coerce('dtick', dtickDflt); + if(isNumeric(dtick)) { + containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt; + } + else if(typeof dtick !== 'string') { + containerOut.dtick = dtickDflt; + } + else { + // date and log special cases are all one character plus a number + var prefix = dtick.charAt(0), + dtickNum = dtick.substr(1); + + dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0; + if((dtickNum <= 0) || !( + // "M" gives ticks every (integer) n months + (axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) || + // "L" gives ticks linearly spaced in data (not in position) every (float) f + (axType === 'log' && prefix === 'L') || + // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5 + (axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2)) + )) { + containerOut.dtick = dtickDflt; + } + } // tick0 can have different valType for different axis types, so // validate that now. Also for dates, change milliseconds to date strings + var tick0 = coerce('tick0'); if(axType === 'date') { containerOut.tick0 = Lib.cleanDate(tick0, '2000-01-01'); } - else if(!isNumeric(tick0)) { + // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely + else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') { + containerOut.tick0 = Number(tick0); + } + else { containerOut.tick0 = 0; } } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 619a22098cf..9fec5534df8 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -676,6 +676,85 @@ describe('Test axes', function() { expect(axOut.dtick).toBe(0.00159); }); + it('should handle tick0 and dtick for date axes', function() { + var someMs = 123456789, + someMsDate = Lib.ms2DateTime(someMs), + oneDay = 24 * 3600 * 1000, + axIn = {tick0: someMs, dtick: String(3 * oneDay)}, + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'date'); + expect(axOut.tick0).toBe(someMsDate); + expect(axOut.dtick).toBe(3 * oneDay); + + var someDate = '2011-12-15 13:45:56'; + axIn = {tick0: someDate, dtick: 'M15'}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'date'); + expect(axOut.tick0).toBe(someDate); + expect(axOut.dtick).toBe('M15'); + + // now some stuff that shouldn't work, should give defaults + [ + ['next thursday', -1], + ['123-45', 'L1'], + ['', 'M0.5'], + ['', 'M-1'], + ['', '2000-01-01'] + ].forEach(function(v) { + axIn = {tick0: v[0], dtick: v[1]}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'date'); + expect(axOut.tick0).toBe('2000-01-01'); + expect(axOut.dtick).toBe(oneDay); + }); + }); + + it('should handle tick0 and dtick for log axes', function() { + var axIn = {tick0: '0.2', dtick: 0.3}, + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'log'); + expect(axOut.tick0).toBe(0.2); + expect(axOut.dtick).toBe(0.3); + + ['D1', 'D2'].forEach(function(v) { + axIn = {tick0: -1, dtick: v}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'log'); + // tick0 gets ignored for D + expect(axOut.tick0).toBe(0); + expect(axOut.dtick).toBe(v); + }); + + [ + [-1, 'L3'], + ['0.2', 'L0.3'], + [-1, 3], + ['0.1234', '0.69238473'] + ].forEach(function(v) { + axIn = {tick0: v[0], dtick: v[1]}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'log'); + expect(axOut.tick0).toBe(Number(v[0])); + expect(axOut.dtick).toBe((+v[1]) ? Number(v[1]) : v[1]); + }); + + // now some stuff that should not work, should give defaults + [ + ['', -1], + ['D1', 'D3'], + ['', 'D0'], + ['2011-01-01', 'L0'], + ['', 'L-1'] + ].forEach(function(v) { + axIn = {tick0: v[0], dtick: v[1]}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'log'); + expect(axOut.tick0).toBe(0); + expect(axOut.dtick).toBe(1); + }); + + }); + it('should set tickvals and ticktext iff tickmode=array', function() { var axIn = {tickmode: 'auto', tickvals: [1, 2, 3], ticktext: ['4', '5', '6']}, axOut = {}; From 37d52fe80f18bdf4fee370018694a3eb467d9577 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 28 Oct 2016 15:14:37 -0400 Subject: [PATCH 12/23] fix handling of default tick0 with dtick for date axes --- src/plots/cartesian/layout_attributes.js | 1 - src/plots/cartesian/tick_value_defaults.js | 7 ++++--- test/jasmine/tests/axes_test.js | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 60573e5df55..ebc60e0b050 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -138,7 +138,6 @@ module.exports = { }, tick0: { valType: 'any', - dflt: 0, role: 'style', description: [ 'Sets the placement of the first tick on this axis.', diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 694c3c46b19..cf08ed64fb5 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -60,16 +60,17 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe // tick0 can have different valType for different axis types, so // validate that now. Also for dates, change milliseconds to date strings - var tick0 = coerce('tick0'); + var tick0Dflt = (axType === 'date') ? '2000-01-01' : 0; + var tick0 = coerce('tick0', tick0Dflt); if(axType === 'date') { - containerOut.tick0 = Lib.cleanDate(tick0, '2000-01-01'); + containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt); } // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') { containerOut.tick0 = Number(tick0); } else { - containerOut.tick0 = 0; + containerOut.tick0 = tick0Dflt; } } else { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 9fec5534df8..b17b4d72ed6 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -693,6 +693,13 @@ describe('Test axes', function() { expect(axOut.tick0).toBe(someDate); expect(axOut.dtick).toBe('M15'); + // dtick without tick0: get the right default + axIn = {dtick: 'M12'}; + axOut = {}; + mockSupplyDefaults(axIn, axOut, 'date'); + expect(axOut.tick0).toBe('2000-01-01'); + expect(axOut.dtick).toBe('M12'); + // now some stuff that shouldn't work, should give defaults [ ['next thursday', -1], From 55b313cc2c300010b2b937e8898934f6536828eb Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 28 Oct 2016 21:22:10 -0400 Subject: [PATCH 13/23] get rid of '2012-01-22 12h' tick format and smarter date suffix handling --- src/plots/cartesian/axes.js | 104 +++++++++++++++++++++----------- test/jasmine/tests/axes_test.js | 84 ++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 36 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 9cd9c034e8b..0ec37c395a6 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -658,9 +658,18 @@ axes.calcTicks = function calcTicks(ax) { // show the exponent only on the last one ax._tmax = vals[vals.length - 1]; + // for showing date suffixes: ax._prevSuffix holds what we showed most + // recently. Start with it cleared and mark that we're in calcTicks (ie + // calculating a whole string of these so we should care what the previous + // suffix was!) + ax._prevSuffix = ''; + ax._inCalcTicks = true; + var ticksOut = new Array(vals.length); for(var i = 0; i < vals.length; i++) ticksOut[i] = axes.tickText(ax, vals[i]); + ax._inCalcTicks = false; + return ticksOut; }; @@ -815,6 +824,11 @@ axes.autoTicks = function(ax, roughDTick) { } }; +var ONEDAY = 86400000, + ONEHOUR = 3600000, + ONEMIN = 60000, + ONESEC = 1000; + // after dtick is already known, find tickround = precision // to display in tick labels // for numeric ticks, integer # digits after . to round to @@ -831,33 +845,43 @@ function autoTickRound(ax) { if(ax.type === 'category') { ax._tickround = null; } + if(ax.type === 'date') { + // If tick0 is unusual, give tickround a bit more information + // not necessarily *all* the information in tick0 though, if it's really odd + // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 + // take off a leading minus (year < 0 so length is consistent) + var tick0str = Lib.ms2DateTime(Lib.dateTime2ms(ax.tick0)).replace(/^-/, ''), + tick0len = tick0str.length; + + if(String(dtick).charAt(0) === 'M') { + // any tick0 more specific than a year: alway show the full date + if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd'; + // show the month unless ticks are full multiples of a year + else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm'; + } + else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd'; + else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M'; + else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S'; + else ax._tickround = Math.max(3 - Math.round(Math.log(dtick / 2) / Math.LN10), tick0len - 20); + } else if(isNumeric(dtick) || dtick.charAt(0) === 'L') { - if(ax.type === 'date') { - if(dtick >= 86400000) ax._tickround = 'd'; - else if(dtick >= 3600000) ax._tickround = 'H'; - else if(dtick >= 60000) ax._tickround = 'M'; - else if(dtick >= 1000) ax._tickround = 'S'; - else ax._tickround = 3 - Math.round(Math.log(dtick / 2) / Math.LN10); - } - else { - // linear or log - var rng = ax.range.map(ax.r2d || Number); - if(!isNumeric(dtick)) dtick = Number(dtick.substr(1)); - // 2 digits past largest digit of dtick - ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); - - var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); - - var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); - if(Math.abs(rangeexp) > 3) { - if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') { - ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); - } - else ax._tickexponent = rangeexp; + // linear or log (except D1, D2) + var rng = ax.range.map(ax.r2d || Number); + if(!isNumeric(dtick)) dtick = Number(dtick.substr(1)); + // 2 digits past largest digit of dtick + ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); + + var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); + + var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); + if(Math.abs(rangeexp) > 3) { + if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') { + ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); } + else ax._tickexponent = rangeexp; } } - else if(dtick.charAt(0) === 'M') ax._tickround = (dtick.length === 2) ? 'm' : 'y'; + // D1 or D2 (log) else ax._tickround = null; } @@ -961,7 +985,7 @@ axes.tickFirst = function(ax) { var yearFormat = d3.time.format('%Y'), monthFormat = d3.time.format('%b %Y'), dayFormat = d3.time.format('%b %-d'), - hourFormat = d3.time.format('%b %-d %Hh'), + yearMonthDayFormat = d3.time.format('%b %-d, %Y'), minuteFormat = d3.time.format('%H:%M'), secondFormat = d3.time.format(':%S'); @@ -1053,10 +1077,12 @@ function tickTextObj(ax, x, text) { function formatDate(ax, out, hover, extraPrecision) { var x = out.x, tr = ax._tickround, + trOriginal = tr, d = new Date(x), // suffix completes the full date info, to be included - // with only the first tick - suffix = '', + // with only the first tick or if any info before what's + // shown has changed + suffix, tt; if(hover && ax.hoverformat) { tt = modDateFormat(ax.hoverformat, x); @@ -1069,21 +1095,18 @@ function formatDate(ax, out, hover, extraPrecision) { else { if(extraPrecision) { if(isNumeric(tr)) tr += 2; - else tr = {y: 'm', m: 'd', d: 'H', H: 'M', M: 'S', S: 2}[tr]; + else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 2}[tr]; } if(tr === 'y') tt = yearFormat(d); else if(tr === 'm') tt = monthFormat(d); else { - if(x === ax._tmin && !hover) { - suffix = '
' + yearFormat(d); - } + if(tr === 'd') { + if(!hover) suffix = '
' + yearFormat(d); - if(tr === 'd') tt = dayFormat(d); - else if(tr === 'H') tt = hourFormat(d); + tt = dayFormat(d); + } else { - if(x === ax._tmin && !hover) { - suffix = '
' + dayFormat(d) + ', ' + yearFormat(d); - } + if(!hover) suffix = '
' + yearMonthDayFormat(d); tt = minuteFormat(d); if(tr !== 'M') { @@ -1093,10 +1116,19 @@ function formatDate(ax, out, hover, extraPrecision) { .substr(1); } } + else if(trOriginal === 'd') { + // for hover on axes with day ticks, minuteFormat (which + // only includes %H:%M) isn't enough, you want the date too + tt = dayFormat(d) + ' ' + tt; + } } } } - out.text = tt + suffix; + if(suffix && (!ax._inCalcTicks || (suffix !== ax._prevSuffix))) { + tt += suffix; + ax._prevSuffix = suffix; + } + out.text = tt; } function formatLog(ax, out, hover, extraPrecision, hideexp) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index b17b4d72ed6..a9d65a7036c 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1392,4 +1392,88 @@ describe('Test axes', function() { expect(ax._max).toEqual([{val: 6, pad: 15}]); }); }); + + describe('calcTicks', function() { + function mockCalc(ax) { + Axes.setConvert(ax); + ax.tickfont = {}; + return Axes.calcTicks(ax).map(function(v) { return v.text; }); + } + + it('provides a new date suffix whenever the suffix changes', function() { + var textOut = mockCalc({ + type: 'date', + tickmode: 'linear', + tick0: '2000-01-01', + dtick: 14 * 24 * 3600 * 1000, // 14 days + range: ['1999-12-01', '2000-02-15'] + }); + + var expectedText = [ + 'Dec 4
1999', + 'Dec 18', + 'Jan 1
2000', + 'Jan 15', + 'Jan 29', + 'Feb 12' + ]; + expect(textOut).toEqual(expectedText); + + textOut = mockCalc({ + type: 'date', + tickmode: 'linear', + tick0: '2000-01-01', + dtick: 12 * 3600 * 1000, // 12 hours + range: ['2000-01-03 11:00', '2000-01-06'] + }); + + expectedText = [ + '12:00
Jan 3, 2000', + '00:00
Jan 4, 2000', + '12:00', + '00:00
Jan 5, 2000', + '12:00', + '00:00
Jan 6, 2000' + ]; + expect(textOut).toEqual(expectedText); + + textOut = mockCalc({ + type: 'date', + tickmode: 'linear', + tick0: '2000-01-01', + dtick: 1000, // 1 sec + range: ['2000-02-03 23:59:57', '2000-02-04 00:00:02'] + }); + + expectedText = [ + '23:59:57
Feb 3, 2000', + '23:59:58', + '23:59:59', + '00:00:00
Feb 4, 2000', + '00:00:01', + '00:00:02' + ]; + expect(textOut).toEqual(expectedText); + }); + + it('should give dates extra precision if tick0 is weird', function() { + var textOut = mockCalc({ + type: 'date', + tickmode: 'linear', + tick0: '2000-01-01 00:05', + dtick: 14 * 24 * 3600 * 1000, // 14 days + range: ['1999-12-01', '2000-02-15'] + }); + + var expectedText = [ + '00:05
Dec 4, 1999', + '00:05
Dec 18, 1999', + '00:05
Jan 1, 2000', + '00:05
Jan 15, 2000', + '00:05
Jan 29, 2000', + '00:05
Feb 12, 2000' + ]; + expect(textOut).toEqual(expectedText); + }); + }); }); From b7121c3b3ecececba81c15ede2bf9bf429bd377d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 28 Oct 2016 23:17:14 -0400 Subject: [PATCH 14/23] fix #1058 - make sure no date ever returns sub-100microsec precision --- src/plots/cartesian/axes.js | 12 +++++++++--- test/jasmine/tests/axes_test.js | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 0ec37c395a6..fdf1927f668 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -850,7 +850,8 @@ function autoTickRound(ax) { // not necessarily *all* the information in tick0 though, if it's really odd // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 // take off a leading minus (year < 0 so length is consistent) - var tick0str = Lib.ms2DateTime(Lib.dateTime2ms(ax.tick0)).replace(/^-/, ''), + var tick0ms = Lib.dateTime2ms(ax.tick0), + tick0str = Lib.ms2DateTime(tick0ms).replace(/^-/, ''), tick0len = tick0str.length; if(String(dtick).charAt(0) === 'M') { @@ -862,7 +863,12 @@ function autoTickRound(ax) { else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd'; else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M'; else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S'; - else ax._tickround = Math.max(3 - Math.round(Math.log(dtick / 2) / Math.LN10), tick0len - 20); + else { + // of any two adjacent ticks, at least one will have the maximum fractional digits + // of all possible ticks - so take the max. length of tick0 and the next one + var tick1len = Lib.ms2DateTime(tick0ms + dtick).replace(/^-/, '').length; + ax._tickround = Math.max(tick0len, tick1len) - 20; + } } else if(isNumeric(dtick) || dtick.charAt(0) === 'L') { // linear or log (except D1, D2) @@ -1112,7 +1118,7 @@ function formatDate(ax, out, hover, extraPrecision) { if(tr !== 'M') { tt += secondFormat(d); if(tr !== 'S') { - tt += numFormat(mod(x / 1000, 1), ax, 'none', hover) + tt += numFormat(d3.round(mod(x / 1000, 1), 4), ax, 'none', hover) .substr(1); } } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index a9d65a7036c..90433bd53af 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1397,6 +1397,7 @@ describe('Test axes', function() { function mockCalc(ax) { Axes.setConvert(ax); ax.tickfont = {}; + ax._gd = {_fullLayout: {separators: '.,'}}; return Axes.calcTicks(ax).map(function(v) { return v.text; }); } @@ -1475,5 +1476,28 @@ describe('Test axes', function() { ]; expect(textOut).toEqual(expectedText); }); + + it('should never give dates more than 100 microsecond precision', function() { + var textOut = mockCalc({ + type: 'date', + tickmode: 'linear', + tick0: '2000-01-01', + dtick: 1.1333, + range: ['2000-01-01', '2000-01-01 00:00:00.01'] + }); + + var expectedText = [ + '00:00:00
Jan 1, 2000', + '00:00:00.0011', + '00:00:00.0023', + '00:00:00.0034', + '00:00:00.0045', + '00:00:00.0057', + '00:00:00.0068', + '00:00:00.0079', + '00:00:00.0091' + ]; + expect(textOut).toEqual(expectedText); + }); }); }); From dd0940ab7482185b6c160f76b3f27146fc758a42 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 29 Oct 2016 00:09:17 -0400 Subject: [PATCH 15/23] fix _forceTick0 error --- src/plots/cartesian/axes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index fdf1927f668..6b8184ef275 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -142,6 +142,8 @@ axes.counterLetter = function(id) { // incorporate a new minimum difference and first tick into // forced +// note that _forceTick0 is linearized, so needs to be turned into +// a range value for setting tick0 axes.minDtick = function(ax, newDiff, newFirst, allow) { // doesn't make sense to do forced min dTick on log or category axes, // and the plot itself may decide to cancel (ie non-grouped bars) @@ -619,7 +621,7 @@ axes.calcTicks = function calcTicks(ax) { // check for a forced minimum dtick if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { ax.dtick = ax._minDtick; - ax.tick0 = ax._forceTick0; + ax.tick0 = ax.l2r(ax._forceTick0); } } From f629f7137f29ecdc4dcfda09d58adde3bfa58c44 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 29 Oct 2016 00:13:34 -0400 Subject: [PATCH 16/23] update baseline with new date tick format --- test/image/baselines/29.png | Bin 54628 -> 54365 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/image/baselines/29.png b/test/image/baselines/29.png index a5bed7f5221fc76183aa3efeed10e5056b86d5e5..0faec1aead8ecab222f3005ebd936e69ab00d947 100644 GIT binary patch delta 26349 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%0A1E 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 z z2H9Xr zmw|Xsfs`{{<$R695z`8@f6N#QtiJOWuy6k8YwQbvQn>t3FDww|{mQ=Z4j_;^$_HE& z0d=Fiu@%kCmInxcgCGSCn@qLngT|kR|2z@~I<^V07l99KDEIqPmijh)-Rjb%OL#{@ z)4o539_VFn!6DPJJ@ zkH=OKh1^ic3q3jPs*U2A7%+s$>1`^z#&tNBVcU) zR^y2$M@bOZ0myU@?8sk$I~Gso>IWr|z#T>7z-DS?zW%EO9gRkl33LO%K`La1w!{m} zWr=}i_kUamR*9ipV(FMCAhppjUE~75@uvWsbmxg~Xh#%`IqzuNzQFlGbx`@ZFa@}n zgTZNCpM;@X;I%>p_(3iy1Lx_khqB>o^LFaB48*--x(Doqnnly_AMXZcvH^@fy_E3J z3;$53m?8$8=Cd4l?O%)k^UrdW3RcTpZbb!HYm8Z3X(JIDmUzE1cC!=(j0TKDRa3%l8 dYfLA0VR>`SapMW1Y)s(isF{svk+CcOzW|5-ZeIWZ delta 26577 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 Date: Tue, 8 Nov 2016 22:01:39 -0500 Subject: [PATCH 17/23] fix images on date axes, and standardize axis object names --- src/components/images/draw.js | 16 ++++++++-------- test/image/baselines/layout_image.png | Bin 66781 -> 67152 bytes test/image/mocks/layout_image.json | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/components/images/draw.js b/src/components/images/draw.js index a39a534adc2..28e668e2175 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -101,12 +101,12 @@ module.exports = function draw(gd) { var thisImage = d3.select(this); // Axes if specified - var xref = Axes.getFromId(gd, d.xref), - yref = Axes.getFromId(gd, d.yref); + var xa = Axes.getFromId(gd, d.xref), + ya = Axes.getFromId(gd, d.yref); var size = fullLayout._size, - width = xref ? Math.abs(xref.l2p(d.sizex) - xref.l2p(0)) : d.sizex * size.w, - height = yref ? Math.abs(yref.l2p(d.sizey) - yref.l2p(0)) : d.sizey * size.h; + width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w, + height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h; // Offsets for anchor positioning var xOffset = width * anchors.x[d.xanchor].offset, @@ -115,8 +115,8 @@ module.exports = function draw(gd) { var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; // Final positions - var xPos = (xref ? xref.l2p(d.x) + xref._offset : d.x * size.w + size.l) + xOffset, - yPos = (yref ? yref.l2p(d.y) + yref._offset : size.h - d.y * size.h + size.t) + yOffset; + var xPos = (xa ? xa.l2p(xa.r2l(d.x)) + xa._offset : d.x * size.w + size.l) + xOffset, + yPos = (ya ? ya.l2p(ya.r2l(d.y)) + ya._offset : size.h - d.y * size.h + size.t) + yOffset; // Construct the proper aspectRatio attribute @@ -141,8 +141,8 @@ module.exports = function draw(gd) { // Set proper clipping on images - var xId = xref ? xref._id : '', - yId = yref ? yref._id : '', + var xId = xa ? xa._id : '', + yId = ya ? ya._id : '', clipAxes = xId + yId; if(clipAxes) { diff --git a/test/image/baselines/layout_image.png b/test/image/baselines/layout_image.png index 8399bc3bafe907f7b87598077ef8eef569165836..def47ebab0fcaeadcaeee22ca3d9ec05a1a15506 100644 GIT binary patch literal 67152 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 KQz&QZ^Zx+2kH2*Q literal 66781 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%0AOa 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
8S3e0{C17@o|)Cn!g zxOhfQ2iMm}n>V&JpV47;$7V^V$!bpUHZYOMpw9)lt#MS>vBuJIs?u>Vp9IP9J;hY9 zk{OXlHyz&_=(N$&;sQjJm|%<$6kOPQ+w|heRvngW;!7#ekZaq9-fOw{YVO8DgTM%?{7c zFh~8o3tv3wE0yHG!hF?BzNJgGJoJxnlnm)|M74kvtzaCM%OGmcTCq%Q+A7}iQ5KHC zBDOhk!B%pG!$Z*&-RM>sQyXQ{v|i|!8g}WmCKB5_M>yP`>k`Wjvz}2v2_akB$&>v{ znqdPDCAw~_o}}`_tH7%>L2d zhyXhB=2XYZitS}uF9oKv{`O%8y-1Ok)TstoqdiCc7QCQlWYxx*4b+TS95l9ADqTwT zs_dYc9Z{Gp&mRt#lJfA`GL{^hitg>YYf-?AYz0a;w-Mp#nr@368MEl^m``Z32QnFZ zeu$tT7*<>!LrcolmOdfW2xp6;y4Hh~gKi5(rDJ?PFYMKE$inCTI$i?q*KOtvvd8=R zW!=^pb+BrNQLf&8;M@I&Q$Dg8k{K63-xuj>)uixfdMimE=&{{tj#KKo6ru{uIpGV% zg_^Hk0BY&4XC0V^{-MiQ=5n^aaY*ic7VtRUHX~l!mNC;^gb`pv%Ci2e%$W3Jo;D$J zK?GWDGnA4AVmcE{GCsJ4kxS%QN^foiW|4+Du7(>RE9d$-@QWIS-aID&^$3H0 zm(jG!jHAZo9$EKfu+1eG_Pb%^8!aFoc)a!NXv^VB{P8W7P12Gzw%5`n+=vx{vt2xm zf@C$ZoR%)2n-QMwMH^tQsIEytl}~J%HI2M z{#QEV9uAh$*yugXkX$(}EkxLP-0hET>`#()pLSoS8BI2KwhhNZ*$6`5^}1oD^#^q2 zq07@a4PZzp%NW^?TE<35krMCsJ`Kvbt}oXXgpE+XwlgY{z3|?BA(i(6XN}6eP0{!b zArmNg(y4s`tn};_TA;u8aMop|$3u4p4y(q)j1L_J8m=LVmwYtg)WjtR2vVwg4-Z}} zCGo_z#cz~eG4!n?yp2IKzh`3OyYA-N*c8~rmPD#Rvc7j#TQ64CXW8xql$Vo{e>sua zmg!xo#%2~r3-;gGTT%&NRSD=~MVEiUMmj;)q2mBOCcdwDsox-r=U7S|LQpSMh(bJA zdDdZ_abeCq=|j-=;LFvSC*XN?>z-3w(#0BAVT%|g%E3rZ?Lf>O3DRrTV&?}^N41vdH{mi+^MY}@Zw(E1*N+*qfj zH=(SjL9fE3@o%VN<*=d8{Y*XZ-%t-8z{!_91ZaXJbhH2T#hQQ7&X7IR;LO+l4h)3v zk9C}YUop6(2n)RzeHSomhxWXr^#14GmJt<2&f=HMVcY-998Ri0{2uez2TY>q*Y)+4 zhaCj;syA;dKfo>gd5hIljRn`2JZ$JtLL8mWID8mL>n6{B$~jB%08e2xkrc_CSx99y z3R(POF&$>fG-E~}p~7+53UD_OYc?6YSF>f}o@pi%HSK3C{BRj-b{9+vRpv&vyXn-* z1%9bzhVD)Rusi1cl<^t34;VjnEB{(@lxlRR1u1Yr`!#H+soVPOpIJt5YCAk?LNP~b zKLc6-XqxcjDX7|g+sVmR-UnTYy?c9NCieW?Pc5+ZdUx!?{;d!Gy@z>8@a@>wYN&5>3>QsHf?AV9nWyccx$WRd+}KpNuxWn; zpdxXRui9iF+3?jIlnq3E2Z#8e|7p-7EL^oWqInTUZW2!`SYidijb$ zG9X?xXrziIi2&;}<_bp>M|D@x$H<&o89Y?qm^qjhkpI*CM~BZ*uTlZ@*%-=BF$bcS zwJw)V9t&FS(NuWvJ$fk@{PKoVeq+C-tMuZYSIn!c!#>uq4F4oHc@XU)q*i;iHIx+j z>O_VyUSI$M`LV|u2ZogCF5k=eB{z=6zQr>Hv)&@C@HJyxjE<=~UPT4m$`1ZwZzWp) z0S-(}WRC-Q($Kvy&`L(G1yg{HNJ;O=mfx!yj2&uY)jR85uRN8~&Ge(G)-L}E1|w!v zEDoG6vPrHipf>4dmFhwu=Cx&gq|M3Tv^TD>d-eQ_TVAXkp`(|4t$xm06mW9I@RS;d z+dv8T^M{>(Q~Q0er!)Rpe~nF1k(?})yu4U0<=7kHfjRp+iW<5tmK;pBi&ndZ?<$r5 zwSG_%ZZ|_};zB62*n`{(P4X!7zJre{AYk@H>AOJIc;b@*T{=i5@{Vc{Gp)4mF9+m? z1{BJeS|N#$H%i`_(h1?!;mRhy;_c6o@6Wd9h4g8dTc!2ez2IkNJi%92h$6N9PJ0+rt zommP?&>H$W#?oFPK8fz4LMo|iVk4QKTU&v%?5t zZ7-s)Wvv%I%=)u?cpU{T<&=dSfKYm{uD4HfDU3M^K^Brpf<;$JW8w6q z%Jk)*%N2u(ZCfuzK(*{_Ie^1#^oWx3u+TRIXi}ALR5)AV$1+KVHFn2F*3i+YtAdPn zJTtcYwmw#zCc>xl$i{vScsMi93h z&SY=+dk9~{fRh2$lP8ns!4^Oq3|d6w1imMvg5f95vr;MFtm5O|eSwXI@5`~va z2SuFrM569~ptH^7yUBz)22_Eey$MTzy}7wZRWnm)cUN3?z6W9cS7!-)QI}QZcIL&H z|GRd)ItD84I8dC3yXG@J5{P1nnM9i``iH%*r}d#`jEloX?(<_uLb;7NRtAuuFq|0+ z5i!F;hR>iC;jWCOWQR>nhkF+vbvIYD0!9x{-n+j6!b_j<>%%1iQf`y=tA&x0&$KqLVQB#Obv5 z^941%{KU%3?W`3)j0HwpuNbXOcj05qNjByuNl>m(3jUVyV3sY;>phu0>oxG;`hGTKqmmhuo? zP&0q~Z2+u6Oepw99T`5K%WUSusRfxjMz6NIba48~q?3uWAEk1dsP~M;Ft8l9Geu_I zT)p-HZ~uKgxd0j)hJ}#AE`9_@?7mP_C<(cVlr1qmKqJud0mQD-Y`17(U^4K*A9nII z@odfi)81EqMfFAP!!St4(9JM(BT7mPFu)K>3nDEFD$+;_Gvv?%NGei8E1`5FAR#R! zA*qx!i1hdRsrUQS`xm^k?z(H;J8@_4v(Mi9dCorPC{xp-#ym5_^LMUZyuIKI!emVR zigFSJx_QyPb)YVbPIAWrk0*JMNsevaHz3I1+3aqO6iB+r(BwtvlWY5nf0oUu>oreK zPW}@#pMP<3Cd4K<(haf81#1GF!qcnT<{Rk5(z4_8qSuKR^a+jqRZ zCP%Bk`LCW|9$$$&n>Hi?cM2Of{D+D?{EHjBc2$K;@ME+0 zE{@<_FVsUYLNd9_+Z{DSK!<=YSsikZY}F?UY4!S8;imUCh>4$UTz_~}Z|1jJJM4PS zW@cC}+0RO7pN4PRz9W+W2ZY}F9@843gh<#adjkG(hRYw5& zAdnU;VU_n;}H>M7vdinAW4)-;L*6L8&Dgx5TSg|bw^>Z6TiuBB@4!0^k#lKDxLPp?%s_b3sv<= zQT%%wgOqRQm^-N6Oh2EZ^8Z9v#l}^D&IHO3J5qtzCafrU@m=@DO|QS{ci!= z=0pV|KU;ozK8aA`ARkYaPMDHG?lUej3ze~=&1EaoHy$mwSNl`>dnq?M(+|E@CZXMg(Ff(3o)H?t zOD-tau1*Dk0-;lGi3za%mmdQ@OJ5x-A6@wM467vP@fg}7aIMV>-8~V$nF`+f_BOyM z#1GwtFh3u7~vM0}$6lzz%c{r@7d$~bo>^6Iz(YwjggWlR@^U-j{;(Jo-QkgrU!E zqRu=E+|c82rxAgx);88W8T=EhU_1-6t4x$kx7+Z@SCW&EMPR6bmBw({wgBLS(>zN? zi8YITm`<=ylgk;!uce3*)I1}&+@mr(JLWc{71dBOK2)0wL~fXvwM?v=YQS;C%_=bH zkrl0I0(E3y&(M>n=yUaRkO{wQB zy^HVh1CL6X{O7k=cuw{;D(#uMrG6^QW5`6YX$iIBp)(j^Sh11=MsR4rH_+28P$qnl zlSb@`f6f@%9-n`U1Tz+h3B}P^QDA7WZ%xaVGM+d2ExvZ7xYP9gPJh$yj-vFNUS=+i zw&KU#jtw#_EV%OPwUw$UkYro(h93Od?|s`B(& zgkS4^$6_Tl4d&UILjLer(=HrI(0UN05T1fyBWeH;TBapt=Ueof%tq|Y47sTmhLWDC zcsE*O9hBh*iKO}5dEha)7d4p`#4**#h(B|8gbG5f)tuNYBs)n%lSn6WqJ+C;qwYz8 z!P$(1wB@Tpk*CQBbp-t53la=nx=v+%4F8@MCP4I^Pd>Gx_yOGWgV>k2$x0CBjRX~0 z*_8rA;L?W-<_{xqD00_!K_~&Zt1wm@M29t~ z7s*?~2|)Agm^Hp@u?ge%%29s1<7w+41W<@touiLCJ(ySaeOLD7>R@a&6tz%)#a1=G zgUC=i(w;&?B&Va=(iZ#L*GtNuHP7`_``^Zo%2VfvhtV4zRJ!M<23I??S9q2BSyu3z z=7dvd4@ATT6djgkGhRqQsMCYXa-kaVDjy^r!u+6bDF1f(O+NBpyiiH3;xc_^G%8KA zlD@oG>`T}hA1N%=@Wu`g+f`1f+o+cemk9S{gl!&*oM24o77rmVnBg@9OKRRPX-!Iq z>xT=kxzhc8*@q;>;LN;MJl1+1j%2L2-_fepF2wdcN;WQP-4R|N?jSU|sW!AjeIg<1 z^nICTT-H_Rm$bQe5E3KoM)o*M22Yel`b>vfthIqN4j+ zG!CIYF|VE-_fMeWhCaE5@oY7ZG}jtNHwV|>Q^Mj#1vwD^_hfWUh>goq8r zC<81=dQVWRV}Bg&bMX^zOv-NKY_OxR{6hFi;efz_1&fnOb{Oif#5l<^b~|{d0YFDOto4cHBt~Z zMXr`?m96_d1V&m_&I#8HL&eyEcvWN@3WGJUm*eaNPcWbn%rS8|-e*twU*UcN_=5J$ zp2C6;3?S55sG_%$KpF{8x?|SBn$k3j4%_>D*S2d;xh+(^5K1j{bs*g^-~MBg1=6!`n{r8^m>wT63n+VS&hg;aW5jWJthm;$caxkVq9m&eov ziWo(sm3jqgf;WzCutVIMFj`9Ks>i=FkyGl= zD<5D0J>R-(xkN;5^lg-fa(_iTD$Xx$Eh3FMP^Q zvi>yD*LsBNB)#wUd)reQ;!Fmu5=*ao`-6=?1|6wVO-|gFHxb2_#|N{6atR3 zO^!q%3(|=({RAV`P;EGlEQK!j^Uwtd2BDs#&~XlF`>mDcx7}stxy&fBxMK7R<7guvf(IdQ`Q1nnx#Oaq`!yAY8O~TMy)^{pb5Ip*okc0JxP(1CCjvyQwx*NhJ&$!X5Vr5 zGP`pD0UCgbUXIs42ME$(B?;bT|7Ej(E<+&Hk29Zk{?X&3@x*~_-mI<`i)+2A9M3Dg z-(GfFm{Wh#Q4Ih*MCh{(cuWl2`&7N{Kz!fY=Ln@>9ZMwNME-_qV&kg>GR=i36ms!| z)upI+b>DWbYR=?_vhHGi`KF0D3CU?V^~RyF3Bl54=)=)Up8du0E9w($#8?|6H#AtC z3~9CT2P1W@@q%CnaAU3pa11y$F1Y8}LbxjNZDmUS7eA!gf)>9QWlC1R^(G?3y_GH9 zx1qz9SUk77NrJ`ubG%GNo$oa(rmA~L=h^2;WvM^qitx$F#^o)@W0yRR9Q;~6Uj_yt z={mcG#?w|%5RE7XtL^%o%vW~&WAkK%=OpD>sRBZXzy?6OM;wXx3Os)5PTY;5PqXna zXg-KHu;B|zFKU4DLh%UgUy6YBLcmDWpW_n^{-l?Qfv=H#q>K>>8xX8`1@Dv*$WZGC zB{J^$9P1s(pDuEOeX0_$qto#%xm=g4blk8vO=glRim4s#uS0x*?WDIAD<<9=%$0*` zV)Z3(P-jDSEs$Q*ILXFMS$MBCs|+dugtXm$s`XSF$2qvPR#rYMCNtCuoNn+6z9v?$T31fn@sM`jUIsm`8ms7^eP<0^Vjhp z26dkN!c;!2I+Dps&y#z3YD0XdfwH02r-*7o2|rx#;f1{R+}Bt720_g4_nHlRVDE=MR0~X9FfLM03a?bRo2& zE)@lW0qN;Xa$I;(m5Ab*+8cgBvjSpE63m}ojG#}};TM5WTVHP|A==BgWyUw~g7Q&( z&aM$Li(S5c@bPoS0&P~uZ$f@9>bRK7#+btU#en@JH@t5Wqk-c8IhZs+Fc#nL_VxhE z`-yH%!{c#cC!J1_Pt)F1__?ffU@%C8HO&$uRZzy=%JC%U<1y38w@MEYjQO9>zI&|o zG7P5>NzRef0cO--b#z!%X1a(9V-e*rB(O8z&X}f30Mnij_w7^%q#X(;knlob-0zPg zBTmg3i%L&8TbQAcNEFy4PwYFV5GePndBQJ+KQI4HF?CPM!^<&y5t=r>e7VQeWd3GfpZKPf)FPUQqYc=j$H2%aM7X?( zCe=FdM4z&EGEN0V&`k9$;NW(^x{kG~SxLdsubKHvhNGjSk_v^Cq2+hRGSd8wCdP>A zoj(4@ys$p0Q)H`S9^vjC+dgj*OUE&pwzwmj9 zi&(d?)y@ar%C$3_MJ@;s1H1`n!tuq%r&!j#q^xtW$Le60VBu?3eCBs#QvIt^!)FUE zXWJd4`8saDzB*7HFz@Z=rMDUBDTdwq@W{3|k$3Iq&rZ?W+2EbP~QI97UkO^t1JIZiecWJpHD=dP(?0B#rc(ti-z>8t`d_{o0*!^pidve$lL~`Cdv0P z0r#qDvQfrueLK_DIv4aCz5H2-2M@Rjai9rLiBt(dPq*ez;!bsHk;F1I65v16<}7>` z!_1ofq;ia}qc68Zx;cZ`EG;eTwi`B2^Yd6_>&1RKj82WsMGi!!Wq7{v3fS`Y|4y7t zS9C@;arT-%mp}I`XwuZ6A)PTd4_ecsDIZZsVQ)TzHnqb47RZXs2N=XLe>%d>RKNTU z#r_4A`AHaA0bw~#Fa6G4fJmG46WAMOFYo_WHBS$@E*fwf;#WyI*AE&2*^O2J;Rk0^ zycWTsAus}=B^1mxc)v=RWpDZMhF;s~)#dGepPe%ZvvQ7;_itc%S512LW?;$kzUR6e zMe=~j2svi*ipU3^`v95vyLB6}@5tl!$(CPIKj#`vFOHt{y%en5`@~vl*A>;?L~m`x z1-37mqL7(I0qv!kCPbT4Z^Dc2m?NT6Jf!9b{B4VXL6YC|ueV&}|D2@BX(5{^qTi(g2wP5!zB%iHgoO8J}^v@5JaW|YR$x`A_ zMNmwO0{!^zI2z+bM~Lulrx8rZI-A*Jxbdw&Q^A(zht-8(fBjFP80(56k>lF)Td<(v zhLaz!PF#@pFn1?a{^aZ5G*1`qsigMl$dRMD=ZI*OQctOTksLdO>nJjN~nvapxSzSi!LG@zar!P?!j>eUry)-IxCELQ1Aq&GW132Jw9u$I8Ry7=ml z)degw7i}z*|D;>#s92AylbKu1&OBDU0LU6kIM+iQ2rY%g9}xmrR`0@=Z zUj?|oQw7B<{$l#Gv!YAj!KU4t~;sFzwS1=H#JjJoa^!Q z*jZ}J!d@-kYCWZ4->b5^OsjSjLY)+z5AKw?u;^6D*H3@m(12&zi_vP9^y0cKGc`9n zrNOGn+iH=fj0OMBgN_2*l(KdrBZUx?TRs#VTas99wlOK2-qh_+O@MFB$(ZJvKiKnO zYdsw=$M)Lqeisk&?de{|FNwwjAo;{AkfBPnT&RxR^cfrm1?XE2kE@8bfVVA&;z1{5 zZ+w3+2b|5!KVp1U*?@T{yVE{Plz1l2p3t%zCeF4;j?=mONi17xF2pPI-PGuKWSP@l zuc7@0`*m~d=*_o)!>p2MUh%KB;bzs#{!)YOS+SMb2|0?dZY#5_FcdT~vl5X#&bLT^ z-dONUt-%r)GNClnP>TA)iH(BnV9-k>M)`Y5+Vh7SN>!n0aEAw(>YXRbc)Z5poluH)vofuqf%`| zT|(OkbvBBP;+?_eY~IC!T%UMEp$roAVvZ=WwHc_Mf9;u%*yPmEWG8$F195?SQ z0z2ZFQjph~P@z}f_}`H#%MLIe$~gAQk7%Ugk(`*ADA-WM5lrN&N9&uKModiH1*W}P z8*G`Tir$xoqF^keyrnl@sTe7QQxMHm+@g=@FLoU({1M1kaGXfXezB$l_e69JtR`sX zkK+#BG!fjFw`%^yGCT$r=Fsf%YQAII@I&oaNnE;VVgH;J%=^xdSKCmYelGRAbkzjIsz;(`%{l(L8 zS!i1yqXW#aHI1nj?j(QenSohYI#kE&UkXc`u9EbWe2I5@QsdM?rq(Z{c*0a4LDO!V zKh;LW+&`k0oqe(UYX1D%pPJ}Q?{6gOCwsv3bRmiTZc^VlLPqkklaFZ8&w@ znzLLReAI-$@Pa<~SV|Zd5ArJJul&M%X#XC=hzfn$?);TXCu&gUSulMx*UD7MT9fjL zKx_bA>v6snv#kIn*GmD6bH`@+*4p?J(?Z*Hb4apZejfh?DF!vsL42F8v!6gvL&cLN z1%ufL%_qdIc?1?Fb!gq>F$`H&_M}g__$))suDn;oy8=|m6RdFUb9r$Jo0b`0g#*>{ z#zR3jn?~OQHuRE^n@aVc^qNOPA5^pKLo+%OUi*N3P4^h;F!Rm&>P)+Lqg=f_B|%jz z-cu-cJ2+ZawW6UBHEC^1ReFS{T9Xo=D91^&jvXtt%SaaA&lxT6-@*OrkU5$4EH@N1 zYY{W!d%svtwtjS0;myX)CA0zafFD9X+JOM~40LACkG))k9pBA_G$kq&xCi6X zfa_lOs~ns^3dnOC_pkgN?QPc=^#xM$-Tn~$?L&Z-FA2Z}d`{Bj{wtdX5^7lxzE*Fd zs;5@3U+D+-P4i>hAW5`E(7IVZz>3iqbGfmNKaqjx7 z4QtwPrmt*T)2#fGyzOqH+e~vZY&3MSZy=&{TxhjkRcgo8kV4JP3gau-R#U$fkU#^W zJZk2EI|6hm{97$k(#w4&sW}By@G~xJV(1P6`+>V>m7pizy@&5Z5ANB@d$Ilfp8AqU zo&7e4-ii}>P)LRn*$uHK^1@lqTTcoH7n-#}-2hBmNwZmfmH0Qn#3MoS@T3nt2zt-T zI(?j1oM)-7#$(y@>L6z|yr_BdiD~tYm!+)XPC_p%-5X0j2@~d=x_`g9s&wEL#PMr{ z4PPv>O?9d#B7HOGsS>52?D4>vg!*c8)KF}wV~#|zaaT_S1EkgScTi!j03V@_rt*pd z8pW6iaHUdM{qa>I+ezI}BN>XYHRHOYFNnJsxbNx%j{`-ZQNT#i$p@e%LX)5w|wv~M2or`3N@Gadm z{55m!<3yaa(~bfr#fPmpV4{>laH@ib&c-)uir1hNdx!hgKwftxmK2=eAzCMThL)K} zRX<0n6&mtMci+d-I*XQ{ONIpS*RE!9Ca;B);=&KAoTchzO0!oC-@ zbchSKWThX9ez~m!g9hR!c(~5t>B|sqcM`f5W|4aDgX$=5pr#|?RR$N|wzXhcG2D~B z)aySikvE&eI^${+rfV7R$S>#Hon*xH1{VupaCW!T!|AQBlpeiRVodntre>wXfda>+ z0L|wZxp(q6PzBB%s_P42y1TZKo!{Dl6qDgI`FK}Fhx0$odkNp?Q&AHo2c z?E>1zm&+W=1E|p-1Oro}U*le!D3C6H%Ar*0uVQE0F;5#CCKfi22BTiQBicR9x{CS5 zI02m>3)R=7g26a7zd*C8A98qhR=pC?dRFV+jfcTC`Ad0gu2Yy9%LPn7z1Jm-Kn zs}=xGG*e9i{Mwb_ZC5JjU=S@8$RGlt5}tDD7LwBG@*3OivT(T7P}>w&BNe2wX%wWY z^z8)(RfI&!2A_sSyY(^dbiO@M>X}IZYrF=SkIsYx3a!6*4}kih)P&31c@@x1jU;{C zR^>==rArD?(e*+{-o>kyk|GZ6=#isbDyEzE-uWcldd4oTd?HIDdgj*~L6eZUYf!YQ=hA1)|^W`78s1u<3+0hSir~VZyF6a$7qA*YGTutp`l z&isAbh9|{UrFiCpJPNFWcJR5!exKGdS)5d{KZ?7vi8UlmkGjk0Gu>M1IK+%i56|1oWKBx0L z-S>x-VvYS@38^0INu|b56?B!AoCs*uOTLGWVC_0{jrkBaV{mI2WMyM=r2B8{o#;S- z+&g$9L~6%nCPUet&#AGN**Uc z>4`(r=-)`7CqX?`ZYxe@`s7dXR{pu$IfFBQ6R_tG&DV3wwGQE=`NrtH!o z^sW`*)BWK6-iUj*yA8~)oQXhhBr8Ap6R27o^% zMAcFGK=QHx^E@nv3}OH6XErD>zy-`qwggKZ&qur)I%ifX1LUBj4F2^lx5Ea3T{syo)P27LegTPrR3S}vlz_>7Am|4s+?miqX)iqWpiAbU1Wq;IHPd6fSN7ZvZzew)jTIi$Kpio?A;BOgQ zLu9AhZZc!VG1V1OI{nULZU{*(;U5w8 z8W5o}a&&G<^%VAf^Yfjf;*%81@Qg2I8a|i)nz~9+hn9@>KB*b>?~fCRMlSbROp2^- z`E%U41O^D)ei35sWGL)McX_{+2NY9s-`z^$A0pkD2tBo(Iko@0!)J439{RP30Ad|& z!OIDQoi1iMJ3mp-ZLez$&y&453~IcQt)nS(F!bu@E+RJXxM5ZXtY?)D9@zH9ROc@98_>Fr{tpxvlJUu#~ zfoSW#%yMW4qAjsxD5JJ{={JV!-H$ARDxf@7VU8+o0`ux1;9?xV=!c-;2 zKu{o^sGd@AI%gd@f8ahJqEL6)=A*#}?POmV9k$QH)QbKeHDFQwjXe+DZ01;g2A-#` z&F;-2`r@dnfJnxb^eL2ASYk_Y70Se%zVzDwAI*P#3FxETaj#vF{~4np z*rHpb-~;=Bo&u`IAt+40jCQ2L2pBp&6c$vFDK?fbF?dNTXAoqZx6-NO^)ZFiew&?7#5@?U(Uu~L+xn&i(B^2jr4Rr4 zg-;UbU#WrdA$nKj6PK}=a55{Fl$3Z>d6G#vosdVfP{!gjq4znQEu5uU(E+lQHZyoE z4$y#6!lGL{(xC;SY_uylvCc{|;_->fYxcTtr=}FONPDI%Z@rQGSDkHmB)H=y^w`r@ zwZJ)Xh}Uun1A=rkHXzN9Qtq{O^{8qN`s`rI>)GGsY&MTk=QCDn zC5HId-EDDs*y zKgo9-{0DUR)B&|}03SAdYzAb1sz4D^;vyqe0{KbHp@SSZ)V3}u^`oU5H;7CO#8N=D zJ^NdTriccx1fhG5R($`q6AEXPkDi++vZKPEIsJ<@9IbNnR0jZwP{X&<0{NA4Q2X)oj}dGlKR5Ui1T?%*MTc_#pOwEP@$5 zmIvQZ!Ye08cx_5#Cs_#8T>&gkEdXsbr$M6z*^44Jm$%zq-axJK6j-u<2wP!|R|iy6 zS@%T2;lKZr^Mws4V#fm`v=A!Bke*gT+?6=CnX^dCANZ}V#6hzHf)uUB#3D@=_$X-q z3##D&EtpDRGzj{*d=wr2Ts}aG#F!RfETrB^mnZ+58nkgQ@S%QMqdVu|uHyR3cnOID z7&;t~Gb$_@WLHoVzq|J7%G7372x_dC0eHp`z>1i`H(Nsg{l3jxT37V!Cj5LJ^-%|I zDMKB`&qYI+a2w2XpTM34b*=^9C4gV>!aX^tvHTtYsAd94lOCI*J*ofv?1L|-4xcrSeF69C}K2 Date: Tue, 8 Nov 2016 23:33:10 -0500 Subject: [PATCH 18/23] fix annotation drag on date axes, and test date/log annotations --- src/components/annotations/draw.js | 12 ++++++------ .../image/baselines/annotations-autorange.png | Bin 28130 -> 31636 bytes test/image/mocks/annotations-autorange.json | 14 ++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 563c4cefe7e..5b36d7430e1 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -579,21 +579,21 @@ function drawOne(gd, index, opt, value) { ann.call(Lib.setTranslate, xcenter, ycenter); update[annbase + '.x'] = xa ? - (options.x + dx / xa._m) : + xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.x)) + dx)) : ((arrowX + dx - gs.l) / gs.w); update[annbase + '.y'] = ya ? - (options.y + dy / ya._m) : + ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.y)) + dy)) : (1 - ((arrowY + dy - gs.t) / gs.h)); if(options.axref === options.xref) { update[annbase + '.ax'] = xa ? - (options.ax + dx / xa._m) : + xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.ax)) + dx)) : ((arrowX + dx - gs.l) / gs.w); } if(options.ayref === options.yref) { update[annbase + '.ay'] = ya ? - (options.ay + dy / ya._m) : + ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.ay)) + dy)) : (1 - ((arrowY + dy - gs.t) / gs.h)); } @@ -640,13 +640,13 @@ function drawOne(gd, index, opt, value) { var csr = 'pointer'; if(options.showarrow) { if(options.axref === options.xref) { - update[annbase + '.ax'] = xa.p2l(xa.l2p(options.ax) + dx); + update[annbase + '.ax'] = xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.ax)) + dx)); } else { update[annbase + '.ax'] = options.ax + dx; } if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya.p2l(ya.l2p(options.ay) + dy); + update[annbase + '.ay'] = ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.ay)) + dy)); } else { update[annbase + '.ay'] = options.ay + dy; } diff --git a/test/image/baselines/annotations-autorange.png b/test/image/baselines/annotations-autorange.png index 064f92726876280158401989b4995dd4776e5fd4..07df4be6243d01cad1669b39d6126bedbaab4065 100644 GIT binary patch literal 31636 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%0Dl zs|}h8rc)G=?t64j+xVrknU(T0i(2@>+og_!R-V(X2?BZg8Ev8W3I=SV#B@`|HE}$} z0=Mq=7M*3eMqQzo(i%>I*p+zl#c`y}O^^z+d4otaaV`gMmi7(*Y8sX;TIolbLB4(i zKI({+UX=VgzI?TgY6dhR^zENBm~L#$cJ#jXyD@dpk}?Y1lPmQ(t$P$|WR1;~9Drw8 z3{srNz%&h6;)YUeGwe;b@YJ>Z4=FJXa9yqH7TW_VtusMle4Dq1P^x%t-* z*V(%W43}w(XPgI$vII@bL|z1*xZHkd8}zKlXby{u*m}RZ_L1j>L%-fa|HCs}DH0Bh zRi`PBKak0A=**`)H=K*~!PVg;vTR}rWrM2)7
)GhkR`7INb7ICyf@Y{-lu4F+9} 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 z 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
arrow","y":1.5,"x":1,"showarrow":false}, {"xref":"x3","yref":"y3","text":"Right
no
arrow","y":1.5,"x":2,"showarrow":false}, {"xref":"x3","yref":"y3","text":"Bottom
no
arrow","y":1,"x":1.5,"showarrow":false}, From 76794837a10e8874f1da9d29d1e73d6e1b1c8355 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 9 Nov 2016 15:02:55 -0500 Subject: [PATCH 19/23] update jasmine test of annotation visibility & autorange --- test/jasmine/tests/annotations_test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index caddd3d406a..bda05520697 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -151,12 +151,16 @@ describe('annotations autosize', function() { // xaxis2 need a bit more tolerance to pass on CI // this most likely due to the different text bounding box values // on headfull vs headless browsers. - var PREC2 = 0.1; + // but also because it's a date axis that we've converted to ms + var PRECX2 = -10; + // yaxis2 needs a bit more now too... + var PRECY2 = 0.2; + var dateAx = fullLayout.xaxis2; expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); - expect(fullLayout.xaxis2.range).toBeCloseToArray(x2, PREC2, 'xaxis2'); - expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PREC, 'yaxis2'); + expect(dateAx.range.map(dateAx.r2l)).toBeCloseToArray(x2.map(dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); + expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, 'yaxis2'); expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, 'xaxis3'); expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, 'yaxis3'); } @@ -164,7 +168,7 @@ describe('annotations autosize', function() { Plotly.plot(gd, mock).then(function() { assertRanges( [0.97, 2.03], [0.97, 2.03], - [-0.32, 3.38], [0.42, 2.58], + ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], [0.9, 2.1], [0.86, 2.14] ); @@ -177,7 +181,7 @@ describe('annotations autosize', function() { .then(function() { assertRanges( [1.44, 2.02], [0.97, 2.03], - [1.31, 2.41], [0.42, 2.58], + ['2001-01-18 15:06:04.0449', '2001-03-27 14:01:20.8989'], [-0.245, 4.245], [1.44, 2.1], [0.86, 2.14] ); @@ -190,7 +194,7 @@ describe('annotations autosize', function() { .then(function() { assertRanges( [1.44, 2.02], [0.99, 1.52], - [0.5, 2.5], [0.42, 2.58], + ['2001-01-31 23:59:59.999', '2001-02-01 00:00:00.001'], [-0.245, 4.245], [0.5, 2.5], [0.86, 2.14] ); @@ -206,7 +210,7 @@ describe('annotations autosize', function() { .then(function() { assertRanges( [0.97, 2.03], [0.97, 2.03], - [-0.32, 3.38], [0.42, 2.58], + ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], [0.9, 2.1], [0.86, 2.14] ); }) From 5e11571c5d64df7a80ef4a3708d4224e6b627426 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 9 Nov 2016 15:55:07 -0500 Subject: [PATCH 20/23] merge lint --- test/jasmine/tests/shapes_test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 279f231eda2..f592201600a 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -12,7 +12,6 @@ var d3 = require('d3'); var customMatchers = require('../assets/custom_matchers'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var customMatchers = require('../assets/custom_matchers'); describe('shape supplyDefaults', function() { From d807259be4e541ddb1e4f0b4de90e772aace8a24 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 10 Nov 2016 15:28:46 -0500 Subject: [PATCH 21/23] add ax.r2p and ax.p2r --- src/components/annotations/draw.js | 20 ++++++++++---------- src/components/images/draw.js | 4 ++-- src/components/shapes/draw.js | 4 ++-- src/components/shapes/helpers.js | 4 ++-- src/plots/cartesian/set_convert.js | 5 +++++ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 482f34dda8a..ce70706fc1f 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -370,7 +370,7 @@ function drawOne(gd, index, opt, value) { if(annotationIsOffscreen) return; } - annPosPx[axLetter] = ax._offset + ax.l2p(ax.r2l(options[axLetter])); + annPosPx[axLetter] = ax._offset + ax.r2p(options[axLetter]); alignPosition = 0.5; } else { @@ -383,7 +383,7 @@ function drawOne(gd, index, opt, value) { var alignShift = 0; if(options['a' + axLetter + 'ref'] === axRef) { - annPosPx['aa' + axLetter] = ax._offset + ax.l2p(ax.r2l(options['a' + axLetter])); + annPosPx['aa' + axLetter] = ax._offset + ax.r2p(options['a' + axLetter]); } else { if(options.showarrow) { alignShift = options['a' + axLetter]; @@ -583,22 +583,22 @@ function drawOne(gd, index, opt, value) { ann.call(Lib.setTranslate, xcenter, ycenter); update[annbase + '.x'] = xa ? - xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.x)) + dx)) : + xa.p2r(xa.r2p(options.x) + dx) : ((arrowX + dx - gs.l) / gs.w); update[annbase + '.y'] = ya ? - ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.y)) + dy)) : + ya.p2r(ya.r2p(options.y) + dy) : (1 - ((arrowY + dy - gs.t) / gs.h)); if(options.axref === options.xref) { update[annbase + '.ax'] = xa ? - xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.ax)) + dx)) : - ((arrowX + dx - gs.l) / gs.w); + xa.p2r(xa.r2p(options.ax) + dx) : + ((arrowX + dx - gs.l) / gs.w); } if(options.ayref === options.yref) { update[annbase + '.ay'] = ya ? - ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.ay)) + dy)) : - (1 - ((arrowY + dy - gs.t) / gs.h)); + ya.p2r(ya.r2p(options.ay) + dy) : + (1 - ((arrowY + dy - gs.t) / gs.h)); } anng.attr({ @@ -644,13 +644,13 @@ function drawOne(gd, index, opt, value) { var csr = 'pointer'; if(options.showarrow) { if(options.axref === options.xref) { - update[annbase + '.ax'] = xa.l2r(xa.p2l(xa.l2p(xa.r2l(options.ax)) + dx)); + update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx); } else { update[annbase + '.ax'] = options.ax + dx; } if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya.l2r(ya.p2l(ya.l2p(ya.r2l(options.ay)) + dy)); + update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy); } else { update[annbase + '.ay'] = options.ay + dy; } diff --git a/src/components/images/draw.js b/src/components/images/draw.js index 99ccc7d0e50..bf570357dd2 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -113,8 +113,8 @@ module.exports = function draw(gd) { var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; // Final positions - var xPos = (xa ? xa.l2p(xa.r2l(d.x)) + xa._offset : d.x * size.w + size.l) + xOffset, - yPos = (ya ? ya.l2p(ya.r2l(d.y)) + ya._offset : size.h - d.y * size.h + size.t) + yOffset; + var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) + xOffset, + yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) + yOffset; // Construct the proper aspectRatio attribute diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 49ea4aa77f0..1d2f195f6b1 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -473,7 +473,7 @@ function getPathString(gd, options) { if(xa) { x2r = helpers.shapePositionToRange(xa); - x2p = function(v) { return xa._offset + xa.l2p(xa.r2l(x2r(v, true))); }; + x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; } else { x2p = function(v) { return gs.l + gs.w * v; }; @@ -481,7 +481,7 @@ function getPathString(gd, options) { if(ya) { y2r = helpers.shapePositionToRange(ya); - y2p = function(v) { return ya._offset + ya.l2p(ya.r2l(y2r(v, true))); }; + y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; } else { y2p = function(v) { return gs.t + gs.h * (1 - v); }; diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index 7d816d883bc..76943237492 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -45,7 +45,7 @@ exports.getDataToPixel = function(gd, axis, isVertical) { var d2r = exports.shapePositionToRange(axis); dataToPixel = function(v) { - return axis._offset + axis.l2p(axis.r2l(d2r(v, true))); + return axis._offset + axis.r2p(d2r(v, true)); }; if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); @@ -66,7 +66,7 @@ exports.getPixelToData = function(gd, axis, isVertical) { if(axis) { var r2d = exports.rangeToShapePosition(axis); - pixelToData = function(p) { return r2d(axis.l2r(axis.p2l(p - axis._offset))); }; + pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); }; } else if(isVertical) { pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index e61b2623e0b..534e380c7a2 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -232,6 +232,11 @@ module.exports = function setConvert(ax) { ax.c2p = function(v, clip) { return ax.l2p(ax.c2l(v, clip)); }; ax.p2c = function(px) { return ax.l2c(ax.p2l(px)); }; + // clip doesn't do anything here yet, but in v2.0 when log axes get + // refactored it will... so including it now so we don't forget. + ax.r2p = function(v, clip) { return ax.l2p(ax.r2l(v, clip)); }; + ax.p2r = function(px) { return ax.l2r(ax.p2l(px)); }; + if(['linear', 'log', '-'].indexOf(ax.type) !== -1) { ax.c2d = num; ax.d2c = Lib.cleanNumber; From 05c762d07d403191789f59cb8e55097f0d034b5c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 10 Nov 2016 15:31:01 -0500 Subject: [PATCH 22/23] date interval milliseconds constants --- src/constants/numerical.js | 15 ++++++- src/lib/dates.js | 24 +++++++---- src/plots/cartesian/axes.js | 49 +++++++++++----------- src/plots/cartesian/tick_value_defaults.js | 3 +- 4 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/constants/numerical.js b/src/constants/numerical.js index 3fcc6c333a8..d10ffa42a51 100644 --- a/src/constants/numerical.js +++ b/src/constants/numerical.js @@ -23,5 +23,18 @@ module.exports = { * to avoid glitches: Make sure that even when you multiply it by the * number of pixels on a giant screen it still works */ - FP_SAFE: Number.MAX_VALUE / 10000 + FP_SAFE: Number.MAX_VALUE / 10000, + + /* + * conversion of date units to milliseconds + * year and month constants are marked "AVG" + * to remind us that not all years and months + * have the same length + */ + ONEAVGYEAR: 31557600000, // 365.25 days + ONEAVGMONTH: 2629800000, // 1/12 of ONEAVGYEAR + ONEDAY: 86400000, + ONEHOUR: 3600000, + ONEMIN: 60000, + ONESEC: 1000 }; diff --git a/src/lib/dates.js b/src/lib/dates.js index 49778767642..72dfecd5f72 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -11,9 +11,16 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); -var BADNUM = require('../constants/numerical').BADNUM; + var logError = require('./loggers').error; +var constants = require('../constants/numerical'); +var BADNUM = constants.BADNUM; +var ONEDAY = constants.ONEDAY; +var ONEHOUR = constants.ONEHOUR; +var ONEMIN = constants.ONEMIN; +var ONESEC = constants.ONESEC; + // is an object a javascript date? exports.isJSDate = function(v) { return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; @@ -140,14 +147,14 @@ exports.dateTime2ms = function(s) { // minute - must be 2 digits m = Number(p[1]); if(p[1].length !== 2 || !(m >= 0 && m <= 59)) return BADNUM; - d += 60000 * m; + d += ONEMIN * m; if(p.length === 2) return d; // second (and milliseconds) - must have 2-digit seconds if(p[2].split('.')[0].length !== 2) return BADNUM; s = Number(p[2]); if(!(s >= 0 && s < 60)) return BADNUM; - return d + s * 1000; + return d + s * ONESEC; } } } @@ -176,6 +183,9 @@ function lpad(val, digits) { * Optional range r is the data range that applies, also in ms. * If rng is big, the later parts of time will be omitted */ +var NINETYDAYS = 90 * ONEDAY; +var THREEHOURS = 3 * ONEHOUR; +var FIVEMIN = 5 * ONEMIN; exports.ms2DateTime = function(ms, r) { if(typeof ms !== 'number' || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM; @@ -184,12 +194,12 @@ exports.ms2DateTime = function(ms, r) { var d = new Date(Math.floor(ms)), dateStr = d3.time.format('%Y-%m-%d')(d), // <90 days: add hours and minutes - never *only* add hours - h = (r < 7776000000) ? d.getHours() : 0, - m = (r < 7776000000) ? d.getMinutes() : 0, + h = (r < NINETYDAYS) ? d.getHours() : 0, + m = (r < NINETYDAYS) ? d.getMinutes() : 0, // <3 hours: add seconds - s = (r < 10800000) ? d.getSeconds() : 0, + s = (r < THREEHOURS) ? d.getSeconds() : 0, // <5 minutes: add ms (plus one extra digit, this is msec*10) - msec10 = (r < 300000) ? Math.round((d.getMilliseconds() + (((ms % 1) + 1) % 1)) * 10) : 0; + msec10 = (r < FIVEMIN) ? Math.round((d.getMilliseconds() + (((ms % 1) + 1) % 1)) * 10) : 0; // include each part that has nonzero data in or after it if(h || m || s || msec10) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 6b8184ef275..2efd62a2e95 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -19,7 +19,14 @@ var Titles = require('../../components/titles'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); -var FP_SAFE = require('../../constants/numerical').FP_SAFE; +var constants = require('../../constants/numerical'); +var FP_SAFE = constants.FP_SAFE; +var ONEAVGYEAR = constants.ONEAVGYEAR; +var ONEAVGMONTH = constants.ONEAVGMONTH; +var ONEDAY = constants.ONEDAY; +var ONEHOUR = constants.ONEHOUR; +var ONEMIN = constants.ONEMIN; +var ONESEC = constants.ONESEC; var axes = module.exports = {}; @@ -727,7 +734,7 @@ function roundDTick(roughDTick, base, roundingSet) { // outputs (into ax): // tick0: starting point for ticks (not necessarily on the graph) // usually 0 for numeric (=10^0=1 for log) or jan 1, 2000 for dates -// dtick: the actual, nice round tick spacing, somewhat larger than roughDTick +// dtick: the actual, nice round tick spacing, usually a little larger than roughDTick // if the ticks are spaced linearly (linear scale, categories, // log with only full powers, date ticks < month), // this will just be a number @@ -741,37 +748,34 @@ axes.autoTicks = function(ax, roughDTick) { if(ax.type === 'date') { ax.tick0 = '2000-01-01'; + // the criteria below are all based on the rough spacing we calculate + // being > half of the final unit - so precalculate twice the rough val + var roughX2 = 2 * roughDTick; - if(roughDTick > 15778800000) { - // years if roughDTick > 6mo - roughDTick /= 31557600000; + if(roughX2 > ONEAVGYEAR) { + roughDTick /= ONEAVGYEAR; base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10)); } - else if(roughDTick > 1209600000) { - // months if roughDTick > 2wk - roughDTick /= 2629800000; + else if(roughX2 > ONEAVGMONTH) { + roughDTick /= ONEAVGMONTH; ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24); } - else if(roughDTick > 43200000) { - // days if roughDTick > 12h - ax.dtick = roundDTick(roughDTick, 86400000, roundDays); + else if(roughX2 > ONEDAY) { + ax.dtick = roundDTick(roughDTick, ONEDAY, roundDays); // get week ticks on sunday // this will also move the base tick off 2000-01-01 if dtick is // 2 or 3 days... but that's a weird enough case that we'll ignore it. ax.tick0 = '2000-01-02'; } - else if(roughDTick > 1800000) { - // hours if roughDTick > 30m - ax.dtick = roundDTick(roughDTick, 3600000, roundBase24); + else if(roughX2 > ONEHOUR) { + ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); } - else if(roughDTick > 30000) { - // minutes if roughDTick > 30sec - ax.dtick = roundDTick(roughDTick, 60000, roundBase60); + else if(roughX2 > ONEMIN) { + ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60); } - else if(roughDTick > 500) { - // seconds if roughDTick > 0.5sec - ax.dtick = roundDTick(roughDTick, 1000, roundBase60); + else if(roughX2 > ONESEC) { + ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60); } else { // milliseconds @@ -826,11 +830,6 @@ axes.autoTicks = function(ax, roughDTick) { } }; -var ONEDAY = 86400000, - ONEHOUR = 3600000, - ONEMIN = 60000, - ONESEC = 1000; - // after dtick is already known, find tickround = precision // to display in tick labels // for numeric ticks, integer # digits after . to round to diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index cf08ed64fb5..f441cb5e055 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -11,6 +11,7 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); +var ONEDAY = require('../../constants/numerical').ONEDAY; module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { @@ -32,7 +33,7 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe // dtick is usually a positive number, but there are some // special strings available for log or date axes // default is 1 day for dates, otherwise 1 - var dtickDflt = (axType === 'date') ? 86400000 : 1; + var dtickDflt = (axType === 'date') ? ONEDAY : 1; var dtick = coerce('dtick', dtickDflt); if(isNumeric(dtick)) { containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt; From 1b0e133c89543b9edc65a441d420c248bf8f90a5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 10 Nov 2016 15:31:46 -0500 Subject: [PATCH 23/23] documentation updates --- src/plots/cartesian/axes.js | 5 ++++- src/plots/cartesian/layout_attributes.js | 26 +++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 2efd62a2e95..b3dbcc63626 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -92,7 +92,10 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption * * Also cleans the values, since the attribute definition itself has to say * valType: 'any' to handle date axes. This allows us to accept: - * - for category axes: category names, and convert them here into serial numbers + * - for category axes: category names, and convert them here into serial numbers. + * Note that this will NOT work for axis range endpoints, because we don't know + * the category list yet (it's set by ax.makeCalcdata during calc) + * but it works for component (note, shape, images) positions. * - for date axes: JS Dates or milliseconds, and convert to date strings * - for other types: coerce them to numbers */ diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index ebc60e0b050..89f0f9dcbdc 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -329,11 +329,14 @@ module.exports = { dflt: '', role: 'style', description: [ - 'Sets the tick label formatting rule using the', - 'python/d3 number formatting language.', - 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', - 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', - 'for more info.' + 'Sets the tick label formatting rule using d3 formatting mini-languages', + 'which are very similar to those in Python. For numbers, see:', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', + 'And for dates see:', + 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', + 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', + 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', + '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, hoverformat: { @@ -341,11 +344,14 @@ module.exports = { dflt: '', role: 'style', description: [ - 'Sets the hover text formatting rule for data values on this axis,', - 'using the python/d3 number formatting language.', - 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', - 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', - 'for more info.' + 'Sets the hover text formatting rule using d3 formatting mini-languages', + 'which are very similar to those in Python. For numbers, see:', + 'https://github.com/d3/d3-format/blob/master/README.md#locale_format', + 'And for dates see:', + 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', + 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', + 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', + '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, // lines and grids