UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

907 lines (722 loc) 29.8 kB
import PlotAreaBase from './plotarea-base'; import AxisGroupRangeTracker from '../axis-group-range-tracker'; import PlotAreaEventsMixin from '../mixins/plotarea-events-mixin'; import SeriesAggregator from '../aggregates/series-aggregator'; import DefaultAggregates from '../aggregates/default-aggregates'; import SeriesBinder from '../series-binder'; import BarChart from '../bar-chart/bar-chart'; import RangeBarChart from '../range-bar-chart/range-bar-chart'; import BulletChart from '../bullet-chart/bullet-chart'; import LineChart from '../line-chart/line-chart'; import AreaChart from '../area-chart/area-chart'; import RangeAreaChart from '../range-area-chart/range-area-chart'; import OHLCChart from '../ohlc-chart/ohlc-chart'; import CandlestickChart from '../candlestick-chart/candlestick-chart'; import BoxPlotChart from '../box-plot-chart/box-plot-chart'; import WaterfallChart from '../waterfall-chart/waterfall-chart'; import trendlineFactory from '../trendlines/trendline-factory'; import trendlineRegistry from '../trendlines/trendline-registry'; import { CategoryAxis, DateCategoryAxis, NumericAxis, LogarithmicAxis, Point } from '../../core'; import { appendIfNotNull, categoriesCount, createOutOfRangePoints, equalsIgnoreCase, filterSeriesByType, isDateAxis, parseDateCategory, singleItemOrArray } from '../utils'; import { BAR, COLUMN, BULLET, VERTICAL_BULLET, LINE, VERTICAL_LINE, AREA, VERTICAL_AREA, RANGE_AREA, VERTICAL_RANGE_AREA, RANGE_COLUMN, RANGE_BAR, WATERFALL, HORIZONTAL_WATERFALL, BOX_PLOT, VERTICAL_BOX_PLOT, OHLC, CANDLESTICK, LOGARITHMIC, STEP, EQUALLY_SPACED_SERIES, RADAR_LINE, RADAR_AREA } from '../constants'; import { DATE, MAX_VALUE } from '../../common/constants'; import { setDefaultOptions, inArray, deepExtend, defined, eventElement, grep, cycleIndex, hasOwnProperty } from '../../common'; const AREA_SERIES = [ AREA, VERTICAL_AREA, RANGE_AREA, VERTICAL_RANGE_AREA ]; const OUT_OF_RANGE_SERIES = [ LINE, VERTICAL_LINE ].concat(AREA_SERIES); class CategoricalPlotArea extends PlotAreaBase { initFields(series) { this.namedCategoryAxes = {}; this.namedValueAxes = {}; this.valueAxisRangeTracker = new AxisGroupRangeTracker(); this._seriesPointsCache = {}; this._currentPointsCache = {}; if (series.length > 0) { this.invertAxes = inArray( series[0].type, [ BAR, BULLET, VERTICAL_LINE, VERTICAL_AREA, VERTICAL_RANGE_AREA, RANGE_BAR, HORIZONTAL_WATERFALL, VERTICAL_BOX_PLOT ] ); for (let i = 0; i < series.length; i++) { const stack = series[i].stack; if (stack && stack.type === "100%") { this.stack100 = true; break; } } } } render(panes = this.panes) { this.series = [...this.originalSeries]; this.createCategoryAxes(panes); this.aggregateCategories(panes); this.createTrendlineSeries(panes); this.createCategoryAxesLabels(panes); this.createCharts(panes); this.createValueAxes(panes); } removeAxis(axis) { const axisName = axis.options.name; super.removeAxis(axis); if (axis instanceof CategoryAxis) { delete this.namedCategoryAxes[axisName]; } else { this.valueAxisRangeTracker.reset(axisName); delete this.namedValueAxes[axisName]; } if (axis === this.categoryAxis) { delete this.categoryAxis; } if (axis === this.valueAxis) { delete this.valueAxis; } } trendlineFactory(options, series) { const categoryAxis = this.seriesCategoryAxis(options); const seriesValues = this.seriesValues.bind(this, series.index); const trendline = trendlineFactory(trendlineRegistry, options.type, { options, categoryAxis, seriesValues }); if (trendline) { // Inherit settings trendline.categoryAxis = series.categoryAxis; trendline.valueAxis = series.valueAxis; return this.filterSeries(trendline, categoryAxis); } return trendline; } trendlineAggregateForecast() { return this.series .map(series => (series.trendline || {}).forecast) .filter(forecast => forecast !== undefined) .reduce((result, forecast) => ({ before: Math.max(result.before, forecast.before || 0), after: Math.max(result.after, forecast.after || 0) }), { before: 0, after: 0 }); } seriesValues(seriesIx, range) { const result = []; let series = this.srcSeries[seriesIx]; const categoryAxis = this.seriesCategoryAxis(series); const dateAxis = equalsIgnoreCase(categoryAxis.options.type, DATE); if (dateAxis) { this._seriesPointsCache = {}; this._currentPointsCache = {}; categoryAxis.options.dataItems = []; series = this.aggregateSeries(series, categoryAxis, categoryAxis.totalRangeIndices()); } const min = range ? range.min : 0; const max = range ? range.max : series.data.length; for (let categoryIx = min; categoryIx < max; categoryIx++) { const data = this.bindPoint(series, categoryIx); result.push({ categoryIx, category: data.fields.category, valueFields: data.valueFields }); } return result; } createCharts(panes) { const seriesByPane = this.groupSeriesByPane(); for (let i = 0; i < panes.length; i++) { const pane = panes[i]; const paneSeries = seriesByPane[pane.options.name || "default"] || []; this.addToLegend(paneSeries); const visibleSeries = this.filterVisibleSeries(paneSeries); if (!visibleSeries) { continue; } const groups = this.groupSeriesByCategoryAxis(visibleSeries); for (let groupIx = 0; groupIx < groups.length; groupIx++) { this.createChartGroup(groups[groupIx], pane); } } } createChartGroup(series, pane) { this.createAreaChart( filterSeriesByType(series, [ AREA, VERTICAL_AREA ]), pane ); this.createRangeAreaChart( filterSeriesByType(series, [ RANGE_AREA, VERTICAL_RANGE_AREA ]), pane ); this.createBarChart( filterSeriesByType(series, [ COLUMN, BAR ]), pane ); this.createRangeBarChart( filterSeriesByType(series, [ RANGE_COLUMN, RANGE_BAR ]), pane ); this.createBulletChart( filterSeriesByType(series, [ BULLET, VERTICAL_BULLET ]), pane ); this.createCandlestickChart( filterSeriesByType(series, CANDLESTICK), pane ); this.createBoxPlotChart( filterSeriesByType(series, [ BOX_PLOT, VERTICAL_BOX_PLOT ]), pane ); this.createOHLCChart( filterSeriesByType(series, OHLC), pane ); this.createWaterfallChart( filterSeriesByType(series, [ WATERFALL, HORIZONTAL_WATERFALL ]), pane ); this.createLineChart( filterSeriesByType(series, [ LINE, VERTICAL_LINE ]), pane ); } aggregateCategories(panes) { const series = [...this.series]; const processedSeries = []; this._currentPointsCache = {}; this._seriesPointsCache = this._seriesPointsCache || {}; for (let i = 0; i < series.length; i++) { let currentSeries = series[i]; if (!this.isTrendline(currentSeries)) { const categoryAxis = this.seriesCategoryAxis(currentSeries); const axisPane = this.findPane(categoryAxis.options.pane); const dateAxis = equalsIgnoreCase(categoryAxis.options.type, DATE); if ((dateAxis || currentSeries.categoryField) && inArray(axisPane, panes)) { currentSeries = this.aggregateSeries(currentSeries, categoryAxis, categoryAxis.currentRangeIndices()); } else { currentSeries = this.filterSeries(currentSeries, categoryAxis); } } processedSeries.push(currentSeries); } this._seriesPointsCache = this._currentPointsCache; this._currentPointsCache = null; this.srcSeries = series; this.series = processedSeries; } filterSeries(series, categoryAxis) { const dataLength = (series.data || {}).length; categoryAxis._seriesMax = Math.max(categoryAxis._seriesMax || 0, dataLength); if (!(defined(categoryAxis.options.min) || defined(categoryAxis.options.max))) { return series; } const range = categoryAxis.currentRangeIndices(); const outOfRangePoints = inArray(series.type, OUT_OF_RANGE_SERIES); const currentSeries = deepExtend({}, series); currentSeries.data = (currentSeries.data || []).slice(range.min, range.max + 1); if (outOfRangePoints) { createOutOfRangePoints(currentSeries, range, dataLength, (idx) => ({ item: series.data[idx], category: categoryAxis.categoryAt(idx, true), categoryIx: idx - range.min }), (idx) => defined(series.data[idx])); } return currentSeries; } clearSeriesPointsCache() { this._seriesPointsCache = {}; } seriesSourcePoints(series, categoryAxis) { const key = `${ series.index };${ categoryAxis.categoriesHash() }`; if (this._seriesPointsCache && this._seriesPointsCache[key]) { this._currentPointsCache[key] = this._seriesPointsCache[key]; return this._seriesPointsCache[key]; } const axisOptions = categoryAxis.options; const srcCategories = axisOptions.srcCategories; const dateAxis = equalsIgnoreCase(axisOptions.type, DATE); const srcData = series.data; const result = []; if (!dateAxis) { categoryAxis.indexCategories(); } for (let idx = 0; idx < srcData.length; idx++) { let category = SeriesBinder.current.bindPoint(series, idx).fields.category; if (dateAxis) { category = parseDateCategory(category, srcData[idx], this.chartService.intl); } if (category === undefined) { category = srcCategories[idx]; } if (category !== undefined && category !== null) { const categoryIx = categoryAxis.totalIndex(category); result[categoryIx] = result[categoryIx] || { items: [], category: category }; result[categoryIx].items.push(idx); } } this._currentPointsCache[key] = result; return result; } aggregateSeries(series, categoryAxis, range) { const srcData = series.data; if (!srcData.length) { return series; } const srcPoints = this.seriesSourcePoints(series, categoryAxis); const result = deepExtend({}, series); const aggregator = new SeriesAggregator(deepExtend({}, series), SeriesBinder.current, DefaultAggregates.current); const data = result.data = []; const dataItems = categoryAxis.options.dataItems || []; const categoryItem = (idx) => { const categoryIdx = idx - range.min; let point = srcPoints[idx]; if (!point) { point = srcPoints[idx] = {}; } point.categoryIx = categoryIdx; if (!point.item) { const category = categoryAxis.categoryAt(idx, true); point.category = category; point.item = aggregator.aggregatePoints(point.items, category); } return point; }; for (let idx = range.min; idx <= range.max; idx++) { const point = categoryItem(idx); data[point.categoryIx] = point.item; if (point.items && point.items.length) { dataItems[point.categoryIx] = point.item; } } if (inArray(result.type, OUT_OF_RANGE_SERIES)) { createOutOfRangePoints(result, range, categoryAxis.totalCount(), categoryItem, (idx) => srcPoints[idx]); } categoryAxis.options.dataItems = dataItems; return result; } appendChart(chart, pane) { const series = chart.options.series; const categoryAxis = this.seriesCategoryAxis(series[0]); let categories = categoryAxis.options.categories; let categoriesToAdd = Math.max(0, categoriesCount(series) - categories.length); if (categoriesToAdd > 0) {//consider setting an option to axis instead of adding fake categories categories = categoryAxis.options.categories = categoryAxis.options.categories.slice(0); while (categoriesToAdd--) { categories.push(""); } } this.valueAxisRangeTracker.update(chart.valueAxisRanges); super.appendChart(chart, pane); } // TODO: Refactor, optionally use series.pane option seriesPaneName(series) { const options = this.options; const axisName = series.axis; const axisOptions = [].concat(options.valueAxis); const axis = grep(axisOptions, function(a) { return a.name === axisName; })[0]; const panes = options.panes || [ {} ]; const defaultPaneName = (panes[0] || {}).name || "default"; const paneName = (axis || {}).pane || defaultPaneName; return paneName; } seriesCategoryAxis(series) { const axisName = series.categoryAxis; const axis = axisName ? this.namedCategoryAxes[axisName] : this.categoryAxis; if (!axis) { throw new Error("Unable to locate category axis with name " + axisName); } return axis; } stackableChartOptions(series, pane) { const anyStackedSeries = series.some(s => s.stack); const isStacked100 = series.some(s => s.stack && s.stack.type === "100%"); const clip = pane.options.clip; return { defaultStack: series[0].stack, isStacked: anyStackedSeries, isStacked100: isStacked100, clip: clip }; } groupSeriesByCategoryAxis(series) { const categoryAxes = []; const unique = {}; for (let idx = 0; idx < series.length; idx++) { const name = series[idx].categoryAxis || "$$default$$"; if (!hasOwnProperty(unique, name)) { unique[name] = true; categoryAxes.push(name); } } const groups = []; for (let axisIx = 0; axisIx < categoryAxes.length; axisIx++) { const axis = categoryAxes[axisIx]; const axisSeries = groupSeries(series, axis, axisIx); if (axisSeries.length === 0) { continue; } groups.push(axisSeries); } return groups; } createBarChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const barChart = new BarChart(this, Object.assign({ series: series, invertAxes: this.invertAxes, gap: firstSeries.gap, spacing: firstSeries.spacing }, this.stackableChartOptions(series, pane))); this.appendChart(barChart, pane); } createRangeBarChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const rangeColumnChart = new RangeBarChart(this, { series: series, invertAxes: this.invertAxes, gap: firstSeries.gap, spacing: firstSeries.spacing }); this.appendChart(rangeColumnChart, pane); } createBulletChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const bulletChart = new BulletChart(this, { series: series, invertAxes: this.invertAxes, gap: firstSeries.gap, spacing: firstSeries.spacing, clip: pane.options.clip }); this.appendChart(bulletChart, pane); } createLineChart(series, pane) { if (series.length === 0) { return; } const lineChart = new LineChart(this, Object.assign({ invertAxes: this.invertAxes, series: series }, this.stackableChartOptions(series, pane))); this.appendChart(lineChart, pane); } createAreaChart(series, pane) { if (series.length === 0) { return; } const areaChart = new AreaChart(this, Object.assign({ invertAxes: this.invertAxes, series: series }, this.stackableChartOptions(series, pane))); this.appendChart(areaChart, pane); } createRangeAreaChart(series, pane) { if (series.length === 0) { return; } const rangeAreaChart = new RangeAreaChart(this, { invertAxes: this.invertAxes, series: series, clip: pane.options.clip }); this.appendChart(rangeAreaChart, pane); } createOHLCChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const chart = new OHLCChart(this, { invertAxes: this.invertAxes, gap: firstSeries.gap, series: series, spacing: firstSeries.spacing, clip: pane.options.clip }); this.appendChart(chart, pane); } createCandlestickChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const chart = new CandlestickChart(this, { invertAxes: this.invertAxes, gap: firstSeries.gap, series: series, spacing: firstSeries.spacing, clip: pane.options.clip }); this.appendChart(chart, pane); } createBoxPlotChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const chart = new BoxPlotChart(this, { invertAxes: this.invertAxes, gap: firstSeries.gap, series: series, spacing: firstSeries.spacing, clip: pane.options.clip }); this.appendChart(chart, pane); } createWaterfallChart(series, pane) { if (series.length === 0) { return; } const firstSeries = series[0]; const waterfallChart = new WaterfallChart(this, { series: series, invertAxes: this.invertAxes, gap: firstSeries.gap, spacing: firstSeries.spacing }); this.appendChart(waterfallChart, pane); } axisRequiresRounding(categoryAxisName, categoryAxisIndex) { const centeredSeries = filterSeriesByType(this.series, EQUALLY_SPACED_SERIES); for (let seriesIx = 0; seriesIx < this.series.length; seriesIx++) { const currentSeries = this.series[seriesIx]; if (inArray(currentSeries.type, AREA_SERIES)) { const line = currentSeries.line; if (line && line.style === STEP) { centeredSeries.push(currentSeries); } } } for (let seriesIx = 0; seriesIx < centeredSeries.length; seriesIx++) { const seriesAxis = centeredSeries[seriesIx].categoryAxis || ""; if (seriesAxis === categoryAxisName || (!seriesAxis && categoryAxisIndex === 0)) { return true; } } } aggregatedAxis(categoryAxisName, categoryAxisIndex) { const series = this.series; for (let seriesIx = 0; seriesIx < series.length; seriesIx++) { const seriesAxis = series[seriesIx].categoryAxis || ""; if ((seriesAxis === categoryAxisName || (!seriesAxis && categoryAxisIndex === 0)) && series[seriesIx].categoryField) { return true; } } } createCategoryAxesLabels() { const axes = this.axes; for (let i = 0; i < axes.length; i++) { if (axes[i] instanceof CategoryAxis) { axes[i].createLabels(); } } } createCategoryAxes(panes) { const invertAxes = this.invertAxes; const definitions = [].concat(this.options.categoryAxis); const axes = []; for (let i = 0; i < definitions.length; i++) { let axisOptions = definitions[i]; const axisPane = this.findPane(axisOptions.pane); if (inArray(axisPane, panes)) { const { name, categories = [] } = axisOptions; axisOptions = deepExtend({ vertical: invertAxes, reverse: !invertAxes && this.chartService.rtl, axisCrossingValue: invertAxes ? MAX_VALUE : 0 }, axisOptions); if (!defined(axisOptions.justified)) { axisOptions.justified = this.isJustified(); } if (this.axisRequiresRounding(name, i)) { axisOptions.justified = false; } let categoryAxis; if (isDateAxis(axisOptions, categories[0])) { axisOptions._forecast = this.trendlineAggregateForecast(); categoryAxis = new DateCategoryAxis(axisOptions, this.chartService); } else { categoryAxis = new CategoryAxis(axisOptions, this.chartService); } definitions[i].categories = categoryAxis.options.srcCategories; if (name) { if (this.namedCategoryAxes[name]) { throw new Error(`Category axis with name ${ name } is already defined`); } this.namedCategoryAxes[name] = categoryAxis; } categoryAxis.axisIndex = i; axes.push(categoryAxis); this.appendAxis(categoryAxis); } } const primaryAxis = this.categoryAxis || axes[0]; this.categoryAxis = primaryAxis; if (invertAxes) { this.axisY = primaryAxis; } else { this.axisX = primaryAxis; } } isJustified() { const series = this.series; for (let i = 0; i < series.length; i++) { const currentSeries = series[i]; if (!inArray(currentSeries.type, AREA_SERIES)) { return false; } } return true; } createValueAxes(panes) { const tracker = this.valueAxisRangeTracker; const defaultRange = tracker.query(); const definitions = [].concat(this.options.valueAxis); const invertAxes = this.invertAxes; const baseOptions = { vertical: !invertAxes, reverse: invertAxes && this.chartService.rtl }; const axes = []; if (this.stack100) { baseOptions.roundToMajorUnit = false; baseOptions.labels = { format: "P0" }; } for (let i = 0; i < definitions.length; i++) { const axisOptions = definitions[i]; const axisPane = this.findPane(axisOptions.pane); if (inArray(axisPane, panes)) { const name = axisOptions.name; const defaultAxisRange = equalsIgnoreCase(axisOptions.type, LOGARITHMIC) ? { min: 0.1, max: 1 } : { min: 0, max: 1 }; const range = tracker.query(name) || defaultRange || defaultAxisRange; if (i === 0 && range && defaultRange) { range.min = Math.min(range.min, defaultRange.min); range.max = Math.max(range.max, defaultRange.max); } let axisType; if (equalsIgnoreCase(axisOptions.type, LOGARITHMIC)) { axisType = LogarithmicAxis; } else { axisType = NumericAxis; } const valueAxis = new axisType(range.min, range.max, deepExtend({}, baseOptions, axisOptions), this.chartService ); if (name) { if (this.namedValueAxes[name]) { throw new Error(`Value axis with name ${ name } is already defined`); } this.namedValueAxes[name] = valueAxis; } valueAxis.axisIndex = i; axes.push(valueAxis); this.appendAxis(valueAxis); } } const primaryAxis = this.valueAxis || axes[0]; this.valueAxis = primaryAxis; if (invertAxes) { this.axisX = primaryAxis; } else { this.axisY = primaryAxis; } } _dispatchEvent(chart, e, eventType) { const coords = chart._eventCoordinates(e); const point = new Point(coords.x, coords.y); const pane = this.pointPane(point); const categories = []; const values = []; if (!pane) { return; } const allAxes = pane.axes; for (let i = 0; i < allAxes.length; i++) { const axis = allAxes[i]; if (axis.getValue) { appendIfNotNull(values, axis.getValue(point)); } else { appendIfNotNull(categories, axis.getCategory(point)); } } if (categories.length === 0) { appendIfNotNull(categories, this.categoryAxis.getCategory(point)); } if (categories.length > 0 && values.length > 0) { chart.trigger(eventType, { element: eventElement(e), originalEvent: e, category: singleItemOrArray(categories), value: singleItemOrArray(values) }); } } pointPane(point) { const panes = this.panes; for (let i = 0; i < panes.length; i++) { const currentPane = panes[i]; if (currentPane.contentBox.containsPoint(point)) { return currentPane; } } } updateAxisOptions(axis, options) { updateAxisOptions(this.options, axis, options); updateAxisOptions(this.originalOptions, axis, options); } _pointsByVertical(basePoint, offset = 0) { if (this.invertAxes) { return this._siblingsBySeriesIndex(basePoint.series.index, offset); } return this._siblingsByPointIndex(basePoint.getIndex()); } _pointsByHorizontal(basePoint, offset = 0) { if (this.invertAxes) { return this._siblingsByPointIndex(basePoint.getIndex()); } const siblings = this._siblingsBySeriesIndex(basePoint.series.index, offset); if (this.chartService.rtl) { return siblings.reverse(); } return siblings; } _siblingsByPointIndex(pointIndex) { const charts = this.charts; const result = []; for (let i = 0; i < charts.length; i++) { let chart = charts[i]; if (chart.pane && chart.pane.options.name === "_navigator") { continue; } let chartPoints = chart.points .filter(point => point && point.visible !== false && point.getIndex() === pointIndex ); result.push(...chartPoints.sort(this._getSeriesCompareFn(chartPoints[0]))); } return result; } _siblingsBySeriesIndex(seriesIndex, offset) { const index = cycleIndex(seriesIndex + offset, this.series.length); return this.pointsBySeriesIndex(index); } _getSeriesCompareFn(point) { const isStacked = this._isInStackedSeries(point); if (isStacked && this.invertAxes || !isStacked && !this.invertAxes) { return (a, b) => a.box.center().x - b.box.center().x; } return (a, b) => a.box.center().y - b.box.center().y; } _isInStackedSeries(point) { const sortableSeries = inArray(point.series.type, [ AREA, VERTICAL_AREA, RANGE_AREA, VERTICAL_RANGE_AREA, LINE, VERTICAL_LINE, RADAR_LINE, RADAR_AREA]); const stackableSeries = inArray(point.series.type, [ COLUMN, BAR]); return sortableSeries || stackableSeries && point.options.isStacked; } } function updateAxisOptions(targetOptions, axis, options) { const axesOptions = axis instanceof CategoryAxis ? [].concat(targetOptions.categoryAxis) : [].concat(targetOptions.valueAxis); deepExtend(axesOptions[axis.axisIndex], options); } function groupSeries(series, axis, axisIx) { return grep(series, function(s) { return (axisIx === 0 && !s.categoryAxis) || (s.categoryAxis === axis); }); } setDefaultOptions(CategoricalPlotArea, { categoryAxis: {}, valueAxis: {} }); deepExtend(CategoricalPlotArea.prototype, PlotAreaEventsMixin); export default CategoricalPlotArea;