@future-grid/fgp-graph
Version:
fgp-graph is a chart lib based on Dygraphs
369 lines (327 loc) • 16.1 kB
text/typescript
import Dygraph from 'dygraphs';
import moment from 'moment-timezone';
import {GraphConstant} from '../metadata/configurations';
export class Formatters {
/**
*show graph timestamp with this timezone
* @type {string}
* @memberof Formatters
*/
public timezone: string;
public dateformat?: string;
private TICK_PLACEMENT: any[];
private SHORT_SPACINGS: any[];
/**
*Creates an instance of Formatters.
* @param {string} timezone show graph timestamp with this timezone
* @memberof Formatters
*/
constructor(timezone: string) {
this.timezone = timezone;
const DATEFIELD_Y = 0;
const DATEFIELD_M = 1;
const DATEFIELD_D = 2;
const DATEFIELD_HH = 3;
const DATEFIELD_MM = 4;
const DATEFIELD_SS = 5;
const DATEFIELD_MS = 6;
const NUM_DATEFIELDS = 7;
this.TICK_PLACEMENT = [];
this.TICK_PLACEMENT[GraphConstant.SECONDLY] = {datefield: DATEFIELD_SS, step: 1, spacing: 1000 * 1};
this.TICK_PLACEMENT[GraphConstant.TWO_SECONDLY] = {datefield: DATEFIELD_SS, step: 2, spacing: 1000 * 2};
this.TICK_PLACEMENT[GraphConstant.FIVE_SECONDLY] = {datefield: DATEFIELD_SS, step: 5, spacing: 1000 * 5};
this.TICK_PLACEMENT[GraphConstant.TEN_SECONDLY] = {datefield: DATEFIELD_SS, step: 10, spacing: 1000 * 10};
this.TICK_PLACEMENT[GraphConstant.THIRTY_SECONDLY] = {datefield: DATEFIELD_SS, step: 30, spacing: 1000 * 30};
this.TICK_PLACEMENT[GraphConstant.MINUTELY] = {datefield: DATEFIELD_MM, step: 1, spacing: 1000 * 60};
this.TICK_PLACEMENT[GraphConstant.TWO_MINUTELY] = {datefield: DATEFIELD_MM, step: 2, spacing: 1000 * 60 * 2};
this.TICK_PLACEMENT[GraphConstant.FIVE_MINUTELY] = {datefield: DATEFIELD_MM, step: 5, spacing: 1000 * 60 * 5};
this.TICK_PLACEMENT[GraphConstant.TEN_MINUTELY] = {datefield: DATEFIELD_MM, step: 10, spacing: 1000 * 60 * 10};
this.TICK_PLACEMENT[GraphConstant.THIRTY_MINUTELY] = {
datefield: DATEFIELD_MM,
step: 30,
spacing: 1000 * 60 * 30
};
this.TICK_PLACEMENT[GraphConstant.HOURLY] = {datefield: DATEFIELD_HH, step: 1, spacing: 1000 * 3600};
this.TICK_PLACEMENT[GraphConstant.TWO_HOURLY] = {datefield: DATEFIELD_HH, step: 2, spacing: 1000 * 3600 * 2};
this.TICK_PLACEMENT[GraphConstant.SIX_HOURLY] = {datefield: DATEFIELD_HH, step: 6, spacing: 1000 * 3600 * 6};
this.TICK_PLACEMENT[GraphConstant.DAILY] = {datefield: DATEFIELD_D, step: 1, spacing: 1000 * 86400};
this.TICK_PLACEMENT[GraphConstant.TWO_DAILY] = {datefield: DATEFIELD_D, step: 2, spacing: 1000 * 86400 * 2};
this.TICK_PLACEMENT[GraphConstant.WEEKLY] = {datefield: DATEFIELD_D, step: 7, spacing: 1000 * 604800};
this.TICK_PLACEMENT[GraphConstant.MONTHLY] = {datefield: DATEFIELD_M, step: 1, spacing: 1000 * 7200 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 / 12
this.TICK_PLACEMENT[GraphConstant.QUARTERLY] = {
datefield: DATEFIELD_M,
step: 3,
spacing: 1000 * 21600 * 365.2524
}; // 1e3 * 60 * 60 * 24 * 365.2524 / 4
this.TICK_PLACEMENT[GraphConstant.BIANNUAL] = {
datefield: DATEFIELD_M,
step: 6,
spacing: 1000 * 43200 * 365.2524
}; // 1e3 * 60 * 60 * 24 * 365.2524 / 2
this.TICK_PLACEMENT[GraphConstant.ANNUAL] = {datefield: DATEFIELD_Y, step: 1, spacing: 1000 * 86400 * 365.2524}; // 1e3 * 60 * 60 * 24 * 365.2524 * 1
this.TICK_PLACEMENT[GraphConstant.DECADAL] = {
datefield: DATEFIELD_Y,
step: 10,
spacing: 1000 * 864000 * 365.2524
}; // 1e3 * 60 * 60 * 24 * 365.2524 * 10
this.TICK_PLACEMENT[GraphConstant.CENTENNIAL] = {
datefield: DATEFIELD_Y,
step: 100,
spacing: 1000 * 8640000 * 365.2524
}; // 1e3 * 60 * 60 * 24 * 365.2524 * 100
this.SHORT_SPACINGS = [];
this.SHORT_SPACINGS[GraphConstant.SECONDLY] = 1000 * 1;
this.SHORT_SPACINGS[GraphConstant.TWO_SECONDLY] = 1000 * 2;
this.SHORT_SPACINGS[GraphConstant.FIVE_SECONDLY] = 1000 * 5;
this.SHORT_SPACINGS[GraphConstant.TEN_SECONDLY] = 1000 * 10;
this.SHORT_SPACINGS[GraphConstant.THIRTY_SECONDLY] = 1000 * 30;
this.SHORT_SPACINGS[GraphConstant.MINUTELY] = 1000 * 60;
this.SHORT_SPACINGS[GraphConstant.TWO_MINUTELY] = 1000 * 60 * 2;
this.SHORT_SPACINGS[GraphConstant.FIVE_MINUTELY] = 1000 * 60 * 5;
this.SHORT_SPACINGS[GraphConstant.TEN_MINUTELY] = 1000 * 60 * 10;
this.SHORT_SPACINGS[GraphConstant.THIRTY_MINUTELY] = 1000 * 60 * 30;
this.SHORT_SPACINGS[GraphConstant.HOURLY] = 1000 * 3600;
this.SHORT_SPACINGS[GraphConstant.TWO_HOURLY] = 1000 * 3600 * 2;
this.SHORT_SPACINGS[GraphConstant.SIX_HOURLY] = 1000 * 3600 * 6;
this.SHORT_SPACINGS[GraphConstant.DAILY] = 1000 * 86400;
this.SHORT_SPACINGS[GraphConstant.WEEKLY] = 1000 * 604800;
this.SHORT_SPACINGS[GraphConstant.TWO_DAILY] = 1000 * 86400 * 2;
}
/**
* update date format for legend and range-bar
* @param format
*/
public setFormat = (format: string) => {
this.dateformat = format;
};
private numDateTicks = (start_time: number, end_time: number, granularity: number) => {
const spacing = this.TICK_PLACEMENT[granularity].spacing;
return Math.round(1.0 * (end_time - start_time) / spacing);
};
private pickDateTickGranularity = (a: any, b: any, pixels: any, opts: any) => {
let pixels_per_tick = opts('pixelsPerLabel');
for (let i = 0; i < 21; i++) {
let num_ticks = this.numDateTicks(a, b, i);
if (pixels / num_ticks >= pixels_per_tick) {
return i;
}
}
return -1;
};
private zeropad = (x: number) => {
if (x < 10) return "0" + x; else return "" + x;
};
private getDateAxis = (start: any, end: any, granularity: any, opts: any, dygraph: Dygraph) => {
//
let formatter = /** @type{AxisLabelFormatter} */(
opts("axisLabelFormatter"));
let ticks = [];
let t;
if (granularity < GraphConstant.MONTHLY) {
// Generate one tick mark for every fixed interval of time.
let spacing = this.SHORT_SPACINGS[granularity];
// Find a time less than start_time which occurs on a "nice" time boundary
// for this granularity.
let g = spacing / 1000;
let d = moment(start).tz(this.timezone ? this.timezone : moment.tz.guess());
d.millisecond(0);
let x;
if (g <= 60) { // seconds
x = d.second();
d.second(x - x % g);
} else {
d.second(0);
g /= 60;
if (g <= 60) { // minutes
x = d.minute();
d.minute(x - x % g);
} else {
d.minute(0);
g /= 60;
if (g <= 24) { // days
x = d.hour();
d.hour(x - x % g);
} else {
d.hour(0);
g /= 24;
if (g == 7) { // one week
d.startOf('week');
}
}
}
}
start = d.valueOf();
let start_offset_min = moment(start).tz(this.timezone ? this.timezone : moment.tz.guess()).utcOffset();
let check_dst = (spacing >= this.SHORT_SPACINGS[GraphConstant.TWO_HOURLY]);
for (t = start; t <= end; t += spacing) {
let d = moment(t).tz(this.timezone ? this.timezone : moment.tz.guess());
// console.info(check_dst , d.utcOffset() , start_offset_min);
if (check_dst && d.utcOffset() != start_offset_min) {
let delta_min = -(d.utcOffset() - start_offset_min);
t += delta_min * 60 * 1000;
d = moment(t).tz(this.timezone ? this.timezone : moment.tz.guess());
start_offset_min = d.utcOffset();
// Check whether we've backed into the previous timezone again.
// This can happen during a "day light" transition. In this case,
// it's best to skip this tick altogether (we may be shooting for a
// non-existent time like the 2AM that's skipped) and go to the next
// one.
if (moment(t + spacing).tz(this.timezone ? this.timezone : moment.tz.guess()).utcOffset() != start_offset_min) {
t += spacing;
d = moment(t).tz(this.timezone ? this.timezone : moment.tz.guess());
start_offset_min = d.utcOffset();
}
}
ticks.push({
v: t,
label: formatter(d, granularity, opts, dygraph)
});
}
} 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.
let months: number[] = [];
let year_mod = 1; // e.g. to only print one point every 10 years.
if (granularity == GraphConstant.MONTHLY) {
months = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
} else if (granularity == GraphConstant.QUARTERLY) {
months = [0, 3, 6, 9];
} else if (granularity == GraphConstant.BIANNUAL) {
months = [0, 6];
} else if (granularity == GraphConstant.ANNUAL) {
months = [0];
} else if (granularity == GraphConstant.DECADAL) {
months = [0];
year_mod = 10;
} else if (granularity == GraphConstant.CENTENNIAL) {
months = [0];
year_mod = 100;
} else {
console.warn("Span of dates is too long");
}
let start_year = moment(start).tz(this.timezone ? this.timezone : moment.tz.guess()).year();
let end_year = moment(end).tz(this.timezone ? this.timezone : moment.tz.guess()).year();
for (let i = start_year; i <= end_year; i++) {
if (i % year_mod !== 0) continue;
for (let j = 0; j < months.length; j++) {
let dt = moment.tz(new Date(i, months[j], 1), this.timezone ? this.timezone : moment.tz.guess());
dt.year(i);
t = dt.valueOf();
if (t < start || t > end) continue;
ticks.push({
v: t,
label: formatter(moment(t).tz(this.timezone ? this.timezone : moment.tz.guess()), granularity, opts, dygraph)
});
}
}
}
return ticks;
};
DateTickerTZ = (a: any, b: any, pixels: any, opts: any, dygraph: Dygraph, vals: any) => {
let granularity = this.pickDateTickGranularity(a, b, pixels, opts);
if (granularity >= 0) {
return this.getDateAxis(a, b, granularity, opts, dygraph); // use own function here
} else {
// this can happen if self.width_ is zero.
return [];
}
};
/**
*
* legend formatter for multiple series
* @param {any} data this data comes from graph
*
* @memberof Formatters
*/
legendForAllSeries = (data: any) => {
const g = data.dygraph;
if (g.getOption('showLabelsOnHighlight') !== true) return '';
if (data.x == null) {
// This happens when there's no selection and {legend: 'always'} is set.
return '<br>' + data.series.map(function (series: any) {
return series.dashHTML + ' ' + series.labelHTML
}).join('<br>');
}
let html = moment.tz(data.x, this.timezone ? this.timezone : moment.tz.guess()).format(this.dateformat ? this.dateformat : 'lll z');
data.series.forEach(function (series: any) {
if (!series.isVisible || series.label.indexOf('_markline') != -1) return;
let labeledData = series.labelHTML + ': ' + (series.yHTML ? series.yHTML : "");
if (series.isHighlighted) {
labeledData = '<b style="color:' + series.color + ';">' + labeledData + '</b>';
}
html += '<br>' + series.dashHTML + ' ' + labeledData;
});
return html;
};
/**
*
* legend formatter for single series
* @param {any} data this data comes from graph
*
* @memberof Formatters
*/
legendForSingleSeries = (data: any) => {
const g = data.dygraph;
if (g.getOption('showLabelsOnHighlight') !== true) return '';
if (data.x == null) {
// This happens when there's no selection and {legend: 'always'} is set.
return '<br>' + data.series.map(function (series: any) {
return series.dashHTML + ' ' + series.labelHTML
}).join('<br>');
}
let html = moment.tz(data.x, this.timezone ? this.timezone : moment.tz.guess()).format(this.dateformat ? this.dateformat : 'lll z');
data.series.forEach(function (series: any) {
if (!series.isVisible || series.label.indexOf('_markline') != -1) return;
let labeledData = series.labelHTML + ': ' + (series.yHTML ? series.yHTML : "");
if (series.isHighlighted) {
labeledData = '<b style="color:' + series.color + ';">' + labeledData + '</b>';
html += '<br>' + series.dashHTML + ' ' + labeledData;
}
});
return html;
}
/**
*formatter for axis label
* @param {number|date} d
* @param {number} granularity
* @param {function} opts
* @param {Dygraph} dygraph
* @returns {string}
* @memberof Formatters
*/
axisLabel = (d: number | Date, granularity: number, opts?: (name: string) => any, dygraph?: Dygraph): any => {
// don't put it into formatters.ts becault we need to timezone later
let momentDatetime;
if (d instanceof Date) {
momentDatetime = moment.tz(d.getTime(), this.timezone ? this.timezone : moment.tz.guess());
} else {
momentDatetime = moment.tz(d, this.timezone ? this.timezone : moment.tz.guess());
}
let SHORT_MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
let zeropad = (x: number) => {
if (x < 10) return "0" + x;
else return "" + x;
};
let hmsString_ = (hh: number, mm: number, ss: number) => {
let ret = zeropad(hh) + ":" + zeropad(mm);
if (ss) {
ret += ":" + zeropad(ss);
}
return ret;
};
if (granularity >= Dygraph.DECADAL) {
return '' + momentDatetime.year();
} else if (granularity >= Dygraph.MONTHLY) {
return SHORT_MONTH_NAMES[momentDatetime.month()] + ' ' + momentDatetime.year();
} else {
let frac = momentDatetime.hours() * 3600 + momentDatetime.minutes() * 60 + momentDatetime.seconds() + 1e-3 * momentDatetime.milliseconds();
if (frac === 0 || granularity >= Dygraph.DAILY) {
// e.g. '21 Jan' (%d%b)
return zeropad(momentDatetime.date()) + ' ' + SHORT_MONTH_NAMES[momentDatetime.month()];
} else {
return hmsString_(momentDatetime.hours(), momentDatetime.minutes(), momentDatetime.seconds());
}
}
}
}