UNPKG

ag-charts-community

Version:

Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue

640 lines 25.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); const group_1 = require("../../../scene/group"); const selection_1 = require("../../../scene/selection"); const series_1 = require("../series"); const node_1 = require("../../../scene/node"); const path_1 = require("../../../scene/shape/path"); const cartesianSeries_1 = require("./cartesianSeries"); const chartAxis_1 = require("../../chartAxis"); const util_1 = require("../../marker/util"); const chart_1 = require("../../chart"); const array_1 = require("../../../util/array"); const equal_1 = require("../../../util/equal"); const observable_1 = require("../../../util/observable"); const string_1 = require("../../../util/string"); const text_1 = require("../../../scene/shape/text"); const label_1 = require("../../label"); const sanitize_1 = require("../../../util/sanitize"); const value_1 = require("../../../util/value"); const continuousScale_1 = require("../../../scale/continuousScale"); class AreaSeriesLabel extends label_1.Label { } __decorate([ observable_1.reactive('change') ], AreaSeriesLabel.prototype, "formatter", void 0); class AreaSeriesTooltip extends series_1.SeriesTooltip { } __decorate([ observable_1.reactive('change') ], AreaSeriesTooltip.prototype, "renderer", void 0); __decorate([ observable_1.reactive('change') ], AreaSeriesTooltip.prototype, "format", void 0); exports.AreaSeriesTooltip = AreaSeriesTooltip; class AreaSeries extends cartesianSeries_1.CartesianSeries { constructor() { super(); this.tooltip = new AreaSeriesTooltip(); this.areaGroup = this.group.insertBefore(new group_1.Group, this.pickGroup); this.strokeGroup = this.group.insertBefore(new group_1.Group, this.pickGroup); this.markerGroup = this.pickGroup.appendChild(new group_1.Group); this.labelGroup = this.group.appendChild(new group_1.Group); this.fillSelection = selection_1.Selection.select(this.areaGroup).selectAll(); this.strokeSelection = selection_1.Selection.select(this.strokeGroup).selectAll(); this.markerSelection = selection_1.Selection.select(this.markerGroup).selectAll(); this.labelSelection = selection_1.Selection.select(this.labelGroup).selectAll(); /** * The assumption is that the values will be reset (to `true`) * in the {@link yKeys} setter. */ this.seriesItemEnabled = new Map(); this.xData = []; this.yData = []; this.areaSelectionData = []; this.markerSelectionData = []; this.labelSelectionData = []; this.yDomain = []; this.directionKeys = { x: ['xKey'], y: ['yKeys'] }; this.marker = new cartesianSeries_1.CartesianSeriesMarker(); this.label = new AreaSeriesLabel(); this.fills = [ '#c16068', '#a2bf8a', '#ebcc87', '#80a0c3', '#b58dae', '#85c0d1' ]; this.strokes = [ '#874349', '#718661', '#a48f5f', '#5a7088', '#7f637a', '#5d8692' ]; this.fillOpacity = 1; this.strokeOpacity = 1; this.lineDash = [0]; this.lineDashOffset = 0; this._xKey = ''; this.xName = ''; this._yKeys = []; this.yNames = []; this.strokeWidth = 2; this.addEventListener('update', this.scheduleUpdate); const { marker, label } = this; marker.enabled = false; marker.addPropertyListener('shape', this.onMarkerShapeChange, this); marker.addEventListener('change', this.scheduleUpdate, this); label.enabled = false; label.addEventListener('change', this.scheduleUpdate, this); } onMarkerShapeChange() { this.markerSelection = this.markerSelection.setData([]); this.markerSelection.exit.remove(); this.fireEvent({ type: 'legendChange' }); } set xKey(value) { if (this._xKey !== value) { this._xKey = value; this.xData = []; this.scheduleData(); } } get xKey() { return this._xKey; } set yKeys(values) { if (!equal_1.equal(this._yKeys, values)) { this._yKeys = values; this.yData = []; const { seriesItemEnabled } = this; seriesItemEnabled.clear(); values.forEach(key => seriesItemEnabled.set(key, true)); this.scheduleData(); } } get yKeys() { return this._yKeys; } setColors(fills, strokes) { this.fills = fills; this.strokes = strokes; } set normalizedTo(value) { const absValue = value ? Math.abs(value) : undefined; if (this._normalizedTo !== absValue) { this._normalizedTo = absValue; this.scheduleData(); } } get normalizedTo() { return this._normalizedTo; } processData() { const { xKey, yKeys, seriesItemEnabled } = this; const data = xKey && yKeys.length && this.data ? this.data : []; // If the data is an array of rows like so: // // [{ // xKy: 'Jan', // yKey1: 5, // yKey2: 7, // yKey3: -9, // }, { // xKey: 'Feb', // yKey1: 10, // yKey2: -15, // yKey3: 20 // }] // let keysFound = true; // only warn once this.xData = data.map(datum => { if (keysFound && !(xKey in datum)) { keysFound = false; console.warn(`The key '${xKey}' was not found in the data: `, datum); } return datum[xKey]; }); this.yData = data.map(datum => yKeys.map(yKey => { if (keysFound && !(yKey in datum)) { keysFound = false; console.warn(`The key '${yKey}' was not found in the data: `, datum); } const value = datum[yKey]; return isFinite(value) && seriesItemEnabled.get(yKey) ? value : 0; })); // xData: ['Jan', 'Feb'] // // yData: [ // [5, 7, -9], // [10, -15, 20] // ] const { yData, normalizedTo } = this; const yMinMax = yData.map(values => array_1.findMinMax(values)); // used for normalization const yLargestMinMax = this.findLargestMinMax(yMinMax); // Calculate the sum of the absolute values of all items in each stack. Used for normalization of stacked areas. const yAbsTotal = this.yData.map(values => values.reduce((acc, stack) => { acc += Math.abs(stack); return acc; }, 0)); let yMin; let yMax; if (normalizedTo && isFinite(normalizedTo)) { yMin = yLargestMinMax.min < 0 ? -normalizedTo : 0; yMax = yLargestMinMax.max > 0 ? normalizedTo : 0; yData.forEach((stack, i) => stack.forEach((y, j) => stack[j] = y / yAbsTotal[i] * normalizedTo)); } else { yMin = yLargestMinMax.min; yMax = yLargestMinMax.max; } if (yMin === 0 && yMax === 0) { yMax = 1; } this.yDomain = this.fixNumericExtent([yMin, yMax], 'y'); this.fireEvent({ type: 'dataProcessed' }); return true; } findLargestMinMax(totals) { let min = 0; let max = 0; for (const total of totals) { if (total.min < min) { min = total.min; } if (total.max > max) { max = total.max; } } return { min, max }; } getDomain(direction) { if (direction === chartAxis_1.ChartAxisDirection.X) { return this.xData; } else { return this.yDomain; } } update() { this.updatePending = false; this.updateSelections(); this.updateNodes(); } updateSelections() { if (!this.nodeDataPending) { return; } this.nodeDataPending = false; this.createSelectionData(); this.updateFillSelection(); this.updateStrokeSelection(); this.updateMarkerSelection(); this.updateLabelSelection(); } updateNodes() { this.group.visible = this.visible && this.xData.length > 0 && this.yData.length > 0; this.updateFillNodes(); this.updateStrokeNodes(); this.updateMarkerNodes(); this.updateLabelNodes(); } createSelectionData() { const { data, xAxis, yAxis, xData, yData, areaSelectionData, markerSelectionData, labelSelectionData } = this; if (!data || !xAxis || !yAxis || !xData.length || !yData.length) { return; } const { yKeys, marker, label, fills, strokes } = this; const { scale: xScale } = xAxis; const { scale: yScale } = yAxis; const xOffset = (xScale.bandwidth || 0) / 2; const yOffset = (yScale.bandwidth || 0) / 2; const last = xData.length * 2 - 1; areaSelectionData.length = 0; markerSelectionData.length = 0; labelSelectionData.length = 0; xData.forEach((xDatum, i) => { const yDatum = yData[i]; const seriesDatum = data[i]; const x = xScale.convert(xDatum) + xOffset; let prevMin = 0; let prevMax = 0; yDatum.forEach((curr, j) => { const prev = curr < 0 ? prevMin : prevMax; const continuousY = yScale instanceof continuousScale_1.ContinuousScale; const y = yScale.convert(prev + curr, continuousY ? continuousScale_1.clamper : undefined); const yKey = yKeys[j]; const yValue = seriesDatum[yKey]; if (marker) { markerSelectionData.push({ index: i, series: this, itemId: yKey, datum: seriesDatum, yValue, yKey, point: { x, y }, fill: fills[j % fills.length], stroke: strokes[j % strokes.length] }); } let labelText; if (label.formatter) { labelText = label.formatter({ value: yValue }); } else { labelText = value_1.isNumber(yValue) ? yValue.toFixed(2) : String(yValue); } if (label) { labelSelectionData.push({ index: i, itemId: yKey, point: { x, y }, label: labelText ? { text: labelText, fontStyle: label.fontStyle, fontWeight: label.fontWeight, fontSize: label.fontSize, fontFamily: label.fontFamily, textAlign: 'center', textBaseline: 'bottom', fill: label.color } : undefined }); } const areaDatum = areaSelectionData[j] || (areaSelectionData[j] = { itemId: yKey, points: [] }); const areaPoints = areaDatum.points; areaPoints[i] = { x, y }; areaPoints[last - i] = { x, y: yScale.convert(prev) + yOffset }; // bottom y if (curr < 0) { prevMin += curr; } else { prevMax += curr; } }); }); } updateFillSelection() { const updateFills = this.fillSelection.setData(this.areaSelectionData); updateFills.exit.remove(); const enterFills = updateFills.enter.append(path_1.Path) .each(path => { path.lineJoin = 'round'; path.stroke = undefined; path.pointerEvents = node_1.PointerEvents.None; }); this.fillSelection = updateFills.merge(enterFills); } updateFillNodes() { const { fills, fillOpacity, strokes, strokeOpacity, strokeWidth, shadow, seriesItemEnabled } = this; this.fillSelection.each((shape, datum, index) => { const path = shape.path; shape.fill = fills[index % fills.length]; shape.fillOpacity = fillOpacity; shape.stroke = strokes[index % strokes.length]; shape.strokeOpacity = strokeOpacity; shape.strokeWidth = strokeWidth; shape.lineDash = this.lineDash; shape.lineDashOffset = this.lineDashOffset; shape.fillShadow = shadow; shape.visible = !!seriesItemEnabled.get(datum.itemId); shape.opacity = this.getOpacity(datum); path.clear(); const { points } = datum; points.forEach(({ x, y }, i) => { if (i > 0) { path.lineTo(x, y); } else { path.moveTo(x, y); } }); path.closePath(); }); } updateStrokeSelection() { const updateStrokes = this.strokeSelection.setData(this.areaSelectionData); updateStrokes.exit.remove(); const enterStrokes = updateStrokes.enter.append(path_1.Path) .each(path => { path.fill = undefined; path.lineJoin = path.lineCap = 'round'; path.pointerEvents = node_1.PointerEvents.None; }); this.strokeSelection = updateStrokes.merge(enterStrokes); } updateStrokeNodes() { if (!this.data) { return; } const { data, strokes, strokeOpacity, seriesItemEnabled } = this; this.strokeSelection.each((shape, datum, index) => { const path = shape.path; shape.visible = !!seriesItemEnabled.get(datum.itemId); shape.opacity = this.getOpacity(datum); shape.stroke = strokes[index % strokes.length]; shape.strokeWidth = this.getStrokeWidth(this.strokeWidth, datum); shape.strokeOpacity = strokeOpacity; shape.lineDash = this.lineDash; shape.lineDashOffset = this.lineDashOffset; path.clear(); const { points } = datum; // The stroke doesn't go all the way around the fill, only on top, // that's why we iterate until `data.length` (rather than `points.length`) and stop. for (let i = 0; i < data.length; i++) { const { x, y } = points[i]; if (i > 0) { path.lineTo(x, y); } else { path.moveTo(x, y); } } }); } updateMarkerSelection() { const MarkerShape = util_1.getMarker(this.marker.shape); const data = MarkerShape ? this.markerSelectionData : []; const updateMarkers = this.markerSelection.setData(data); updateMarkers.exit.remove(); const enterMarkers = updateMarkers.enter.append(MarkerShape); this.markerSelection = updateMarkers.merge(enterMarkers); } updateMarkerNodes() { if (!this.chart) { return; } const { xKey, marker, seriesItemEnabled, yKeys, fills, strokes, chart: { highlightedDatum }, highlightStyle: { fill: deprecatedFill, stroke: deprecatedStroke, strokeWidth: deprecatedStrokeWidth, item: { fill: highlightedFill = deprecatedFill, stroke: highlightedStroke = deprecatedStroke, strokeWidth: highlightedDatumStrokeWidth = deprecatedStrokeWidth, } } } = this; const { size, formatter } = marker; const markerStrokeWidth = marker.strokeWidth !== undefined ? marker.strokeWidth : this.strokeWidth; this.markerSelection.each((node, datum) => { const yKeyIndex = yKeys.indexOf(datum.yKey); const isDatumHighlighted = datum === highlightedDatum; const fill = isDatumHighlighted && highlightedFill !== undefined ? highlightedFill : marker.fill || fills[yKeyIndex % fills.length]; const stroke = isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : marker.stroke || strokes[yKeyIndex % fills.length]; const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined ? highlightedDatumStrokeWidth : markerStrokeWidth; let format = undefined; if (formatter) { format = formatter({ datum: datum.datum, xKey, yKey: datum.yKey, fill, stroke, strokeWidth, size, highlighted: isDatumHighlighted }); } node.fill = format && format.fill || fill; node.stroke = format && format.stroke || stroke; node.strokeWidth = format && format.strokeWidth !== undefined ? format.strokeWidth : strokeWidth; node.size = format && format.size !== undefined ? format.size : size; node.translationX = datum.point.x; node.translationY = datum.point.y; node.visible = marker.enabled && node.size > 0 && !!seriesItemEnabled.get(datum.yKey) && !isNaN(datum.point.x); node.opacity = this.getOpacity(datum); }); } updateLabelSelection() { const updateLabels = this.labelSelection.setData(this.labelSelectionData); updateLabels.exit.remove(); const enterLabels = updateLabels.enter.append(text_1.Text); this.labelSelection = updateLabels.merge(enterLabels); } updateLabelNodes() { if (!this.chart) { return; } const { enabled: labelEnabled, fontStyle, fontWeight, fontSize, fontFamily, color } = this.label; this.labelSelection.each((text, datum) => { const { point, label } = datum; if (label && labelEnabled) { text.fontStyle = fontStyle; text.fontWeight = fontWeight; text.fontSize = fontSize; text.fontFamily = fontFamily; text.textAlign = label.textAlign; text.textBaseline = label.textBaseline; text.text = label.text; text.x = point.x; text.y = point.y - 10; text.fill = color; text.visible = true; text.opacity = this.getOpacity(datum); } else { text.visible = false; } }); } getNodeData() { return this.markerSelectionData; } fireNodeClickEvent(event, datum) { this.fireEvent({ type: 'nodeClick', event, series: this, datum: datum.datum, xKey: this.xKey, yKey: datum.yKey }); } getTooltipHtml(nodeDatum) { const { xKey } = this; const { yKey } = nodeDatum; if (!(xKey && yKey)) { return ''; } const datum = nodeDatum.datum; const xValue = datum[xKey]; const yValue = datum[yKey]; const { xAxis, yAxis } = this; if (!(xAxis && yAxis && value_1.isNumber(yValue))) { return ''; } const x = xAxis.scale.convert(xValue); const y = yAxis.scale.convert(yValue); // Don't show the tooltip for the off-screen markers. // Node: some markers might still go off-screen despite virtual rendering // (to connect the dots and render the area properly). if (!(xAxis.inRange(x) && yAxis.inRange(y))) { return ''; } const { xName, yKeys, yNames, yData, fills, strokes, tooltip, marker } = this; const { size, formatter: markerFormatter, strokeWidth: markerStrokeWidth, fill: markerFill, stroke: markerStroke } = marker; const xString = xAxis.formatDatum(xValue); const yString = yAxis.formatDatum(yValue); const yKeyIndex = yKeys.indexOf(yKey); const yGroup = yData[nodeDatum.index]; const processedYValue = yGroup[yKeyIndex]; const yName = yNames[yKeyIndex]; const title = sanitize_1.sanitizeHtml(yName); const content = sanitize_1.sanitizeHtml(xString + ': ' + yString); const strokeWidth = markerStrokeWidth !== undefined ? markerStrokeWidth : this.strokeWidth; const fill = markerFill || fills[yKeyIndex % fills.length]; const stroke = markerStroke || strokes[yKeyIndex % fills.length]; let format = undefined; if (markerFormatter) { format = markerFormatter({ datum, xKey, yKey, fill, stroke, strokeWidth, size, highlighted: false }); } const color = format && format.fill || markerFill; const defaults = { title, backgroundColor: color, content }; const { renderer: tooltipRenderer, format: tooltipFormat } = tooltip; if (tooltipFormat || tooltipRenderer) { const params = { datum, xKey, xName, xValue, yKey, yValue, processedYValue, yName, color }; if (tooltipFormat) { return chart_1.toTooltipHtml({ content: string_1.interpolate(tooltipFormat, params) }, defaults); } if (tooltipRenderer) { return chart_1.toTooltipHtml(tooltipRenderer(params), defaults); } } return chart_1.toTooltipHtml(defaults); } listSeriesItems(legendData) { const { data, id, xKey, yKeys, yNames, seriesItemEnabled, marker, fills, strokes, fillOpacity, strokeOpacity } = this; if (data && data.length && xKey && yKeys.length) { yKeys.forEach((yKey, index) => { legendData.push({ id, itemId: yKey, enabled: seriesItemEnabled.get(yKey) || false, label: { text: yNames[index] || yKeys[index] }, marker: { shape: marker.shape, fill: marker.fill || fills[index % fills.length], stroke: marker.stroke || strokes[index % strokes.length], fillOpacity, strokeOpacity } }); }); } } toggleSeriesItem(itemId, enabled) { this.seriesItemEnabled.set(itemId, enabled); this.scheduleData(); } } AreaSeries.className = 'AreaSeries'; AreaSeries.type = 'area'; __decorate([ observable_1.reactive('dataChange') ], AreaSeries.prototype, "fills", void 0); __decorate([ observable_1.reactive('dataChange') ], AreaSeries.prototype, "strokes", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "fillOpacity", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "strokeOpacity", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "lineDash", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "lineDashOffset", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "xName", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "yNames", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "strokeWidth", void 0); __decorate([ observable_1.reactive('update') ], AreaSeries.prototype, "shadow", void 0); exports.AreaSeries = AreaSeries; //# sourceMappingURL=areaSeries.js.map