Created
August 12, 2021 19:07
-
-
Save trickeyone/6d6394b73435b5a5a39f00c6bcfb03d6 to your computer and use it in GitHub Desktop.
Fix for Flot time tick generation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const $ = require('jquery'); | |
const floorInBase = $.plot.saturated.floorInBase; | |
const CreateMicroSecondDate = function(dateType, microEpoch) { | |
const newDate = new dateType(microEpoch); | |
const oldSetTime = dateType.prototype.setTime.bind(newDate); | |
const newMethods = { | |
update: function(microtime) { | |
oldSetTime(microtime); | |
// Microseconds are stored as integers | |
const seconds = microtime/1000; | |
this.microseconds = 1000000 * (seconds - Math.floor(seconds)); | |
}, | |
setTime: function(microtime) { | |
this.update(microtime); | |
}, | |
getMicroseconds: function() { | |
return this.microseconds; | |
}, | |
setMicroseconds: function(microseconds) { | |
// Replace the microsecond part (6 last digits) in microEpoch | |
const epochWithoutMicroseconds = 1000 * Math.floor(this.getTime() / 1000); | |
const newEpoch = epochWithoutMicroseconds + microseconds / 1000; | |
this.setTime(newEpoch); | |
}, | |
setUTCMicroseconds: function(microseconds) { this.setMicroseconds(microseconds); }, | |
getUTCMicroseconds: function() { return this.getMicroseconds(); } | |
}; | |
Object.defineProperties( | |
newDate, | |
{ | |
microseconds: { | |
value: null, | |
writable: true | |
} | |
} | |
); | |
Object.keys(newMethods) | |
.forEach(name => { | |
const newMethod = newMethods[name].bind(newDate); | |
Object.defineProperty(newDate, name, {value: newMethod}); | |
}); | |
newDate.setTime(microEpoch); | |
return newDate; | |
}; | |
function makeUtcWrapper(d) { | |
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { | |
sourceObj[sourceMethod] = function() { | |
return targetObj[targetMethod].apply(targetObj, arguments); | |
}; | |
} | |
const utc = { | |
date: d | |
}; | |
// support strftime, if found | |
if (d.strftime !== undefined) { | |
addProxyMethod(utc, "strftime", d, "strftime"); | |
} | |
addProxyMethod(utc, "getTime", d, "getTime"); | |
addProxyMethod(utc, "setTime", d, "setTime"); | |
let props = ["Date", "Day", "FullYear", "Hours", "Minutes", "Month", "Seconds", "Milliseconds", "Microseconds"]; | |
for (let p = 0; p < props.length; p++) { | |
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); | |
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); | |
} | |
return utc; | |
} | |
// select time zone strategy. This returns a date-like object tied to the | |
// desired timezone | |
export function dateGenerator(ts, opts) { | |
let maxDateValue = 8640000000000000; | |
if (opts && opts.timeBase === 'seconds') { | |
ts *= 1000; | |
} else if (opts.timeBase === 'microseconds') { | |
ts /= 1000; | |
} | |
if (ts > maxDateValue) { | |
ts = maxDateValue; | |
} else if (ts < -maxDateValue) { | |
ts = -maxDateValue; | |
} | |
if (opts.timezone === "browser") { | |
return CreateMicroSecondDate(Date, ts); | |
} else if (!opts.timezone || opts.timezone === "utc") { | |
return makeUtcWrapper(CreateMicroSecondDate(Date, ts)); | |
} else if (typeof timezoneJS !== "undefined" && typeof timezoneJS.Date !== "undefined") { | |
let d = CreateMicroSecondDate(timezoneJS.Date, ts); | |
// timezone-js is fickle, so be sure to set the time zone before | |
// setting the time. | |
d.setTimezone(opts.timezone); | |
d.setTime(ts); | |
return d; | |
} else { | |
return makeUtcWrapper(CreateMicroSecondDate(Date, ts)); | |
} | |
} | |
// map of app. size of time units in seconds | |
let timeUnitSizeSeconds = { | |
"microsecond": 0.000001, | |
"millisecond": 0.001, | |
"second": 1, | |
"minute": 60, | |
"hour": 60 * 60, | |
"day": 24 * 60 * 60, | |
"month": 30 * 24 * 60 * 60, | |
"quarter": 3 * 30 * 24 * 60 * 60, | |
"year": 365.2425 * 24 * 60 * 60 | |
}; | |
// map of app. size of time units in milliseconds | |
let timeUnitSizeMilliseconds = { | |
"microsecond": 0.001, | |
"millisecond": 1, | |
"second": 1000, | |
"minute": 60 * 1000, | |
"hour": 60 * 60 * 1000, | |
"day": 24 * 60 * 60 * 1000, | |
"month": 30 * 24 * 60 * 60 * 1000, | |
"quarter": 3 * 30 * 24 * 60 * 60 * 1000, | |
"year": 365.2425 * 24 * 60 * 60 * 1000 | |
}; | |
// map of app. size of time units in microseconds | |
let timeUnitSizeMicroseconds = { | |
"microsecond": 1, | |
"millisecond": 1000, | |
"second": 1000000, | |
"minute": 60 * 1000000, | |
"hour": 60 * 60 * 1000000, | |
"day": 24 * 60 * 60 * 1000000, | |
"month": 30 * 24 * 60 * 60 * 1000000, | |
"quarter": 3 * 30 * 24 * 60 * 60 * 1000000, | |
"year": 365.2425 * 24 * 60 * 60 * 1000000 | |
}; | |
// the allowed tick sizes, after 1 year we use | |
// an integer algorithm | |
let baseSpec = [ | |
[1, "microsecond"], [2, "microsecond"], [5, "microsecond"], [10, "microsecond"], | |
[25, "microsecond"], [50, "microsecond"], [100, "microsecond"], [250, "microsecond"], [500, "microsecond"], | |
[1, "millisecond"], [2, "millisecond"], [5, "millisecond"], [10, "millisecond"], | |
[25, "millisecond"], [50, "millisecond"], [100, "millisecond"], [250, "millisecond"], [500, "millisecond"], | |
[1, "second"], [2, "second"], [5, "second"], [10, "second"], | |
[30, "second"], | |
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], | |
[30, "minute"], | |
[1, "hour"], [2, "hour"], [4, "hour"], | |
[8, "hour"], [12, "hour"], | |
[1, "day"], [2, "day"], [3, "day"], | |
[0.25, "month"], [0.5, "month"], [1, "month"], | |
[2, "month"] | |
]; | |
// we don't know which variant(s) we'll need yet, but generating both is | |
// cheap | |
let specMonths = baseSpec.concat([[3, "month"], [6, "month"], | |
[1, "year"]]); | |
let specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], | |
[1, "year"]]); | |
export function dateTickGenerator(axis) { | |
let opts = axis.options, | |
ticks = [], | |
d = dateGenerator(axis.min, opts), | |
minSize = 0; | |
// make quarter use a possibility if quarters are | |
// mentioned in either of these options | |
let spec = (opts.tickSize && opts.tickSize[1] === | |
"quarter") || | |
(opts.minTickSize && opts.minTickSize[1] === | |
"quarter") ? specQuarters : specMonths; | |
let timeUnitSize; | |
if (opts.timeBase === 'seconds') { | |
timeUnitSize = timeUnitSizeSeconds; | |
} else if (opts.timeBase === 'microseconds') { | |
timeUnitSize = timeUnitSizeMicroseconds; | |
} else { | |
timeUnitSize = timeUnitSizeMilliseconds; | |
} | |
if (opts.minTickSize !== null && opts.minTickSize !== undefined) { | |
if (typeof opts.tickSize === "number") { | |
minSize = opts.tickSize; | |
} else { | |
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; | |
} | |
} | |
let i; | |
for (i = 0; i < spec.length - 1; ++i) { | |
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + | |
spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && | |
spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { | |
break; | |
} | |
} | |
let size = spec[i][0]; | |
let unit = spec[i][1]; | |
// special-case the possibility of several years | |
if (unit === "year") { | |
// if given a minTickSize in years, just use it, | |
// ensuring that it's an integer | |
if (opts.minTickSize !== null && opts.minTickSize !== undefined && opts.minTickSize[1] === "year") { | |
size = Math.floor(opts.minTickSize[0]); | |
} else { | |
let magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); | |
let norm = (axis.delta / timeUnitSize.year) / magn; | |
if (norm < 1.5) { | |
size = 1; | |
} else if (norm < 3) { | |
size = 2; | |
} else if (norm < 7.5) { | |
size = 5; | |
} else { | |
size = 10; | |
} | |
size *= magn; | |
} | |
// minimum size for years is 1 | |
if (size < 1) { | |
size = 1; | |
} | |
} | |
axis.tickSize = opts.tickSize || [size, unit]; | |
let tickSize = axis.tickSize[0]; | |
unit = axis.tickSize[1]; | |
let step = tickSize * timeUnitSize[unit]; | |
if (unit === "microsecond") { | |
d.setMicroseconds(floorInBase(d.getMicroseconds(), tickSize)); | |
} else if (unit === "millisecond") { | |
d.setMilliseconds(floorInBase(d.getMilliseconds(), tickSize)); | |
} else if (unit === "second") { | |
d.setSeconds(floorInBase(d.getSeconds(), tickSize)); | |
} else if (unit === "minute") { | |
d.setMinutes(floorInBase(d.getMinutes(), tickSize)); | |
} else if (unit === "hour") { | |
d.setHours(floorInBase(d.getHours(), tickSize)); | |
} else if (unit === "month") { | |
d.setMonth(floorInBase(d.getMonth(), tickSize)); | |
} else if (unit === "quarter") { | |
d.setMonth(3 * floorInBase(d.getMonth() / 3, | |
tickSize)); | |
} else if (unit === "year") { | |
d.setFullYear(floorInBase(d.getFullYear(), tickSize)); | |
} | |
// reset smaller components | |
if (step >= timeUnitSize.millisecond) { | |
if (step >= timeUnitSize.second) { | |
d.setMicroseconds(0); | |
} else { | |
d.setMicroseconds(d.getMilliseconds()*1000); | |
} | |
} | |
if (step >= timeUnitSize.second) { | |
d.setMilliseconds(0); | |
} | |
if (step >= timeUnitSize.minute) { | |
d.setSeconds(0); | |
} | |
if (step >= timeUnitSize.hour) { | |
d.setMinutes(0); | |
} | |
if (step >= timeUnitSize.day) { | |
d.setHours(0); | |
} | |
if (step >= timeUnitSize.day * 4) { | |
d.setDate(1); | |
} | |
if (step >= timeUnitSize.month * 2) { | |
d.setMonth(floorInBase(d.getMonth(), 3)); | |
} | |
if (step >= timeUnitSize.quarter * 2) { | |
d.setMonth(floorInBase(d.getMonth(), 6)); | |
} | |
if (step >= timeUnitSize.year) { | |
d.setMonth(0); | |
} | |
let carry = 0; | |
let v = Number.NaN; | |
let v1000; | |
let prev; | |
do { | |
prev = v; | |
v1000 = d.getTime(); | |
if (opts && opts.timeBase === 'seconds') { | |
v = v1000 / 1000; | |
} else if (opts && opts.timeBase === 'microseconds') { | |
v = v1000 * 1000; | |
} else { | |
v = v1000; | |
} | |
ticks.push(v); | |
if (unit === "month" || unit === "quarter") { | |
if (tickSize < 1) { | |
// a bit complicated - we'll divide the | |
// month/quarter up but we need to take | |
// care of fractions so we don't end up in | |
// the middle of a day | |
d.setDate(1); | |
let start = d.getTime(); | |
d.setMonth(d.getMonth() + | |
(unit === "quarter" ? 3 : 1)); | |
let end = d.getTime(); | |
d.setTime((v + carry * timeUnitSize.hour + (end - start) * tickSize)); | |
carry = d.getHours(); | |
d.setHours(0); | |
} else { | |
d.setMonth(d.getMonth() + tickSize * (unit === "quarter" ? 3 : 1)); | |
} | |
} else if (unit === "year") { | |
d.setFullYear(d.getFullYear() + tickSize); | |
} else { | |
if (opts.timeBase === 'seconds') { | |
d.setTime((v + step) * 1000); | |
} else if (opts.timeBase === 'microseconds') { | |
d.setTime((v + step) / 1000); | |
} else { | |
d.setTime(v + step); | |
} | |
} | |
} while (v < axis.max && v !== prev); | |
return ticks; | |
} | |
$.plot.dateGenerator = dateGenerator; | |
$.plot.dateTickGenerator = dateTickGenerator; | |
$.plot.plugins.push({ | |
name: 'time-fixer', | |
version: '1.0', | |
init: function(plot) { | |
plot.hooks.processOptions.push(function (plot) { | |
$.each(plot.getAxes(), function(axisName, axis) { | |
const opts = axis.options; | |
if (opts.mode === "time") { | |
axis.tickGenerator = dateTickGenerator; | |
} | |
}); | |
}); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment