horizon-timeseries-chart
Version:
A chart to represent time-series data using multiple vertically layered horizon area plots
486 lines (471 loc) • 17.1 kB
JavaScript
import { select } from 'd3-selection';
import { transition } from 'd3-transition';
import { zoom, zoomTransform } from 'd3-zoom';
import { axisBottom } from 'd3-axis';
import { scaleUtc, scaleTime } from 'd3-scale';
import { curveBasis } from 'd3-shape';
import { extent, max } from 'd3-array';
import Horizon from 'd3-horizon';
import Kapsule from 'kapsule';
import accessorFn from 'accessor-fn';
import indexBy from 'index-array-by';
import memo from 'lodash.memoize';
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear, utcSecond, utcMinute, utcHour, utcDay, utcWeek, utcMonth, utcYear } from 'd3-time';
import { utcFormat, timeFormat } from 'd3-time-format';
function styleInject(css, ref) {
if (ref === undefined) ref = {};
var insertAt = ref.insertAt;
if (typeof document === 'undefined') {
return;
}
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".horizon-series {\n position: relative;\n border: 0 solid darkgray;\n}\n\n.horizon-series:hover {\n border-top: 1px solid indianred;\n border-bottom: 1px solid indianred;\n}\n\n.horizon-series .label {\n position: absolute;\n bottom: 0;\n left: 4px;\n font-family: sans-serif;\n text-shadow: 0 1px 0 rgba(255, 255, 255, .5);\n pointer-events: none;\n}\n\n.horizon-series .chart .float-tooltip-kap {\n z-index: 1000;\n}\n\n.horizon-ruler {\n position: absolute;\n background: black;\n opacity: 0;\n top: 0;\n bottom: 0;\n width: 1px;\n pointer-events: none;\n transition: opacity 0.5s;\n z-index: 100;\n}";
styleInject(css_248z);
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _objectWithoutProperties(e, t) {
if (null == e) return {};
var o,
r,
i = _objectWithoutPropertiesLoose(e, t);
if (Object.getOwnPropertySymbols) {
var s = Object.getOwnPropertySymbols(e);
for (r = 0; r < s.length; r++) o = s[r], t.includes(o) || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
}
return i;
}
function _objectWithoutPropertiesLoose(r, e) {
if (null == r) return {};
var t = {};
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
if (e.includes(n)) continue;
t[n] = r[n];
}
return t;
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (undefined !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : undefined;
}
}
var local = {
second: timeSecond,
minute: timeMinute,
hour: timeHour,
day: timeDay,
week: timeWeek,
month: timeMonth,
year: timeYear
};
var utc = {
second: utcSecond,
minute: utcMinute,
hour: utcHour,
day: utcDay,
week: utcWeek,
month: utcMonth,
year: utcYear
};
var formats = {
millisecond: function millisecond() {
return '.%L';
},
second: function second() {
return ':%S';
},
minute: function minute(_ref) {
var use24h = _ref.use24h;
return "%".concat(use24h ? 'H' : 'I', ":%M");
},
hour: function hour(_ref2) {
var use24h = _ref2.use24h;
return use24h ? '%H:%M' : '%I %p';
},
day: function day() {
return '%a %d';
},
week: function week() {
return '%b %d';
},
month: function month() {
return '%b';
},
year: function year() {
return '%Y';
}
};
function axisTimeFormatter (_ref3) {
var _ref3$useUtc = _ref3.useUtc,
useUtc = _ref3$useUtc === undefined ? false : _ref3$useUtc,
_ref3$use24h = _ref3.use24h,
use24h = _ref3$use24h === undefined ? false : _ref3$use24h;
var formatOpts = {
use24h: use24h
};
var formatFn = useUtc ? utcFormat : timeFormat;
var f = Object.assign.apply(Object, [{}].concat(_toConsumableArray(Object.entries(formats).map(function (_ref4) {
var _ref5 = _slicedToArray(_ref4, 2),
unit = _ref5[0],
fn = _ref5[1];
return _defineProperty({}, unit, formatFn(fn(formatOpts)));
}))));
var t = useUtc ? utc : local;
return function (date) {
return (t.second(date) < date ? f.millisecond : t.minute(date) < date ? f.second : t.hour(date) < date ? f.minute : t.day(date) < date ? f.hour : t.month(date) < date ? t.week(date) < date ? f.day : f.week : t.year(date) < date ? f.month : f.year)(date);
};
}
var _excluded = ["x", "y"];
var AXIS_HEIGHT = 20;
var MAX_FONT_SIZE = 13;
var MIN_SERIES_HEIGHT_WITH_BORDER = 20;
var MAX_ZOOM_SCALE = 5000;
var horizonChart = Kapsule({
props: {
width: {
"default": window.innerWidth
},
height: {
"default": window.innerHeight
},
data: {
"default": []
},
ts: {
"default": 'ts'
},
val: {
"default": 'val'
},
series: {},
seriesComparator: {
"default": function _default(a, b) {
return a.localeCompare(b);
}
},
horizonBands: {
"default": 4
},
horizonMode: {
"default": 'offset'
},
// or mirror
useUtc: {
"default": false
},
// local timezone vs utc
use24h: {
"default": true
},
// 24h vs am/pm format
yExtent: {},
// undefined means it will be derived dynamically from the data
yNormalize: {
"default": false
},
yScaleExp: {
"default": 1
},
yAggregation: {
"default": function _default(vals) {
return vals.reduce(function (agg, val) {
return agg + val;
});
}
},
// sum reduce
positiveColors: {
"default": ['white', 'midnightblue']
},
negativeColors: {
"default": ['white', 'crimson']
},
positiveColorStops: {},
negativeColorStops: {},
interpolationCurve: {
"default": curveBasis
},
seriesLabelFormatter: {
"default": function _default(series) {
return series;
}
},
showRuler: {
"default": true,
triggerUpdate: false,
onChange: function onChange(show, state) {
return state.ruler && state.ruler.style('visibility', show ? 'visible' : 'hidden');
}
},
enableZoom: {
"default": false
},
tooltipContent: {
"default": function _default(_ref) {
var series = _ref.series,
ts = _ref.ts,
val = _ref.val,
useUtc = _ref.useUtc,
use24h = _ref.use24h;
return "<b>".concat(series, "</b><br>\n ").concat(new Date(ts).toLocaleString(undefined, {
timeZone: useUtc ? 'UTC' : undefined,
hour12: !use24h
}), ":\n <b>").concat(val, "</b>");
}
},
transitionDuration: {
"default": 250
},
onHover: {
triggerUpdate: false
},
onClick: {}
},
stateInit: function stateInit() {
return {
horizonLayouts: {},
// per series
zoomedInteraction: false,
timeAxis: axisBottom()
};
},
init: function init(el, state) {
var isD3Selection = !!el && _typeof(el) === 'object' && !!el.node && typeof el.node === 'function';
var d3El = select(isD3Selection ? el.node() : el);
d3El.html(null); // wipe DOM
state.chartsEl = d3El.append('div').style('position', 'relative');
state.ruler = state.chartsEl.append('div').attr('class', 'horizon-ruler');
state.axisEl = d3El.append('svg').attr('height', AXIS_HEIGHT).style('display', 'block');
// Bind zoom
state.chartsEl.call(state.zoom = zoom());
state.zoom.__baseElem = state.chartsEl; // Attach controlling elem for easy access
state.zoom.on('zoom', function () {
if (state.enableZoom) {
state.zoomedInteraction = true;
state._rerender();
}
});
},
update: function update(state) {
var valAccessor = accessorFn(state.val);
var yExtentAccessor = accessorFn(state.yExtent);
var yScaleExpAccessor = accessorFn(state.yScaleExp);
var positiveColorsAccessor = accessorFn(state.positiveColors);
var negativeColorsAccessor = accessorFn(state.negativeColors);
var positiveColorStopsAccessor = accessorFn(state.positiveColorStops);
var negativeColorStopsAccessor = accessorFn(state.negativeColorStops);
// memoize to prevent calling timeAccessor multiple times
var tsMemo = memo(accessorFn(state.ts));
var times = state.data.map(tsMemo);
var timeScale = (state.useUtc ? scaleUtc : scaleTime)().domain(extent(times)).range([0, state.width]);
// const timeAxis = d3AxisBottom(timeScale)
state.timeAxis.scale(timeScale).tickFormat(axisTimeFormatter({
useUtc: state.useUtc,
use24h: state.use24h
}));
// set scale extent to 1 if zoom is disabled, to allow default wheel events to bubble
state.zoom.scaleExtent([1, state.enableZoom ? MAX_ZOOM_SCALE : 1]);
if (state.enableZoom) {
state.zoom.translateExtent([[0, 0], [state.width, 0]]);
// apply zoom
var zoomTransform$1 = zoomTransform(state.zoom.__baseElem.node());
var zoomedTimeLength = (timeScale.domain()[1] - timeScale.domain()[0]) / zoomTransform$1.k;
var zoomedStartTime = timeScale.invert(-zoomTransform$1.x / zoomTransform$1.k);
timeScale.domain([zoomedStartTime, new Date(+zoomedStartTime + zoomedTimeLength)]);
}
var bySeries = indexBy(state.data, state.series);
var seriesData = Object.keys(bySeries).sort(state.seriesComparator).map(function (series) {
return {
series: series,
data: bySeries[series]
};
});
// Calculate global Y max if yNormalize is on
var normalizedYMax = !state.yNormalize ? null : max(indexBy(state.data, [state.series, function (d) {
return +tsMemo(d);
}], function (points) {
return state.yAggregation(points.map(valAccessor));
}, true).map(function (d) {
return d.vals;
}).map(Math.abs) // get max absolute val
);
var seriesHeight = (state.height - AXIS_HEIGHT) / seriesData.length;
var fontSize = Math.min(MAX_FONT_SIZE, Math.round(seriesHeight * 0.9));
var borderWidth = seriesHeight >= MIN_SERIES_HEIGHT_WITH_BORDER ? 1 : 0;
var tr = transition().duration(state.zoomedInteraction ? 0 : state.transitionDuration);
var horizons = state.chartsEl.selectAll('.horizon-series').data(seriesData, function (d) {
return d.series;
});
horizons.exit().each(function (_ref2) {
var series = _ref2.series;
state.horizonLayouts[series].height(0);
delete state.horizonLayouts[series];
}).transition(tr).remove();
var newHorizons = horizons.enter().append('div').attr('class', 'horizon-series');
newHorizons.append('div').attr('class', 'chart').each(function (_ref3) {
var series = _ref3.series;
// instantiate horizon for each new series
state.horizonLayouts[series] = new Horizon(this).width(state.width).height(seriesHeight - borderWidth);
});
newHorizons.append('span').attr('class', 'label').style('font-size', 0);
var allHorizons = horizons.merge(newHorizons).order();
allHorizons.transition(tr).style('width', state.width).style('border-top-width', "".concat(borderWidth, "px"));
allHorizons.select('.chart').each(function (_ref4) {
var series = _ref4.series,
data = _ref4.data;
return state.horizonLayouts[series].width(state.width).height(seriesHeight - borderWidth).data(data).bands(state.horizonBands).mode(state.horizonMode).x(tsMemo).y(valAccessor).yExtent(yExtentAccessor(series) || normalizedYMax || null) // Use normalizedYMax (if calculated) if yExtent is not explicitly set
.yScaleExp(yScaleExpAccessor(series)).yAggregation(state.yAggregation).xMin(timeScale.domain()[0]).xMax(timeScale.domain()[1]).positiveColors(positiveColorsAccessor(series)).negativeColors(negativeColorsAccessor(series)).positiveColorStops(positiveColorStopsAccessor(series)).negativeColorStops(negativeColorStopsAccessor(series)).interpolationCurve(state.interpolationCurve).duration(state.transitionDuration).tooltipContent(state.tooltipContent && function (_ref5) {
var x = _ref5.x,
y = _ref5.y,
rest = _objectWithoutProperties(_ref5, _excluded);
return state.tooltipContent(_objectSpread2(_objectSpread2({
series: series,
ts: x,
val: y
}, rest), {}, {
useUtc: state.useUtc,
use24h: state.use24h
}));
}).onHover(function (d) {
d && state.ruler.style('left', "".concat(timeScale(d.x), "px"));
state.ruler.style('opacity', d ? 0.2 : 0);
d && state.onHover && state.onHover({
series: series,
ts: d.x,
val: d.y,
points: d.points
});
}).onClick(state.onClick ? function (d) {
return d && state.onClick({
series: series,
ts: d.x,
val: d.y,
points: d.points
});
} : undefined);
});
allHorizons.select('.label').transition(tr).style('font-size', "".concat(fontSize, "px")).text(function (d) {
return state.seriesLabelFormatter(d.series);
});
state.axisEl.call(state.timeAxis).transition(tr).attr('width', state.width);
state.zoomedInteraction = false;
}
});
export { horizonChart as default };