X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-tickers.js;h=35cd75af7f905ca25abc146d1701fc7284a33705;hb=2e4626574edc0a30cc3059694b5d04193e0f32ae;hp=80eb291c9235872da1ff45232d1c15207415af17;hpb=0815ea828a3ec9bb9474eea7aaab954baac778c2;p=dygraphs.git diff --git a/dygraph-tickers.js b/dygraph-tickers.js index 80eb291..35cd75a 100644 --- a/dygraph-tickers.js +++ b/dygraph-tickers.js @@ -58,12 +58,13 @@ * middle of the years. */ -/*jshint globalstrict: true */ +/*jshint sub:true */ /*global Dygraph:false */ +(function() { "use strict"; /** @typedef {Array.<{v:number, label:string, label_v:(string|undefined)}>} */ -Dygraph.TickList; +Dygraph.TickList = undefined; // the ' = undefined' keeps jshint happy. /** @typedef {function( * number, @@ -74,8 +75,7 @@ Dygraph.TickList; * Array.= * ): Dygraph.TickList} */ -Dygraph.Ticker; - +Dygraph.Ticker = undefined; // the ' = undefined' keeps jshint happy. /** @type {Dygraph.Ticker} */ Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) { @@ -88,16 +88,6 @@ Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) { /** @type {Dygraph.Ticker} */ Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { - // This masks some numeric issues in older versions of Firefox, - // where 1.0/Math.pow(10,2) != Math.pow(10,-2). - /** @type {function(number,number):number} */ - var pow = function(base, exp) { - if (exp < 0) { - return 1.0 / Math.pow(base, -exp); - } - return Math.pow(base, exp); - }; - var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel')); var ticks = []; var i, j, tickV, nTicks; @@ -182,7 +172,7 @@ Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { // that results in tick marks spaced sufficiently far apart. // The "mults" array should cover the range 1 .. base^2 to // adjust for rounding and edge effects. - var scale, low_val, high_val, nTicks, spacing; + var scale, low_val, high_val, spacing; for (j = 0; j < mults.length; j++) { scale = base_scale * mults[j]; low_val = Math.floor(a / scale) * scale; @@ -195,65 +185,20 @@ Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { // Construct the set of ticks. // Allow reverse y-axis if it's explicitly requested. if (low_val > high_val) scale *= -1; - for (i = 0; i < nTicks; i++) { + for (i = 0; i <= nTicks; i++) { tickV = low_val + i * scale; ticks.push( {v: tickV} ); } } } - // Add formatted labels to the ticks. - var k; - var k_labels = []; - var m_labels = []; - if (opts("labelsKMB")) { - k = 1000; - k_labels = [ "K", "M", "B", "T", "Q" ]; - } - if (opts("labelsKMG2")) { - if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); - k = 1024; - k_labels = [ "k", "M", "G", "T", "P", "E", "Z", "Y" ]; - m_labels = [ "m", "u", "n", "p", "f", "a", "z", "y" ]; - } - - k = k || 1; // If neither option is specified. - var formatter = /**@type{AxisLabelFormatter}*/(opts('axisLabelFormatter')); // Add labels to the ticks. - var digitsAfterDecimal = /** @type{number} */(opts('digitsAfterDecimal')); for (i = 0; i < ticks.length; i++) { if (ticks[i].label !== undefined) continue; // Use current label. - tickV = ticks[i].v; - var absTickV = Math.abs(tickV); // TODO(danvk): set granularity to something appropriate here. - var label = formatter(tickV, 0, opts, dygraph); - if (k_labels.length > 0) { - // TODO(danvk): should this be integrated into the axisLabelFormatter? - // Round up to an appropriate unit. - var n = pow(k, k_labels.length); - for (j = k_labels.length - 1; j >= 0; j--, n /= k) { - if (absTickV >= n) { - label = Dygraph.round_(tickV / n, digitsAfterDecimal) + k_labels[j]; - break; - } - } - } - if(opts("labelsKMG2")){ - tickV = String(tickV.toExponential()); - if(tickV.split('e-').length === 2 && tickV.split('e-')[1] >= 3 && tickV.split('e-')[1] <= 24){ - if(tickV.split('e-')[1] % 3 > 0) { - label = Dygraph.round_(tickV.split('e-')[0] / - pow(10,(tickV.split('e-')[1] % 3)), - digitsAfterDecimal); - } else { - label = Number(tickV.split('e-')[0]).toFixed(2); - } - label += m_labels[Math.floor(tickV.split('e-')[1] / 3) - 1]; - } - } - ticks[i].label = label; + ticks[i].label = formatter(ticks[i].v, 0, opts, dygraph); } return ticks; @@ -288,32 +233,65 @@ Dygraph.HOURLY = 10; Dygraph.TWO_HOURLY = 11; Dygraph.SIX_HOURLY = 12; Dygraph.DAILY = 13; -Dygraph.WEEKLY = 14; -Dygraph.MONTHLY = 15; -Dygraph.QUARTERLY = 16; -Dygraph.BIANNUAL = 17; -Dygraph.ANNUAL = 18; -Dygraph.DECADAL = 19; -Dygraph.CENTENNIAL = 20; -Dygraph.NUM_GRANULARITIES = 21; - -/** @type {Array.} */ -Dygraph.SHORT_SPACINGS = []; -Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5; -Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10; -Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30; -Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5; -Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10; -Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30; -Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6; -Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400; -Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; +Dygraph.TWO_DAILY = 14; +Dygraph.WEEKLY = 15; +Dygraph.MONTHLY = 16; +Dygraph.QUARTERLY = 17; +Dygraph.BIANNUAL = 18; +Dygraph.ANNUAL = 19; +Dygraph.DECADAL = 20; +Dygraph.CENTENNIAL = 21; +Dygraph.NUM_GRANULARITIES = 22; + +// Date components enumeration (in the order of the arguments in Date) +// TODO: make this an @enum +Dygraph.DATEFIELD_Y = 0; +Dygraph.DATEFIELD_M = 1; +Dygraph.DATEFIELD_D = 2; +Dygraph.DATEFIELD_HH = 3; +Dygraph.DATEFIELD_MM = 4; +Dygraph.DATEFIELD_SS = 5; +Dygraph.DATEFIELD_MS = 6; +Dygraph.NUM_DATEFIELDS = 7; + + +/** + * The value of datefield will start at an even multiple of "step", i.e. + * if datefield=SS and step=5 then the first tick will be on a multiple of 5s. + * + * For granularities <= HOURLY, ticks are generated every `spacing` ms. + * + * At coarser granularities, ticks are generated by incrementing `datefield` by + * `step`. In this case, the `spacing` value is only used to estimate the + * number of ticks. It should roughly correspond to the spacing between + * adjacent ticks. + * + * @type {Array.<{datefield:number, step:number, spacing:number}>} + */ +Dygraph.TICK_PLACEMENT = []; +Dygraph.TICK_PLACEMENT[Dygraph.SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step: 1, spacing: 1000 * 1}; +Dygraph.TICK_PLACEMENT[Dygraph.TWO_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step: 2, spacing: 1000 * 2}; +Dygraph.TICK_PLACEMENT[Dygraph.FIVE_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step: 5, spacing: 1000 * 5}; +Dygraph.TICK_PLACEMENT[Dygraph.TEN_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step: 10, spacing: 1000 * 10}; +Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_SECONDLY] = {datefield: Dygraph.DATEFIELD_SS, step: 30, spacing: 1000 * 30}; +Dygraph.TICK_PLACEMENT[Dygraph.MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step: 1, spacing: 1000 * 60}; +Dygraph.TICK_PLACEMENT[Dygraph.TWO_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step: 2, spacing: 1000 * 60 * 2}; +Dygraph.TICK_PLACEMENT[Dygraph.FIVE_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step: 5, spacing: 1000 * 60 * 5}; +Dygraph.TICK_PLACEMENT[Dygraph.TEN_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step: 10, spacing: 1000 * 60 * 10}; +Dygraph.TICK_PLACEMENT[Dygraph.THIRTY_MINUTELY] = {datefield: Dygraph.DATEFIELD_MM, step: 30, spacing: 1000 * 60 * 30}; +Dygraph.TICK_PLACEMENT[Dygraph.HOURLY] = {datefield: Dygraph.DATEFIELD_HH, step: 1, spacing: 1000 * 3600}; +Dygraph.TICK_PLACEMENT[Dygraph.TWO_HOURLY] = {datefield: Dygraph.DATEFIELD_HH, step: 2, spacing: 1000 * 3600 * 2}; +Dygraph.TICK_PLACEMENT[Dygraph.SIX_HOURLY] = {datefield: Dygraph.DATEFIELD_HH, step: 6, spacing: 1000 * 3600 * 6}; +Dygraph.TICK_PLACEMENT[Dygraph.DAILY] = {datefield: Dygraph.DATEFIELD_D, step: 1, spacing: 1000 * 86400}; +Dygraph.TICK_PLACEMENT[Dygraph.TWO_DAILY] = {datefield: Dygraph.DATEFIELD_D, step: 2, spacing: 1000 * 86400 * 2}; +Dygraph.TICK_PLACEMENT[Dygraph.WEEKLY] = {datefield: Dygraph.DATEFIELD_D, step: 7, spacing: 1000 * 604800}; +Dygraph.TICK_PLACEMENT[Dygraph.MONTHLY] = {datefield: Dygraph.DATEFIELD_M, step: 1, spacing: 1000 * 7200 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 12 +Dygraph.TICK_PLACEMENT[Dygraph.QUARTERLY] = {datefield: Dygraph.DATEFIELD_M, step: 3, spacing: 1000 * 21600 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 4 +Dygraph.TICK_PLACEMENT[Dygraph.BIANNUAL] = {datefield: Dygraph.DATEFIELD_M, step: 6, spacing: 1000 * 43200 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 2 +Dygraph.TICK_PLACEMENT[Dygraph.ANNUAL] = {datefield: Dygraph.DATEFIELD_Y, step: 1, spacing: 1000 * 86400 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 1 +Dygraph.TICK_PLACEMENT[Dygraph.DECADAL] = {datefield: Dygraph.DATEFIELD_Y, step: 10, spacing: 1000 * 864000 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 10 +Dygraph.TICK_PLACEMENT[Dygraph.CENTENNIAL] = {datefield: Dygraph.DATEFIELD_Y, step: 100, spacing: 1000 * 8640000 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 100 + /** * This is a list of human-friendly values at which to show tick marks on a log @@ -322,7 +300,7 @@ Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; * NOTE: this assumes that Dygraph.LOG_SCALE = 10. * @type {Array.} */ -Dygraph.PREFERRED_LOG_TICK_VALUES = function() { +Dygraph.PREFERRED_LOG_TICK_VALUES = (function() { var vals = []; for (var power = -39; power <= 39; power++) { var range = Math.pow(10, power); @@ -332,7 +310,7 @@ Dygraph.PREFERRED_LOG_TICK_VALUES = function() { } } return vals; -}(); +})(); /** * Determine the correct granularity of ticks on a date axis. @@ -340,8 +318,7 @@ Dygraph.PREFERRED_LOG_TICK_VALUES = function() { * @param {number} a Left edge of the chart (ms) * @param {number} b Right edge of the chart (ms) * @param {number} pixels Size of the chart in the relevant dimension (width). - * @param {function(string):*} opts Function mapping from option name -> - * value. + * @param {function(string):*} opts Function mapping from option name -> value. * @return {number} The appropriate axis granularity for this chart. See the * enumeration of possible values in dygraph-tickers.js. */ @@ -357,32 +334,19 @@ Dygraph.pickDateTickGranularity = function(a, b, pixels, opts) { }; /** + * Compute the number of ticks on a date axis for a given granularity. * @param {number} start_time * @param {number} end_time * @param {number} granularity (one of the granularities enumerated above) - * @return {number} Number of ticks that would result. + * @return {number} (Approximate) number of ticks that would result. */ Dygraph.numDateTicks = function(start_time, end_time, granularity) { - if (granularity < Dygraph.MONTHLY) { - // Generate one tick mark for every fixed interval of time. - var spacing = Dygraph.SHORT_SPACINGS[granularity]; - return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing); - } else { - var year_mod = 1; // e.g. to only print one point every 10 years. - var num_months = 12; - if (granularity == Dygraph.QUARTERLY) num_months = 3; - if (granularity == Dygraph.BIANNUAL) num_months = 2; - if (granularity == Dygraph.ANNUAL) num_months = 1; - if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; } - if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; } - - var msInYear = 365.2524 * 24 * 3600 * 1000; - var num_years = 1.0 * (end_time - start_time) / msInYear; - return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod); - } + var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing; + return Math.round(1.0 * (end_time - start_time) / spacing); }; /** + * Compute the positions and labels of ticks on a date axis for a given granularity. * @param {number} start_time * @param {number} end_time * @param {number} granularity (one of the granularities enumerated above) @@ -393,94 +357,100 @@ Dygraph.numDateTicks = function(start_time, end_time, granularity) { Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) { var formatter = /** @type{AxisLabelFormatter} */( opts("axisLabelFormatter")); - var ticks = []; - var t; - - if (granularity < Dygraph.MONTHLY) { - // Generate one tick mark for every fixed interval of time. - var spacing = Dygraph.SHORT_SPACINGS[granularity]; - - // Find a time less than start_time which occurs on a "nice" time boundary - // for this granularity. - var g = spacing / 1000; - var d = new Date(start_time); - d.setMilliseconds(0); - var x; - if (g <= 60) { // seconds - x = d.getSeconds(); d.setSeconds(x - x % g); - } else { - d.setSeconds(0); - g /= 60; - if (g <= 60) { // minutes - x = d.getMinutes(); d.setMinutes(x - x % g); - } else { - d.setMinutes(0); - g /= 60; - - if (g <= 24) { // days - x = d.getHours(); d.setHours(x - x % g); - } else { - d.setHours(0); - g /= 24; + var utc = opts("labelsUTC"); + var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal; + + var datefield = Dygraph.TICK_PLACEMENT[granularity].datefield; + var step = Dygraph.TICK_PLACEMENT[granularity].step; + var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing; + + // Choose a nice tick position before the initial instant. + // Currently, this code deals properly with the existent daily granularities: + // DAILY (with step of 1) and WEEKLY (with step of 7 but specially handled). + // Other daily granularities (say TWO_DAILY) should also be handled specially + // by setting the start_date_offset to 0. + var start_date = new Date(start_time); + var date_array = []; + date_array[Dygraph.DATEFIELD_Y] = accessors.getFullYear(start_date); + date_array[Dygraph.DATEFIELD_M] = accessors.getMonth(start_date); + date_array[Dygraph.DATEFIELD_D] = accessors.getDate(start_date); + date_array[Dygraph.DATEFIELD_HH] = accessors.getHours(start_date); + date_array[Dygraph.DATEFIELD_MM] = accessors.getMinutes(start_date); + date_array[Dygraph.DATEFIELD_SS] = accessors.getSeconds(start_date); + date_array[Dygraph.DATEFIELD_MS] = accessors.getMilliseconds(start_date); + + var start_date_offset = date_array[datefield] % step; + if (granularity == Dygraph.WEEKLY) { + // This will put the ticks on Sundays. + start_date_offset = accessors.getDay(start_date); + } + + date_array[datefield] -= start_date_offset; + for (var df = datefield + 1; df < Dygraph.NUM_DATEFIELDS; df++) { + // The minimum value is 1 for the day of month, and 0 for all other fields. + date_array[df] = (df === Dygraph.DATEFIELD_D) ? 1 : 0; + } - if (g == 7) { // one week - d.setDate(d.getDate() - d.getDay()); - } - } - } + // Generate the ticks. + // For granularities not coarser than HOURLY we use the fact that: + // the number of milliseconds between ticks is constant + // and equal to the defined spacing. + // Otherwise we rely on the 'roll over' property of the Date functions: + // when some date field is set to a value outside of its logical range, + // the excess 'rolls over' the next (more significant) field. + // However, when using local time with DST transitions, + // there are dates that do not represent any time value at all + // (those in the hour skipped at the 'spring forward'), + // and the JavaScript engines usually return an equivalent value. + // Hence we have to check that the date is properly increased at each step, + // returning a date at a nice tick position. + var ticks = []; + var tick_date = accessors.makeDate.apply(null, date_array); + var tick_time = tick_date.getTime(); + if (granularity <= Dygraph.HOURLY) { + if (tick_time < start_time) { + tick_time += spacing; + tick_date = new Date(tick_time); } - start_time = d.getTime(); - - for (t = start_time; t <= end_time; t += spacing) { - ticks.push({ v:t, - label: formatter(new Date(t), granularity, opts, dg) + while (tick_time <= end_time) { + ticks.push({ v: tick_time, + label: formatter(tick_date, granularity, opts, dg) }); + tick_time += spacing; + tick_date = new Date(tick_time); } } else { - // Display a tick mark on the first of a set of months of each year. - // Years get a tick mark iff y % year_mod == 0. This is useful for - // displaying a tick mark once every 10 years, say, on long time scales. - var months; - var year_mod = 1; // e.g. to only print one point every 10 years. - - if (granularity == Dygraph.MONTHLY) { - months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]; - } else if (granularity == Dygraph.QUARTERLY) { - months = [ 0, 3, 6, 9 ]; - } else if (granularity == Dygraph.BIANNUAL) { - months = [ 0, 6 ]; - } else if (granularity == Dygraph.ANNUAL) { - months = [ 0 ]; - } else if (granularity == Dygraph.DECADAL) { - months = [ 0 ]; - year_mod = 10; - } else if (granularity == Dygraph.CENTENNIAL) { - months = [ 0 ]; - year_mod = 100; - } else { - Dygraph.warn("Span of dates is too long"); + if (tick_time < start_time) { + date_array[datefield] += step; + tick_date = accessors.makeDate.apply(null, date_array); + tick_time = tick_date.getTime(); } - - var start_year = new Date(start_time).getFullYear(); - var end_year = new Date(end_time).getFullYear(); - var zeropad = Dygraph.zeropad; - for (var i = start_year; i <= end_year; i++) { - if (i % year_mod !== 0) continue; - for (var j = 0; j < months.length; j++) { - var date_str = i + "/" + zeropad(1 + months[j]) + "/01"; - t = Dygraph.dateStrToMillis(date_str); - if (t < start_time || t > end_time) continue; - ticks.push({ v:t, - label: formatter(new Date(t), granularity, opts, dg) + while (tick_time <= end_time) { + if (granularity >= Dygraph.DAILY || + accessors.getHours(tick_date) % step === 0) { + ticks.push({ v: tick_time, + label: formatter(tick_date, granularity, opts, dg) }); } + date_array[datefield] += step; + tick_date = accessors.makeDate.apply(null, date_array); + tick_time = tick_date.getTime(); } } - return ticks; }; -// These are set here so that this file can be included after dygraph.js. -Dygraph.DEFAULT_ATTRS['axes']['x']['ticker'] = Dygraph.dateTicker; -Dygraph.DEFAULT_ATTRS['axes']['y']['ticker'] = Dygraph.numericTicks; -Dygraph.DEFAULT_ATTRS['axes']['y2']['ticker'] = Dygraph.numericTicks; +// These are set here so that this file can be included after dygraph.js +// or independently. +if (Dygraph && + Dygraph.DEFAULT_ATTRS && + Dygraph.DEFAULT_ATTRS['axes'] && + Dygraph.DEFAULT_ATTRS['axes']['x'] && + Dygraph.DEFAULT_ATTRS['axes']['y'] && + Dygraph.DEFAULT_ATTRS['axes']['y2']) { + Dygraph.DEFAULT_ATTRS['axes']['x']['ticker'] = Dygraph.dateTicker; + Dygraph.DEFAULT_ATTRS['axes']['y']['ticker'] = Dygraph.numericTicks; + Dygraph.DEFAULT_ATTRS['axes']['y2']['ticker'] = Dygraph.numericTicks; +} + +})();