UNPKG

@attivio/suit

Version:

Attivio SUIT, the Search UI Toolkit, is a library for creating search clients for searching the Attivio platform.

442 lines (392 loc) 14.7 kB
var _class2, _temp; 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; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } import React from 'react'; import ReactHighcharts from 'react-highcharts'; import DataPoint from '../api/DataPoint'; import LocalDateUtils from '../util/DateUtils'; import StringUtils from '../util/StringUtils'; /** * An individual series of points for the time series. */ export var SeriesDataSource = /** * This should be set on time series that have percentage data. * It forces the Y-axis to range from 0-100, regardless of the values * in the data set. This only applies if this source has a Y-axis * label (and therefore has its own axis). */ /** * Describes how to format the values for this data set in tool tips. * If not set, JavaScript's toLocaleString() method is called on the * value. If set, the format string must be an integer followed by a * colon followed by the formatting string. The integer before the colon * is the number of decimal places to use when formatting the value. * The remainder of the format string will have occurrences of '{}' * substituted with the formatted version of the value. It may consist * of multiple alternative format strings separated by pipe (|) * characters, similar to the fmt() method in SUIT's StringUtils * class—if just one string, it will be used for all values of the * point; if there are two strings, the first is used when the value * is exactly 1 and the second is used in all other cases; if there * are three strings, the first is used when the value is 0, the * second when it's 1, and the third in all other cases. * For example, it may be something like '2:${}' for formatting dollar * amounts or '0:None|1 instance|{} instances' for formatting countable * items, etc. */ /** Data points to be rendered. */ /** Name for the series */ function SeriesDataSource(name, type, data) { var color = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '#000'; var tooltipFormat = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; var yAxisLabel = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; var percentage = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : false; var integer = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false; _classCallCheck(this, SeriesDataSource); this.name = name; this.type = type; this.data = data; this.color = color; this.tooltipFormat = tooltipFormat; this.yAxisLabel = yAxisLabel; this.percentage = percentage; this.integer = integer; } /** * This should be set on time series that have only integer data. * It forces the Y-axis to not show fractional values on its tick marks. * This only applies if this source has a Y-axis label (and therefore * has its own axis). */ /** * Y-axis label on the chart, if any. Generally, at least the first * series should have a label. There should not be more than 2 labels. * The second label, if one exists, will be on the right-hand side of * the chart. Any series following the first one but not having their * own yAxisLabel will use the same Y-axis as the first series. Any * series following the one with the second yAxisLabel will use that * Y-axis. */ /** Color for the data on the chart (defaults to black) */ /** How to render this data set; it could be a BAR, LINE, or AREA chart. */ ; /** * Component to display a chart of values over time. */ var TimeSeries = (_temp = _class2 = function (_React$Component) { _inherits(TimeSeries, _React$Component); TimeSeries.getDataSourceType = function getDataSourceType(type) { switch (type) { case 'AREA': return 'area'; case 'BAR': return 'column'; case 'LINE': default: return 'line'; } }; /** * Construct a tooltip containing the date and values for all of the data series. */ TimeSeries.tooltipFormatter = function tooltipFormatter() { var pointInfo = this; if (pointInfo.points && pointInfo.points.length > 0) { var firstPoint = pointInfo.points[0].point; // All of the points should have the same time range var formattedDate = LocalDateUtils.formatDateRange(firstPoint.startTime, firstPoint.endTime); var header = '<b>' + formattedDate + '</b>'; var rows = pointInfo.points.map(function (point) { var formattedValue = point.series.tooltipOptions.formatter(point.y); return '<br /><span style="color: ' + point.color + '">\u25CF</span> ' + point.series.name + ': <b>' + formattedValue + '</b>'; }); rows.unshift(header); return rows.join(''); } // If there are no points, don't display a tooltip... shouldn't happen return null; }; /** * Format the value for a single series as HTML. */ TimeSeries.formatPointValue = function formatPointValue(value, formatString) { if (value === null) { // If the value is null, then there's no data for this time point. // Note that currently Highcharts doesn't bother calling the tooltip // function if the data is null, so this is just in case they ever // change this (for maps, there's a parameter called "nullInteraction" // that can be set to true to make this work but it doesn't apply to // the chart types we use). return '<span style="font-style: italic; color: #ccc;">N/A</span>'; } if (formatString) { return StringUtils.formatNumber(formatString, value); } return value.toLocaleString(); }; /** * Look at the maxValue passed in and see if it's greater than * the max set on the Y-axis. If so, make it be a round number and * update the Y-axis to use it instead. Return the new Y-axis * definition. */ TimeSeries.normalizeYAxisMax = function normalizeYAxisMax(yAxis) { var maxValue = yAxis.max; var roundUpTo = [10, 20, 25, 30, 40, 50, 100, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 10000]; var roundMax = -1; roundUpTo.forEach(function (cap) { if (roundMax < 0 && maxValue <= cap) { roundMax = cap; } }); if (roundMax < 0) { // We didn't find one... just round up to nearest multiple of 10,000 roundMax = Math.floor((maxValue + 9999) / 10000) * 10000; } var newYAxis = Object.assign({}, yAxis); newYAxis.max = roundMax; return newYAxis; }; function TimeSeries(props) { _classCallCheck(this, TimeSeries); var _this = _possibleConstructorReturn(this, _React$Component.call(this, props)); _this.onSelectRange = _this.onSelectRange.bind(_this); // (this: any).tooltipFormatter = this.tooltipFormatter.bind(this); return _this; } /** * Convert from the HighCharts select event to the callback passed into this component. */ TimeSeries.prototype.onSelectRange = function onSelectRange(event) { if (this.props.onSelect) { this.props.onSelect(new Date(event.xAxis[0].min), new Date(event.xAxis[0].max)); } }; TimeSeries.prototype.render = function render() { var _this2 = this; ReactHighcharts.Highcharts.setOptions({ global: { useUTC: false } }); // Convert the data series passed to us into something HighCharts understands // At the same time, find the Y axes... var yAxes = []; var series = this.props.dataSources.map(function (source) { // Assume this series will have the same y axis as the most recent one // (or the first one, if no y axes exist yet) var currentYAxisIndex = Math.max(yAxes.length - 1, 0); if (source.yAxisLabel) { var yAxisInfo = { title: { text: source.yAxisLabel }, min: 0, opposite: false, visible: true, minRange: 0.1, // This makes sure the 0 is always at the bottom of the chart max: source.percentage ? 100 : null, alignTicks: !source.percentage, gridLineColor: source.percentage ? 'transparent' : undefined, allowDecimals: !source.integer }; if (yAxes.length === 1) { // This is the second label, put it on the right yAxisInfo.opposite = true; } if (yAxes.length < 2) { // Make sure we never have more than two labels... yAxes.push(yAxisInfo); currentYAxisIndex = yAxes.length - 1; } else { // We're configured for too many Y axes... we only deal with up to 2 console.warn('Ignoring Y-axis named ' + yAxisInfo.title.text); } } var type = TimeSeries.getDataSourceType(source.type); var maxValue = 0; var dataPoints = source.data.map(function (point) { if (point.data !== null && point.data > maxValue) { maxValue = point.data; } var dataPoint = { x: (point.startTime + point.endTime) / 2, y: point.data, startTime: point.startTime, endTime: point.endTime }; return dataPoint; }); var previousMax = yAxes[currentYAxisIndex].max || 0; if (!source.percentage) { // If it's a percentage, we've already set the max to 100 if (source.type === 'BAR' && !_this2.props.barsSideBySide) { // If there are multiple series for this y-axis, and they're bars, and the bars are stacked, // add the max values together since they'll be stacked. yAxes[currentYAxisIndex].max = previousMax + maxValue; } else { // Otherwise, find the bigger maxValue and use that. yAxes[currentYAxisIndex].max = Math.max(previousMax, maxValue); } } // Make sure bar charts are in front of area charts and line charts are in front of everything var zIndex = void 0; if (source.type === 'BAR') { zIndex = 1; } else if (source.type === 'LINE') { zIndex = 2; } // Ensure we always have something to show for non-empty data points in bar charts // Note that we use a height of 2 pixels instead of 1 because 1-pixel bars get hidden // by the X-axis' line. var minPointLength = type === 'column' ? 2 : 0; var seriesInfo = { type: type, data: dataPoints, name: source.name, showInLegend: source.name ? source.name : '', color: source.color, yAxis: currentYAxisIndex, stacking: source.type === 'BAR' && !_this2.props.barsSideBySide ? 'normal' : undefined, zIndex: zIndex, tooltip: { formatter: function formatter(value) { return TimeSeries.formatPointValue(value, source.tooltipFormat); } }, minPointLength: minPointLength }; return seriesInfo; }); // Go through the y axes and round up the max values in a consistent way, so they're pretty yAxes = yAxes.map(function (notNormal) { return TimeSeries.normalizeYAxisMax(notNormal); }); var chart = this.props.onSelect ? { backgroundColor: null, borderWidth: null, shadow: false, height: this.props.height, zoomType: 'x', resetZoomButton: { position: { x: 0, y: -40 } }, events: { selection: this.onSelectRange }, ignoreHiddenSeries: true } : { backgroundColor: null, borderWidth: null, shadow: false, marginTop: 40, height: this.props.height, ignoreHiddenSeries: true }; var legend = void 0; if (this.props.legendAtRight) { legend = { backgroundColor: '#fff', enabled: true, align: 'right', borderWidth: 1, layout: 'vertical', verticalAlign: 'bottom', y: -10, itemMarginBottom: 10, itemStyle: { 'font-size': '.8em' } }; } else { legend = { backgroundColor: '#fff', itemStyle: { 'font-size': '.8em' } }; } var config = { chart: chart, plotOptions: { series: { animation: false }, area: { cursor: 'crosshair', pointPlacement: 'on', lineWidth: 2, marker: { radius: 3 } }, line: { cursor: 'crosshair', pointPlacement: 'on', fillOpacity: 0.1, lineWidth: 3, marker: { radius: 3 }, softThreshold: false }, column: { cursor: 'crosshair', pointPadding: 0.1, grouping: true, groupPadding: 0, borderWidth: 1, states: { hover: { color: '#203267', borderColor: '#5d6a90' } } } }, legend: legend, xAxis: { type: 'datetime', second: '%H:%M:%S', dateTimeLabelFormats: { minute: '%l:%M%p', hour: '%l%P', day: '%B %e', week: '%B %e', month: '%B, %Y', year: '%Y' }, minorTickLength: 0, startOfWeek: 0, labels: { autoRotation: [45] } }, yAxis: yAxes, tooltip: { shared: true, formatter: TimeSeries.tooltipFormatter }, title: { text: '' }, series: series, credits: { enabled: false } }; return React.createElement(ReactHighcharts, { config: config }); }; return TimeSeries; }(React.Component), _class2.defaultProps = { onSelect: null, height: 300, barsSideBySide: false, legendAtRight: false }, _class2.displayName = 'TimeSeries', _temp); export { TimeSeries as default }; TimeSeries.SeriesDataSource = SeriesDataSource;