@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
JavaScript
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;