UNPKG

react-timeseries-charts

Version:
882 lines (773 loc) 36.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /** * Copyright (c) 2015-present, The Regents of the University of California, * through Lawrence Berkeley National Laboratory (subject to receipt * of any required approvals from the U.S. Dept. of Energy). * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ var _underscore = require("underscore"); var _underscore2 = _interopRequireDefault(_underscore); var _merge = require("merge"); var _merge2 = _interopRequireDefault(_merge); var _react = require("react"); var _react2 = _interopRequireDefault(_react); var _propTypes = require("prop-types"); var _propTypes2 = _interopRequireDefault(_propTypes); var _pondjs = require("pondjs"); var _EventMarker = require("./EventMarker"); var _EventMarker2 = _interopRequireDefault(_EventMarker); var _styler = require("../js/styler"); var _util = require("../js/util"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var defaultFillStyle = { fill: "steelblue", stroke: "none" }; var defaultMutedStyle = { fill: "grey", stroke: "none" }; var defaultStyle = [{ normal: _extends({}, defaultFillStyle, { opacity: 0.2 }), highlighted: _extends({}, defaultFillStyle, { opacity: 0.3 }), selected: _extends({}, defaultFillStyle, { opacity: 0.3 }), muted: _extends({}, defaultMutedStyle, { opacity: 0.1 }) }, { normal: _extends({}, defaultFillStyle, { opacity: 0.5 }), highlighted: _extends({}, defaultFillStyle, { opacity: 0.6 }), selected: _extends({}, defaultFillStyle, { opacity: 0.6 }), muted: _extends({}, defaultMutedStyle, { opacity: 0.2 }) }, { normal: _extends({}, defaultFillStyle, { opacity: 0.9 }), highlighted: _extends({}, defaultFillStyle, { opacity: 1.0 }), selected: _extends({}, defaultFillStyle, { opacity: 1.0 }), muted: _extends({}, defaultMutedStyle, { opacity: 0.2 }) }]; var defaultAggregation = { size: "5m", reducers: { outer: [(0, _pondjs.min)(), (0, _pondjs.max)()], inner: [(0, _pondjs.percentile)(25), (0, _pondjs.percentile)(75)], center: (0, _pondjs.median)() } }; function getSeries(series, column) { return series.map(function (e) { var v = e.get(column); var d = {}; switch (v.length) { case 1: d.center = v[0]; break; case 2: d.innerMin = v[0]; d.innerMax = v[1]; break; case 3: d.innerMin = v[0]; d.center = v[1]; d.innerMax = v[2]; break; case 4: d.outerMin = v[0]; d.innerMin = v[1]; d.innerMax = v[2]; d.outerMax = v[3]; break; case 5: d.outerMin = v[0]; d.innerMin = v[1]; d.center = v[2]; d.innerMax = v[3]; d.outerMax = v[4]; break; default: console.error("Tried to make boxchart from invalid array"); } var ee = new _pondjs.IndexedEvent(e.index(), d); return ee; }); } function getAggregatedSeries(series, column) { var aggregation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultAggregation; var size = aggregation.size, reducers = aggregation.reducers; var inner = reducers.inner, outer = reducers.outer, center = reducers.center; function mapColumn(c, r) { var obj = {}; obj[c] = r; return obj; } var fixedWindowAggregation = {}; if (inner) { fixedWindowAggregation.innerMin = mapColumn(column, inner[0]); fixedWindowAggregation.innerMax = mapColumn(column, inner[1]); } if (outer) { fixedWindowAggregation.outerMin = mapColumn(column, outer[0]); fixedWindowAggregation.outerMax = mapColumn(column, outer[1]); } if (center) { fixedWindowAggregation.center = mapColumn(column, center); } var aggregatedSeries = series.fixedWindowRollup({ windowSize: size, aggregation: fixedWindowAggregation }); return aggregatedSeries; } /** * Renders a boxplot chart. * * The TimeSeries supplied to the boxplot, as the `series` prop can be one of two types: * * 1) It can be a TimeSeries containing IndexedEvents or TimeRangeEvents. * In this case a `column` prop should be supplied to specify the * data column containing the dimensions of the boxes. This props * should be an array of size 1 to 5 elements. e.g. [12, 18, 22, 28]. The * numbers should be ordered, lowest to greatest. * * 2) A TimeSeries containing timestamp based Events. In this case the * boxplot will be aggregated for you. To control the aggregation you can supply * an `aggregation` prop: a structure to specify the window size and * reducers used to determine the boxes. * * In both cases you are generating up to two ranges and a center marker. In the * first case you are defining this based on the array of numbers. The outer numbers * specify the outerRange, the inner numbers specify the innerRange and the middle * number specifies the center marker. In the second case you are building those ranges * from denser data, specifying a window and aggregation functions to build each * of the ranges and center maker. * * In both cases you do not need to supply all the values. For example if you * provide an array of 2 elements, that would define a single range, with no outer range * and no center marker. The BoxChart is pretty flexible in that way, so you * can use it in many situations. * * Here is an example of using it to display temperature ranges. The series * passed to this code would be a TimeSeries containing IndexedEvents. For * each event, the column `temp` contains an array of values used for the * box plot ranges: * * ``` * <BoxChart * axis="temperatureAxis" * style={style} * column="temp" * series={series} /> * ``` * * While here is an example with a dense TimeSeries of Events supplied, * along with an aggregation specification. This code would produce an * outer range from the 5th percentile to the 95th, along with an inner * range for the interquantile, and a center marker at the median: * * ``` * <BoxChart * axis="speedaxis" * series={speed} * column="speed" * style={style} * aggregation={{ * size: this.state.rollup, * reducers: { * outer: [percentile(5), percentile(95)], * inner: [percentile(25), percentile(75)], * center: median(), * }, * }} * /> * ``` * * The BoxChart supports Info boxes, highlighting and selection. * * Note: selection and highlighting is on the whole event, not individual ranges. * Also note that since the box chart builds an internal TimeSeries for performance * reasons, selection will give you and IndexedEvent, but it won't be the same * IndexedEvent in your `series`. Similarly if you are using the aggregation * specification you will get events for the rollup, not your original data. */ var BoxChart = function (_React$Component) { _inherits(BoxChart, _React$Component); function BoxChart(props) { _classCallCheck(this, BoxChart); var _this = _possibleConstructorReturn(this, (BoxChart.__proto__ || Object.getPrototypeOf(BoxChart)).call(this, props)); if (props.series._collection._type === _pondjs.TimeEvent // eslint-disable-line ) { _this.series = getAggregatedSeries(props.series, props.column, props.aggregation); } else { _this.series = getSeries(props.series, props.column); } return _this; } _createClass(BoxChart, [{ key: "componentWillReceiveProps", value: function componentWillReceiveProps(nextProps) { var aggregation = nextProps.aggregation; var aggregationChanged = false; if (_underscore2.default.isUndefined(aggregation) !== _underscore2.default.isUndefined(this.props.aggregation)) { aggregationChanged = true; } if (aggregation && this.props.aggregation) { if (aggregation.size !== this.props.aggregation.size) { aggregationChanged = true; } } if (aggregationChanged) { this.series = getAggregatedSeries(nextProps.series, nextProps.column, nextProps.aggregation); } } }, { key: "shouldComponentUpdate", value: function shouldComponentUpdate(nextProps) { var newSeries = nextProps.series; var oldSeries = this.props.series; var width = nextProps.width; var timeScale = nextProps.timeScale; var yScale = nextProps.yScale; var column = nextProps.column; var style = nextProps.style; var aggregation = nextProps.aggregation; var highlighted = nextProps.highlighted; var selected = nextProps.selected; var widthChanged = this.props.width !== width; var timeScaleChanged = (0, _util.scaleAsString)(this.props.timeScale) !== (0, _util.scaleAsString)(timeScale); var yAxisScaleChanged = this.props.yScale !== yScale; var columnChanged = this.props.column !== column; var styleChanged = JSON.stringify(this.props.style) !== JSON.stringify(style); var highlightedChanged = this.props.highlighted !== highlighted; var selectedChanged = this.props.selected !== selected; var aggregationChanged = false; if (_underscore2.default.isUndefined(aggregation) !== _underscore2.default.isUndefined(this.props.aggregation)) { aggregationChanged = true; } if (aggregation && this.props.aggregation) { if (aggregation.size !== this.props.aggregation.size) { aggregationChanged = true; } } var seriesChanged = false; if (oldSeries.size() !== newSeries.size()) { seriesChanged = true; } else { seriesChanged = !_pondjs.TimeSeries.is(oldSeries, newSeries); } // If the series changes we need to rebuild this.series with // the incoming props if (seriesChanged) { if (nextProps.series._collection._type === _pondjs.TimeEvent // eslint-disable-line ) { this.series = getAggregatedSeries(nextProps.series, nextProps.column, nextProps.aggregation); } else { this.series = getSeries(nextProps.series, nextProps.column); } } return seriesChanged || timeScaleChanged || widthChanged || columnChanged || styleChanged || yAxisScaleChanged || aggregationChanged || highlightedChanged || selectedChanged; } }, { key: "handleHover", value: function handleHover(e, event) { if (this.props.onHighlightChange) { this.props.onHighlightChange(event); } } }, { key: "handleHoverLeave", value: function handleHoverLeave() { if (this.props.onHighlightChange) { this.props.onHighlightChange(null); } } }, { key: "handleClick", value: function handleClick(e, event) { if (this.props.onSelectionChange) { this.props.onSelectionChange(event); } e.stopPropagation(); } }, { key: "providedStyleArray", value: function providedStyleArray(column) { var style = {}; if (this.props.style) { if (this.props.style instanceof _styler.Styler) { style = this.props.style.boxChartStyle()[column]; } else if (_underscore2.default.isFunction(this.props.style)) { style = this.props.style(column); } else if (_underscore2.default.isObject(this.props.style)) { style = this.props.style ? this.props.style[column] : defaultStyle; } } return style; } /** * Returns the style used for drawing the path */ }, { key: "style", value: function style(column, event, level) { var style = void 0; if (!this.providedStyle) { this.providedStyle = this.providedStyleArray(this.props.column); } if (!_underscore2.default.isNull(this.providedStyle) && (!_underscore2.default.isArray(this.providedStyle) || this.providedStyle.length !== 3)) { console.warn("Provided style to BoxChart should be an array of 3 objects"); return defaultStyle; } var isHighlighted = this.props.highlighted && _pondjs.Event.is(this.props.highlighted, event); var isSelected = this.props.selected && _pondjs.Event.is(this.props.selected, event); if (this.props.selected) { if (isSelected) { if (!this.selectedStyle || !this.selectedStyle[level]) { if (!this.selectedStyle) { this.selectedStyle = []; } this.selectedStyle[level] = (0, _merge2.default)(true, defaultStyle[level].selected, this.providedStyle[level].selected ? this.providedStyle[level].selected : {}); } style = this.selectedStyle[level]; } else if (isHighlighted) { if (!this.highlightedStyle || !this.highlightedStyle[level]) { if (!this.highlightedStyle) { this.highlightedStyle = []; } this.highlightedStyle[level] = (0, _merge2.default)(true, defaultStyle[level].highlighted, this.providedStyle[level].highlighted ? this.providedStyle[level].highlighted : {}); } style = this.highlightedStyle[level]; } else { if (!this.mutedStyle) { this.mutedStyle = []; } if (!this.mutedStyle[level]) { this.mutedStyle[level] = (0, _merge2.default)(true, defaultStyle[level].muted, this.providedStyle[level].muted ? this.providedStyle[level].muted : {}); } style = this.mutedStyle[level]; } } else if (isHighlighted) { style = (0, _merge2.default)(true, defaultStyle[level].highlighted, this.providedStyle[level].highlighted ? this.providedStyle[level].highlighted : {}); } else { if (!this.normalStyle) { this.normalStyle = []; } if (!this.normalStyle[level]) { this.normalStyle[level] = (0, _merge2.default)(true, defaultStyle[level].normal, this.providedStyle[level].normal ? this.providedStyle[level].normal : {}); } style = this.normalStyle[level]; } return style; } }, { key: "renderBars", value: function renderBars() { var _this2 = this; var _props = this.props, timeScale = _props.timeScale, yScale = _props.yScale, column = _props.column; var innerSpacing = +this.props.innerSpacing; var outerSpacing = +this.props.outerSpacing; var innerSize = +this.props.innerSize; var outerSize = +this.props.outerSize; var bars = []; var eventMarker = void 0; var scaled = function scaled(d, field) { return d.has(field) && !_underscore2.default.isUndefined(d.get(field)) && !_underscore2.default.isNaN(d.get(field)) ? yScale(d.get(field)) : null; }; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { var _loop = function _loop() { var event = _step.value; var index = event.index(); var begin = event.begin(); var end = event.end(); var d = event.data(); var beginPosInner = timeScale(begin) + innerSpacing; var endPosInner = timeScale(end) - innerSpacing; var beginPosOuter = timeScale(begin) + outerSpacing; var endPosOuter = timeScale(end) - outerSpacing; var innerWidth = innerSize || endPosInner - beginPosInner; if (innerWidth < 1) { innerWidth = 1; } var outerWidth = outerSize || endPosOuter - beginPosOuter; if (outerWidth < 1) { outerWidth = 1; } var c = timeScale(begin) + (timeScale(end) - timeScale(begin)) / 2; var xInner = timeScale(begin) + innerSpacing; if (innerSize) { xInner = c - innerSize / 2; } var xOuter = timeScale(begin) + outerSpacing; if (outerSize) { xOuter = c - outerSize / 2; } var styles = []; styles[0] = _this2.style(column, event, 0); styles[1] = _this2.style(column, event, 1); styles[2] = _this2.style(column, event, 2); var innerMin = scaled(d, "innerMin"); var innerMax = scaled(d, "innerMax"); var outerMin = scaled(d, "outerMin"); var outerMax = scaled(d, "outerMax"); var center = scaled(d, "center"); var hasInner = true; var hasOuter = true; var hasCenter = true; if (_underscore2.default.isNull(innerMin) || _underscore2.default.isNull(innerMax)) { hasInner = false; } if (_underscore2.default.isNull(outerMin) || _underscore2.default.isNull(outerMax)) { hasOuter = false; } if (_underscore2.default.isNull(center)) { hasCenter = false; } var ymax = 0; if (hasOuter) { var level = 0; if (!hasInner) { level += 1; } if (!hasCenter) { level += 1; } var keyOuter = (_this2.series.name() ? _this2.series.name() : "series") + "-" + index + "-outer"; var boxOuter = { x: xOuter, y: outerMax, width: outerWidth, height: outerMin - outerMax, rx: 2, ry: 2 }; var barOuterProps = _extends({ key: keyOuter }, boxOuter, { style: styles[level] }); if (_this2.props.onSelectionChange) { barOuterProps.onClick = function (e) { return _this2.handleClick(e, event); }; } if (_this2.props.onHighlightChange) { barOuterProps.onMouseMove = function (e) { return _this2.handleHover(e, event); }; barOuterProps.onMouseLeave = function () { return _this2.handleHoverLeave(); }; } bars.push(_react2.default.createElement("rect", barOuterProps)); ymax = "outerMax"; } if (hasInner) { var _level = 1; if (!hasCenter) { _level += 1; } var keyInner = _this2.series.name() + "-" + index + "-inner"; var boxInner = { x: xInner, y: innerMax, width: innerWidth, height: innerMin - innerMax, rx: 1, ry: 1 }; var barInnerProps = _extends({ key: keyInner }, boxInner, { style: styles[_level] }); if (_this2.props.onSelectionChange) { barInnerProps.onClick = function (e) { return _this2.handleClick(e, event); }; } if (_this2.props.onHighlightChange) { barInnerProps.onMouseMove = function (e) { return _this2.handleHover(e, event); }; barInnerProps.onMouseLeave = function () { return _this2.handleHoverLeave(); }; } bars.push(_react2.default.createElement("rect", barInnerProps)); ymax = ymax || "innerMax"; } if (hasCenter) { var _level2 = 2; var keyCenter = _this2.series.name() + "-" + index + "-center"; var boxCenter = { x: xInner, y: center, width: innerWidth, height: 1 }; var barCenterProps = _extends({ key: keyCenter }, boxCenter, { style: styles[_level2] }); if (_this2.props.onSelectionChange) { barCenterProps.onClick = function (e) { return _this2.handleClick(e, event); }; } if (_this2.props.onHighlightChange) { barCenterProps.onMouseMove = function (e) { return _this2.handleHover(e, event); }; barCenterProps.onMouseLeave = function () { return _this2.handleHoverLeave(); }; } if (_underscore2.default.isNaN(barCenterProps.y)) { console.log(d.toString()); } bars.push(_react2.default.createElement("rect", barCenterProps)); ymax = ymax || "center"; } // Event marker if info provided and hovering var isHighlighted = _this2.props.highlighted && _pondjs.Event.is(_this2.props.highlighted, event); if (isHighlighted && _this2.props.info) { eventMarker = _react2.default.createElement(_EventMarker2.default, _extends({}, _this2.props, { yValueFunc: function yValueFunc(e) { return e.get(ymax); }, event: event, column: column, marker: "circle", markerRadius: 2 })); } }; for (var _iterator = this.series.events()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { _loop(); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return _react2.default.createElement( "g", null, bars, eventMarker ); } }, { key: "render", value: function render() { return _react2.default.createElement( "g", null, this.renderBars() ); } }]); return BoxChart; }(_react2.default.Component); exports.default = BoxChart; BoxChart.propTypes = { /** * Show or hide this chart */ visible: _propTypes2.default.bool, /** * What [Pond TimeSeries](https://esnet-pondjs.appspot.com/#/timeseries) * data to visualize. See general notes on the BoxChart. */ series: function series(props, propName, componentName) { var value = props[propName]; if (!(value instanceof _pondjs.TimeSeries)) { return new Error("A TimeSeries needs to be passed to " + componentName + " as the 'series' prop."); } // TODO: Better detection of errors // everything ok return null; }, /** * The column within the TimeSeries to plot. Unlike other charts, the BoxChart * works on just a single column. * * NOTE : Columns can't have periods because periods * represent a path to deep data in the underlying events * (i.e. reference into nested data structures) */ column: _propTypes2.default.string, /** * The aggregation specification. This object should contain: * - innerMax * - innerMin * - outerMax * - outerMin * - center * Though each of the pairs, and center, is optional. * For each of these keys you should supply the function you * want to use to calculate these. You can import common functions * from Pond, e.g. min(), avg(), percentile(95), etc. * * For example: * ``` * { * size: this.state.rollup, * reducers: { * outer: [min(), max()], * inner: [percentile(25), percentile(75)], * center: median(), * }, * } * ``` */ aggregation: _propTypes2.default.shape({ size: _propTypes2.default.string, reducers: _propTypes2.default.shape({ inner: _propTypes2.default.arrayOf(_propTypes2.default.func), // eslint-disable-line outer: _propTypes2.default.arrayOf(_propTypes2.default.func), // eslint-disable-line center: _propTypes2.default.func // eslint-disable-line }) }), // eslint-disable-line /** * The style of the box chart drawing (using SVG CSS properties) or * a styler object. It is recommended to user the styler unless you need * detailed customization. */ style: _propTypes2.default.oneOfType([_propTypes2.default.object, _propTypes2.default.func, _propTypes2.default.instanceOf(_styler.Styler)]), /** * The style of the info box and connecting lines */ infoStyle: _propTypes2.default.object, //eslint-disable-line /** * The width of the hover info box */ infoWidth: _propTypes2.default.number, //eslint-disable-line /** * The height of the hover info box */ infoHeight: _propTypes2.default.number, //eslint-disable-line /** * The vertical offset in pixels of the EventMarker info box from the * top of the chart. */ infoOffsetY: _propTypes2.default.number, /** * The values to show in the info box. This is an array of * objects, with each object specifying the label and value * to be shown in the info box. */ info: _propTypes2.default.arrayOf(_propTypes2.default.shape({ //eslint-disable-line label: _propTypes2.default.string, //eslint-disable-line value: _propTypes2.default.string //eslint-disable-line })), /** * If spacing is specified, then the boxes will be separated from the * timerange boundary by this number of pixels. Use this to space out * the boxes from each other. Inner and outer boxes are controlled * separately. */ innerSpacing: _propTypes2.default.number, /** * If spacing is specified, then the boxes will be separated from the * timerange boundary by this number of pixels. Use this to space out * the boxes from each other. Inner and outer boxes are controlled * separately. */ outerSpacing: _propTypes2.default.number, /** * If size is specified, then the innerBox will be this number of pixels wide. This * prop takes priority over "spacing". */ innerSize: _propTypes2.default.number, /** * If size is specified, then the outer box will be this number of pixels wide. This * prop takes priority over "spacing". */ outerSize: _propTypes2.default.number, /** * The selected item, which will be rendered in the "selected" style. * If a bar is selected, all other bars will be rendered in the "muted" style. * * See also `onSelectionChange` */ selected: _propTypes2.default.instanceOf(_pondjs.IndexedEvent), /** * The highlighted item, which will be rendered in the "highlighted" style. * * See also `onHighlightChange` */ highlighted: _propTypes2.default.instanceOf(_pondjs.IndexedEvent), /** * A callback that will be called when the selection changes. It will be called * with the event corresponding to the box clicked as its only arg. */ onSelectionChange: _propTypes2.default.func, /** * A callback that will be called when the hovered over box changes. * It will be called with the event corresponding to the box hovered over. */ onHighlightChange: _propTypes2.default.func, /** * [Internal] The timeScale supplied by the surrounding ChartContainer */ timeScale: _propTypes2.default.func, /** * [Internal] The yScale supplied by the associated YAxis */ yScale: _propTypes2.default.func, /** * [Internal] The width supplied by the surrounding ChartContainer */ width: _propTypes2.default.number }; BoxChart.defaultProps = { visible: true, column: "value", innerSpacing: 1.0, outerSpacing: 2.0, infoStyle: { stroke: "#999", fill: "white", opacity: 0.9, pointerEvents: "none" }, stemStyle: { stroke: "#999", cursor: "crosshair", pointerEvents: "none" }, markerStyle: { fill: "#999" }, markerRadius: 2, infoWidth: 90, infoHeight: 30, infoOffsetY: 20 };