UNPKG

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
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 };