react-timeseries-charts
Version:
Declarative timeseries charts
639 lines (559 loc) • 29.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
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; };
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 _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");
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; } /**
* 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 defaultStyle = {
normal: { fill: "steelblue", opacity: 0.8 },
highlighted: { fill: "steelblue", opacity: 1.0 },
selected: { fill: "steelblue", opacity: 1.0 },
muted: { fill: "steelblue", opacity: 0.4 }
};
/**
* Renders a bar chart based on IndexedEvents within a TimeSeries.
*
* This BarChart implementation is a little different that other time axis
* bar charts in that it will render across a the time range of the event
* rather than rendering to specific categories. As a result,
* a Aug-2014 bar will render between the Aug 2014 tick mark and
* the Sept 2014 tickmark. However, this allows it to play well with other
* types of charts that maybe integrated into the same visualization.
*
* The BarChart will render a single TimeSeries. You can specify the columns
* you want to render with the `columns` prop. Each column will be stacked on
* the other, in the order specified in the `columns` array.
*
* ### IndexedEvents
*
* BarCharts are supposed to be for aggregated values (e.g. average of
* many points over an hour), so the hours themselves are specified
* with an "Index". An Index is a string that represents that range of time,
* rather than a specific time like a timestamp would.
*
* Pond provides several mechanisms for building aggregated series from
* a TimeSeries, and the BarChart code is suited to visualizing that
* output. See Pond for more details (especially TimeSeries.fixedWindowRollup
* and the Pipeline processing facilities). The realtime example in this
* library also shows how to do this on incoming streams of data.
*
* If you have one timestamped point per hour and really want to represent
* those with a BarChart, you can use the Pond static method
* `Index.getIndexString(period, date)` to take the Date and return an
* Index string. Say if those points were hourly, you'll end up with
* strings that look like "1h-412715". This represents a specific hour
* in time (the 412,715th hour since midnight 1 Jan 1970, actually).
* Note that for larger time periods, index strings can be partial
* dates, like "2016-08-31" for Aug 31st, 2016 or "2016-08" for Aug 2016.
*
* Use those index strings to build your timeseries instead of timestamps.
* Here's the Pond code needed to convert a date to an index string:
*
* ```
* import { Index } from "pondjs";
* const d = new Date("2017-01-30T11:58:38.741Z");
* const index = Index.getIndexString("1h", d); // '1h-412715'
* ```
*
* With either the aggregated approach, or the above timestamped
* conversion, you will want a `TimeSeries` of `IndexedEvent`s that
* looks like this:
* ```
* const series = new TimeSeries({
* name: "myseries",
* columns: ["index", "value"],
* points: [
* ["1h-41275", 22],
* ["1h-41276", 35],
* ["1h-41277", 72],
* ...
* ]
* })
* ```
*
* Note: the first column of the timeseries should be "index" (not "time")
* and each point should have an index string at the beginning.
*
* ### Interactivity
*
* The BarChart supports selection of individual bars. To control this use
* `onSelectionChange` to get a callback of selection changed. Your callback
* will be called with the selection (an object containing the event
* and column). You can pass this back into the BarChart as `selection`. For
* example:
*
* ```
* <BarChart
* ...
* selection={this.state.selection}
* onSelectionChange={selection => this.setState({selection})} />
* ```
*
* Similarly you can monitor which bar is being hovered over with the
* `onHighlightChange` callback. This can be used to determine the info box
* to display. Info box will display a box (like a tooltip) with a line
* connecting it to the bar. You use the `info` prop to evoke this and to
* supply the text for the info box. See the styling notes below for more
* information on this.
*
* ### Styling
*
* A BarChart supports per-column or per-event styling. Styles can be set for
* each of the four states that are possible: normal, highlighted,
* selected and muted. To style per-column, supply an object. For per-event styling
* supply a function: `(event, column) => {}` The functon should return a style object.
*
* See the `style` prop in the API documentation for more information.
*
* Separately the size of the bars can be controlled with the `spacing` and
* `offset` props. Spacing controls the gap between the bars. Offset moves the
* bars left or right by the given number of pixels. You can use this to place
* bars along side each other. Alternatively, you can give each column a fixed width
* using the `size` prop. In this case this size will be used in preference to the size
* determined from the timerange of the event and the `spacing`.
*
* The info box is also able to be styled using `infoStyle`, `stemStyle` and
* `markerStyle` This enables you to control the drawing of the box, the connecting
* lines (stem) and dot respectively. Using the `infoWidth` and `infoHeight`
* props you can control the size of the box, which is fixed. For the info inside
* the box, it's up to you: it can either be a simple string or an array of
* {label, value} pairs.
*/
var BarChart = function (_React$Component) {
_inherits(BarChart, _React$Component);
function BarChart() {
_classCallCheck(this, BarChart);
return _possibleConstructorReturn(this, (BarChart.__proto__ || Object.getPrototypeOf(BarChart)).apply(this, arguments));
}
_createClass(BarChart, [{
key: "handleHover",
value: function handleHover(e, event, column) {
var bar = { event: event, column: column };
if (this.props.onHighlightChange) {
this.props.onHighlightChange(bar);
}
}
}, {
key: "handleHoverLeave",
value: function handleHoverLeave() {
if (this.props.onHighlightChange) {
this.props.onHighlightChange(null);
}
}
}, {
key: "handleClick",
value: function handleClick(e, event, column) {
var bar = { event: event, column: column };
if (this.props.onSelectionChange) {
this.props.onSelectionChange(bar);
}
e.stopPropagation();
}
}, {
key: "providedStyleMap",
value: function providedStyleMap(column, event) {
var style = {};
if (this.props.style) {
if (this.props.style instanceof _styler.Styler) {
style = this.props.style.barChartStyle()[column];
} else if (_underscore2.default.isFunction(this.props.style)) {
style = this.props.style(column, event);
} 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) {
var style = void 0;
var styleMap = this.providedStyleMap(column, event);
var isHighlighted = this.props.highlighted && (column === this.props.highlighted.column && _pondjs.Event.is(this.props.highlighted.event, event) || this.props.highlightEntireEvent && _pondjs.Event.is(this.props.highlighted.event, event));
var isSelected = this.props.selected && column === this.props.selected.column && _pondjs.Event.is(this.props.selected.event, event);
if (this.props.selected) {
if (isSelected) {
style = (0, _merge2.default)(true, defaultStyle.selected, styleMap.selected ? styleMap.selected : {});
} else if (isHighlighted) {
style = (0, _merge2.default)(true, defaultStyle.highlighted, styleMap.highlighted ? styleMap.highlighted : {});
} else {
style = (0, _merge2.default)(true, defaultStyle.muted, styleMap.muted ? styleMap.muted : {});
}
} else if (isHighlighted) {
style = (0, _merge2.default)(true, defaultStyle.highlighted, styleMap.highlighted ? styleMap.highlighted : {});
} else {
style = (0, _merge2.default)(true, defaultStyle.normal, styleMap.normal ? styleMap.normal : {});
}
return style;
}
}, {
key: "renderBars",
value: function renderBars() {
var _this2 = this;
var spacing = +this.props.spacing;
var offset = +this.props.offset;
var minBarHeight = this.props.minBarHeight;
var series = this.props.series;
var timeScale = this.props.timeScale;
var yScale = this.props.yScale;
var columns = this.props.columns || ["value"];
var bars = [];
var eventMarker = void 0;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
var _loop = function _loop() {
var event = _step.value;
var begin = event.begin();
var end = event.end();
var beginPos = timeScale(begin) + spacing;
var endPos = timeScale(end) - spacing;
var width = void 0;
if (_this2.props.size) {
width = _this2.props.size;
} else {
width = endPos - beginPos;
}
if (width < 1) {
width = 1;
}
var x = void 0;
if (_this2.props.size) {
var center = timeScale(begin) + (timeScale(end) - timeScale(begin)) / 2;
x = center - _this2.props.size / 2 + offset;
} else {
x = timeScale(begin) + spacing + offset;
}
var yBase = yScale(0);
var yposPositive = yBase;
var yposNegative = yBase;
if (columns) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
var _loop2 = function _loop2() {
var column = _step2.value;
var index = event.index();
var key = series.name() + "-" + index + "-" + column;
var value = event.get(column);
var style = _this2.style(column, event);
var height = yScale(0) - yScale(value);
// Allow negative values. Minimum bar height = 1 pixel.
// Stack negative bars below X-axis and positive above X-Axis
var positiveBar = height >= 0;
height = Math.max(Math.abs(height), minBarHeight);
var y = positiveBar ? yposPositive - height : yposNegative;
// Don't draw a rect when height and minBarHeight are both 0
if (height === 0) return "break";
// Event marker if info provided and hovering
var isHighlighted = _this2.props.highlighted && column === _this2.props.highlighted.column && _pondjs.Event.is(_this2.props.highlighted.event, event);
if (isHighlighted && _this2.props.info) {
eventMarker = _react2.default.createElement(_EventMarker2.default, _extends({}, _this2.props, {
event: event,
column: column,
offsetX: offset,
offsetY: yBase - (positiveBar ? yposPositive : yposNegative)
}));
}
var box = { x: x, y: y, width: width, height: height };
var barProps = _extends({ key: key }, box, { style: style });
if (_this2.props.onSelectionChange) {
barProps.onClick = function (e) {
return _this2.handleClick(e, event, column);
};
}
if (_this2.props.onHighlightChange) {
barProps.onMouseMove = function (e) {
return _this2.handleHover(e, event, column);
};
barProps.onMouseLeave = function () {
return _this2.handleHoverLeave();
};
}
bars.push(_react2.default.createElement("rect", barProps));
if (positiveBar) {
yposPositive -= height;
} else {
yposNegative += height;
}
};
for (var _iterator2 = columns[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var _ret2 = _loop2();
if (_ret2 === "break") break;
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
}
};
for (var _iterator = 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 BarChart;
}(_react2.default.Component);
exports.default = BarChart;
BarChart.propTypes = {
/**
* Show or hide this chart
*/
visible: _propTypes2.default.bool,
/**
* What [Pond TimeSeries](https://esnet-pondjs.appspot.com/#/timeseries)
* data to visualize
*/
series: _propTypes2.default.instanceOf(_pondjs.TimeSeries).isRequired,
/**
* The distance in pixels to inset the bar chart from its actual timerange
*/
spacing: _propTypes2.default.number,
/**
* The distance in pixels to offset the bar from its center position within the timerange
* it represents
*/
offset: _propTypes2.default.number,
/**
* The minimum height of a bar given in pixels.
* By default, the minimum height of a bar is 1 pixel
*/
minBarHeight: _propTypes2.default.number,
/**
* A list of columns within the series that will be stacked on top of each other
*
* 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)
*/
columns: _propTypes2.default.arrayOf(_propTypes2.default.string),
/**
* When true, the entire `highlighted` event will be highlighted, instead of
* only the column bar that's currently being hovered
*/
highlightEntireEvent: _propTypes2.default.bool,
/**
* The style of the bar chart drawing (using SVG CSS properties).
* This is an object with a key for each column which is being drawn,
* per the `columns` prop. For each column a style is defined for
* each state the bar may be in. This style is the CSS properties for
* the underlying SVG <Rect>, so most likely you'll define fill and
* opacity.
*
* For example:
* ```
* style = {
* columnName: {
* normal: {
* fill: "steelblue",
* opacity: 0.8,
* },
* highlighted: {
* fill: "#a7c4dd",
* opacity: 1.0,
* },
* selected: {
* fill: "orange",
* opacity: 1.0,
* },
* muted: {
* fill: "grey",
* opacity: 0.5
* }
* }
* }
* ```
*
* You can also supply a function, which will be called with an event
* and column. The function should return an object containing the
* four states (normal, highlighted, selected and muted) and the corresponding
* CSS properties.
*/
style: _propTypes2.default.oneOfType([_propTypes2.default.object, _propTypes2.default.func, _propTypes2.default.instanceOf(_styler.Styler)]),
/**
* 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({
label: _propTypes2.default.string, //eslint-disable-line
value: _propTypes2.default.string //eslint-disable-line
})),
/**
* The style of the info box itself. Typically you'd want to
* specify a fill color, and stroke color / width here.
*/
infoStyle: _propTypes2.default.object, //eslint-disable-line
/**
* The width of the info box
*/
infoWidth: _propTypes2.default.number, //eslint-disable-line
/**
* The height of the 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,
/**
* Alter the format of the timestamp shown on the info box.
* This may be either a function or a string. If you provide a function
* that will be passed an Index and should return a string. For example:
* ```
* index => moment(index.begin()).format("Do MMM 'YY")
* ```
* Alternatively you can pass in a d3 format string. That will be applied
* to the begin time of the Index range.
*/
infoTimeFormat: _propTypes2.default.oneOfType([
//eslint-disable-line
_propTypes2.default.string, //eslint-disable-line
_propTypes2.default.func //eslint-disable-line
]),
/**
* The radius of the infoBox dot at the end of the marker
*/
markerRadius: _propTypes2.default.number,
/**
* The style of the infoBox dot at the end of the marker
*/
markerStyle: _propTypes2.default.object,
/**
* If size is specified, then the bar will be this number of pixels wide. This
* prop takes priority over "spacing".
*/
size: _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.shape({
event: _propTypes2.default.instanceOf(_pondjs.IndexedEvent),
column: _propTypes2.default.string
}),
/**
* A callback that will be called when the selection changes. It will be called
* with an object containing the event and column.
*/
onSelectionChange: _propTypes2.default.func,
/**
* The highlighted item, which will be rendered in the "highlighted" style.
*
* See also `onHighlightChange`
*/
highlighted: _propTypes2.default.shape({
event: _propTypes2.default.instanceOf(_pondjs.IndexedEvent),
column: _propTypes2.default.string
}),
/**
* A callback that will be called when the hovered over bar changes.
* It will be called with an object containing the event and column.
*/
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
};
BarChart.defaultProps = {
visible: true,
columns: ["value"],
highlightEntireEvent: false,
spacing: 1.0,
offset: 0,
minBarHeight: 1,
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
};