UNPKG

ag-charts-community

Version:

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

757 lines 30 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 rect_1 = require("../../../scene/shape/rect"); const text_1 = require("../../../scene/shape/text"); const bandScale_1 = require("../../../scale/bandScale"); const series_1 = require("../series"); const label_1 = require("../../label"); const node_1 = require("../../../scene/node"); const cartesianSeries_1 = require("./cartesianSeries"); const chartAxis_1 = require("../../chartAxis"); const chart_1 = require("../../chart"); const array_1 = require("../../../util/array"); const equal_1 = require("../../../util/equal"); const observable_1 = require("../../../util/observable"); const sanitize_1 = require("../../../util/sanitize"); const value_1 = require("../../../util/value"); const continuousScale_1 = require("../../../scale/continuousScale"); var BarSeriesNodeTag; (function (BarSeriesNodeTag) { BarSeriesNodeTag[BarSeriesNodeTag["Bar"] = 0] = "Bar"; BarSeriesNodeTag[BarSeriesNodeTag["Label"] = 1] = "Label"; })(BarSeriesNodeTag || (BarSeriesNodeTag = {})); var BarLabelPlacement; (function (BarLabelPlacement) { BarLabelPlacement["Inside"] = "inside"; BarLabelPlacement["Outside"] = "outside"; })(BarLabelPlacement = exports.BarLabelPlacement || (exports.BarLabelPlacement = {})); class BarSeriesLabel extends label_1.Label { constructor() { super(...arguments); this.placement = BarLabelPlacement.Inside; } } __decorate([ observable_1.reactive('change') ], BarSeriesLabel.prototype, "formatter", void 0); __decorate([ observable_1.reactive('change') ], BarSeriesLabel.prototype, "placement", void 0); exports.BarSeriesLabel = BarSeriesLabel; class BarSeriesTooltip extends series_1.SeriesTooltip { } __decorate([ observable_1.reactive('change') ], BarSeriesTooltip.prototype, "renderer", void 0); exports.BarSeriesTooltip = BarSeriesTooltip; function flat(arr, target = []) { arr.forEach(v => { if (Array.isArray(v)) { flat(v, target); } else { target.push(v); } }); return target; } function is2dArray(array) { return array.length > 0 && Array.isArray(array[0]); } class BarSeries extends cartesianSeries_1.CartesianSeries { constructor() { super(); // Need to put bar and label nodes into separate groups, because even though label nodes are // created after the bar nodes, this only guarantees that labels will always be on top of bars // on the first run. If on the next run more bars are added, they might clip the labels // rendered during the previous run. this.rectGroup = this.pickGroup.appendChild(new group_1.Group); this.labelGroup = this.group.appendChild(new group_1.Group); this.rectSelection = selection_1.Selection.select(this.rectGroup).selectAll(); this.labelSelection = selection_1.Selection.select(this.labelGroup).selectAll(); this.nodeData = []; this.xData = []; this.yData = []; this.yDomain = []; this.label = new BarSeriesLabel(); /** * The assumption is that the values will be reset (to `true`) * in the {@link yKeys} setter. */ this.seriesItemEnabled = new Map(); this.tooltip = new BarSeriesTooltip(); this.flipXY = false; 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; /** * Used to get the position of bars within each group. */ this.groupScale = new bandScale_1.BandScale(); this.directionKeys = { [chartAxis_1.ChartAxisDirection.X]: ['xKey'], [chartAxis_1.ChartAxisDirection.Y]: ['yKeys'] }; this._xKey = ''; this._xName = ''; this.cumYKeyCount = []; this.flatYKeys = undefined; // only set when a user used a flat array for yKeys this.hideInLegend = []; /** * yKeys: [['coffee']] - regular bars, each category has a single bar that shows a value for coffee * yKeys: [['coffee'], ['tea'], ['milk']] - each category has three bars that show values for coffee, tea and milk * yKeys: [['coffee', 'tea', 'milk']] - each category has a single bar with three stacks that show values for coffee, tea and milk * yKeys: [['coffee', 'tea', 'milk'], ['paper', 'ink']] - each category has 2 stacked bars, * first showing values for coffee, tea and milk and second values for paper and ink */ this._yKeys = []; this._grouped = false; /** * A map of `yKeys` to their names (used in legends and tooltips). * For example, if a key is `product_name` it's name can be a more presentable `Product Name`. */ this._yNames = {}; this._strokeWidth = 1; this.addEventListener('update', this.scheduleUpdate); this.label.enabled = false; this.label.addEventListener('change', this.scheduleUpdate, this); } getKeys(direction) { const { directionKeys } = this; const keys = directionKeys && directionKeys[this.flipXY ? chartAxis_1.flipChartAxisDirection(direction) : direction]; let values = []; if (keys) { keys.forEach(key => { const value = this[key]; if (value) { if (Array.isArray(value)) { values = values.concat(flat(value)); } else { values.push(value); } } }); } return values; } set xKey(value) { if (this._xKey !== value) { this._xKey = value; this.xData = []; this.scheduleData(); } } get xKey() { return this._xKey; } set xName(value) { if (this._xName !== value) { this._xName = value; this.scheduleUpdate(); } } get xName() { return this._xName; } set yKeys(yKeys) { let flatYKeys = undefined; // Convert from flat y-keys to grouped y-keys. if (!is2dArray(yKeys)) { flatYKeys = yKeys; yKeys = this.grouped ? flatYKeys.map(k => [k]) : [flatYKeys]; } if (!equal_1.equal(this._yKeys, yKeys)) { this.flatYKeys = flatYKeys ? flatYKeys : undefined; this._yKeys = yKeys; let prevYKeyCount = 0; this.cumYKeyCount = []; const visibleStacks = []; yKeys.forEach((stack, index) => { if (stack.length > 0) { visibleStacks.push(String(index)); } this.cumYKeyCount.push(prevYKeyCount); prevYKeyCount += stack.length; }); this.yData = []; const { seriesItemEnabled } = this; seriesItemEnabled.clear(); yKeys.forEach(stack => { stack.forEach(yKey => seriesItemEnabled.set(yKey, true)); }); const { groupScale } = this; groupScale.domain = visibleStacks; groupScale.padding = 0.1; groupScale.round = true; this.scheduleData(); } } get yKeys() { return this._yKeys; } set grouped(value) { if (this._grouped !== value) { this._grouped = value; if (this.flatYKeys) { this.yKeys = this.flatYKeys; } } } get grouped() { return this._grouped; } set yNames(values) { if (Array.isArray(values) && this.flatYKeys) { const map = {}; this.flatYKeys.forEach((k, i) => { map[k] = values[i]; }); values = map; } this._yNames = values; this.scheduleData(); } get yNames() { return this._yNames; } 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; } set strokeWidth(value) { if (this._strokeWidth !== value) { this._strokeWidth = value; this.scheduleUpdate(); } } get strokeWidth() { return this._strokeWidth; } set shadow(value) { if (this._shadow !== value) { this._shadow = value; this.scheduleUpdate(); } } get shadow() { return this._shadow; } onHighlightChange() { this.updateRectNodes(); } processData() { const { xKey, yKeys, seriesItemEnabled, xAxis } = this; const data = xKey && yKeys.length && this.data ? this.data : []; if (!xAxis) { return false; } 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 value_1.isDiscrete(datum[xKey]) ? datum[xKey] : String(datum[xKey]); }); this.yData = data.map(datum => yKeys.map(stack => { return stack.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; }); })); // Contains min/max values for each stack in each group, // where min is zero and max is a positive total of all values in the stack // or min is a negative total of all values in the stack and max is zero. const yMinMax = this.yData.map(group => group.map(stack => array_1.findMinMax(stack))); const { yData, normalizedTo } = this; // Calculate the sum of the absolute values of all items in each stack in each group. Used for normalization of stacked bars. const yAbsTotal = this.yData.map(group => group.map(stack => stack.reduce((acc, stack) => { acc += Math.abs(stack); return acc; }, 0))); const yLargestMinMax = this.findLargestMinMax(yMinMax); let yMin; let yMax; if (normalizedTo && isFinite(normalizedTo)) { yMin = yLargestMinMax.min < 0 ? -normalizedTo : 0; yMax = yLargestMinMax.max > 0 ? normalizedTo : 0; yData.forEach((group, i) => { group.forEach((stack, j) => { stack.forEach((y, k) => { stack[k] = y / yAbsTotal[i][j] * normalizedTo; }); }); }); } else { yMin = yLargestMinMax.min; yMax = yLargestMinMax.max; } if (yMin === yMax && yMin === 0) { yMax = 1; } this.yDomain = this.fixNumericExtent([yMin, yMax], 'y'); this.fireEvent({ type: 'dataProcessed' }); return true; } findLargestMinMax(groups) { let tallestStackMin = 0; let tallestStackMax = 0; for (const group of groups) { for (const stack of group) { if (stack.min < tallestStackMin) { tallestStackMin = stack.min; } if (stack.max > tallestStackMax) { tallestStackMax = stack.max; } } } return { min: tallestStackMin, max: tallestStackMax }; } getDomain(direction) { if (this.flipXY) { direction = chartAxis_1.flipChartAxisDirection(direction); } if (direction === chartAxis_1.ChartAxisDirection.X) { return this.xData; } else { return this.yDomain; } } fireNodeClickEvent(event, datum) { this.fireEvent({ type: 'nodeClick', event, series: this, datum: datum.datum, xKey: this.xKey, yKey: datum.yKey }); } getCategoryAxis() { return this.flipXY ? this.yAxis : this.xAxis; } getValueAxis() { return this.flipXY ? this.xAxis : this.yAxis; } createNodeData() { const { chart, data, visible } = this; const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); if (!(chart && data && visible && xAxis && yAxis) || chart.layoutPending || chart.dataPending) { return []; } const { flipXY } = this; const xScale = xAxis.scale; const yScale = yAxis.scale; const { groupScale, yKeys, cumYKeyCount, fills, strokes, strokeWidth, seriesItemEnabled, xData, yData, label } = this; const { fontStyle: labelFontStyle, fontWeight: labelFontWeight, fontSize: labelFontSize, fontFamily: labelFontFamily, color: labelColor, formatter: labelFormatter, placement: labelPlacement } = label; groupScale.range = [0, xScale.bandwidth]; const grouped = true; const barWidth = grouped ? groupScale.bandwidth : xScale.bandwidth; const nodeData = []; xData.forEach((group, groupIndex) => { const seriesDatum = data[groupIndex]; const x = xScale.convert(group); const groupYs = yData[groupIndex]; // y-data for groups of stacks for (let stackIndex = 0; stackIndex < groupYs.length; stackIndex++) { const stackYs = groupYs[stackIndex]; // y-data for a stack within a group let prevMinY = 0; let prevMaxY = 0; for (let levelIndex = 0; levelIndex < stackYs.length; levelIndex++) { const currY = stackYs[levelIndex]; const yKey = yKeys[stackIndex][levelIndex]; const barX = grouped ? x + groupScale.convert(String(stackIndex)) : x; // Bars outside of visible range are not rendered, so we create node data // only for the visible subset of user data. if (!xAxis.inRange(barX, barWidth)) { continue; } const prevY = currY < 0 ? prevMinY : prevMaxY; const continuousY = yScale instanceof continuousScale_1.ContinuousScale; const y = yScale.convert(prevY + currY, continuousY ? continuousScale_1.clamper : undefined); const bottomY = yScale.convert(prevY, continuousY ? continuousScale_1.clamper : undefined); const yValue = seriesDatum[yKey]; // unprocessed y-value let labelText; if (labelFormatter) { labelText = labelFormatter({ value: value_1.isNumber(yValue) ? yValue : undefined }); } else { labelText = value_1.isNumber(yValue) ? yValue.toFixed(2) : ''; } let labelX; let labelY; if (flipXY) { labelY = barX + barWidth / 2; if (labelPlacement === BarLabelPlacement.Inside) { labelX = y + (yValue >= 0 ? -1 : 1) * Math.abs(bottomY - y) / 2; } else { labelX = y + (yValue >= 0 ? 1 : -1) * 4; } } else { labelX = barX + barWidth / 2; if (labelPlacement === BarLabelPlacement.Inside) { labelY = y + (yValue >= 0 ? 1 : -1) * Math.abs(bottomY - y) / 2; } else { labelY = y + (yValue >= 0 ? -3 : 4); } } let labelTextAlign; let labelTextBaseline; if (labelPlacement === BarLabelPlacement.Inside) { labelTextAlign = 'center'; labelTextBaseline = 'middle'; } else { labelTextAlign = flipXY ? (yValue >= 0 ? 'start' : 'end') : 'center'; labelTextBaseline = flipXY ? 'middle' : (yValue >= 0 ? 'bottom' : 'top'); } const colorIndex = cumYKeyCount[stackIndex] + levelIndex; nodeData.push({ index: groupIndex, series: this, itemId: yKey, datum: seriesDatum, yValue, yKey, x: flipXY ? Math.min(y, bottomY) : barX, y: flipXY ? barX : Math.min(y, bottomY), width: flipXY ? Math.abs(bottomY - y) : barWidth, height: flipXY ? barWidth : Math.abs(bottomY - y), fill: fills[colorIndex % fills.length], stroke: strokes[colorIndex % strokes.length], strokeWidth, label: seriesItemEnabled.get(yKey) && labelText ? { text: labelText, fontStyle: labelFontStyle, fontWeight: labelFontWeight, fontSize: labelFontSize, fontFamily: labelFontFamily, textAlign: labelTextAlign, textBaseline: labelTextBaseline, fill: labelColor, x: labelX, y: labelY } : undefined }); if (currY < 0) { prevMinY += currY; } else { prevMaxY += currY; } } } }); return this.nodeData = nodeData; } update() { this.updatePending = false; this.updateSelections(); this.updateNodes(); } updateSelections() { if (!this.nodeDataPending) { return; } this.nodeDataPending = false; this.createNodeData(); this.updateRectSelection(); this.updateLabelSelection(); } updateNodes() { this.group.visible = this.visible; this.updateRectNodes(); this.updateLabelNodes(); } updateRectSelection() { const updateRects = this.rectSelection.setData(this.nodeData); updateRects.exit.remove(); const enterRects = updateRects.enter.append(rect_1.Rect).each(rect => { rect.tag = BarSeriesNodeTag.Bar; rect.crisp = true; }); this.rectSelection = updateRects.merge(enterRects); } updateRectNodes() { if (!this.chart) { return; } const { fills, strokes, fillOpacity, strokeOpacity, shadow, formatter, xKey, flipXY, yKeys, chart: { highlightedDatum }, highlightStyle: { fill: deprecatedFill, stroke: deprecatedStroke, strokeWidth: deprecatedStrokeWidth, item: { fill: highlightedFill = deprecatedFill, stroke: highlightedStroke = deprecatedStroke, strokeWidth: highlightedDatumStrokeWidth = deprecatedStrokeWidth, } } } = this; this.rectSelection.each((rect, datum, index) => { let colorIndex = 0; let i = 0; for (let j = 0; j < yKeys.length; j++) { const stack = yKeys[j]; i = stack.indexOf(datum.yKey); if (i >= 0) { colorIndex += i; break; } colorIndex += stack.length; } const isDatumHighlighted = datum === highlightedDatum; const fill = isDatumHighlighted && highlightedFill !== undefined ? highlightedFill : fills[colorIndex % fills.length]; const stroke = isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : strokes[colorIndex % fills.length]; const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined ? highlightedDatumStrokeWidth : this.getStrokeWidth(this.strokeWidth, datum); let format = undefined; if (formatter) { format = formatter({ datum: datum.datum, fill, stroke, strokeWidth, highlighted: isDatumHighlighted, xKey, yKey: datum.yKey }); } rect.x = datum.x; rect.y = datum.y; rect.width = datum.width; rect.height = datum.height; rect.fill = format && format.fill || fill; rect.stroke = format && format.stroke || stroke; rect.strokeWidth = format && format.strokeWidth !== undefined ? format.strokeWidth : strokeWidth; rect.fillOpacity = fillOpacity; rect.strokeOpacity = strokeOpacity; rect.lineDash = this.lineDash; rect.lineDashOffset = this.lineDashOffset; rect.fillShadow = shadow; // Prevent stroke from rendering for zero height columns and zero width bars. rect.visible = flipXY ? datum.width > 0 : datum.height > 0; rect.zIndex = isDatumHighlighted ? series_1.Series.highlightedZIndex : index; rect.opacity = this.getOpacity(datum); }); } updateLabelSelection() { const updateLabels = this.labelSelection.setData(this.nodeData); updateLabels.exit.remove(); const enterLabels = updateLabels.enter.append(text_1.Text).each(text => { text.tag = BarSeriesNodeTag.Label; text.pointerEvents = node_1.PointerEvents.None; }); this.labelSelection = updateLabels.merge(enterLabels); } updateLabelNodes() { if (!this.chart) { return; } const { chart: { highlightedDatum }, label: { enabled: labelEnabled, fontStyle, fontWeight, fontSize, fontFamily, color } } = this; this.labelSelection.each((text, datum, index) => { const label = datum.label; 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 = label.x; text.y = label.y; text.fill = color; text.visible = true; text.opacity = this.getOpacity(datum); } else { text.visible = false; } }); } getTooltipHtml(nodeDatum) { const { xKey, yKeys, yData } = this; const xAxis = this.getCategoryAxis(); const yAxis = this.getValueAxis(); const { yKey } = nodeDatum; if (!yData.length || !xKey || !yKey || !xAxis || !yAxis) { return ''; } const yGroup = yData[nodeDatum.index]; let fillIndex = 0; let i = 0; let j = 0; for (; j < yKeys.length; j++) { const stack = yKeys[j]; i = stack.indexOf(yKey); if (i >= 0) { fillIndex += i; break; } fillIndex += stack.length; } const { xName, yNames, fills, strokes, tooltip, formatter } = this; const { renderer: tooltipRenderer } = tooltip; const datum = nodeDatum.datum; const yName = yNames[yKey]; const fill = fills[fillIndex % fills.length]; const stroke = strokes[fillIndex % fills.length]; const strokeWidth = this.getStrokeWidth(this.strokeWidth, datum); const xValue = datum[xKey]; const yValue = datum[yKey]; const processedYValue = yGroup[j][i]; const xString = sanitize_1.sanitizeHtml(xAxis.formatDatum(xValue)); const yString = sanitize_1.sanitizeHtml(yAxis.formatDatum(yValue)); const title = sanitize_1.sanitizeHtml(yName); const content = xString + ': ' + yString; let format = undefined; if (formatter) { format = formatter({ datum, fill, stroke, strokeWidth, highlighted: false, xKey, yKey }); } const color = format && format.fill || fill; const defaults = { title, backgroundColor: color, content }; if (tooltipRenderer) { return chart_1.toTooltipHtml(tooltipRenderer({ datum, xKey, xValue, xName, yKey, yValue, processedYValue, yName, color }), defaults); } return chart_1.toTooltipHtml(defaults); } listSeriesItems(legendData) { const { id, data, xKey, yKeys, yNames, cumYKeyCount, seriesItemEnabled, hideInLegend, fills, strokes, fillOpacity, strokeOpacity } = this; if (data && data.length && xKey && yKeys.length) { this.yKeys.forEach((stack, stackIndex) => { stack.forEach((yKey, levelIndex) => { if (hideInLegend.indexOf(yKey) < 0) { const colorIndex = cumYKeyCount[stackIndex] + levelIndex; legendData.push({ id, itemId: yKey, enabled: seriesItemEnabled.get(yKey) || false, label: { text: yNames[yKey] || yKey }, marker: { fill: fills[colorIndex % fills.length], stroke: strokes[colorIndex % strokes.length], fillOpacity: fillOpacity, strokeOpacity: strokeOpacity } }); } }); }); } } toggleSeriesItem(itemId, enabled) { const { seriesItemEnabled } = this; seriesItemEnabled.set(itemId, enabled); const yKeys = this.yKeys.map(stack => stack.slice()); // deep clone seriesItemEnabled.forEach((enabled, yKey) => { if (!enabled) { yKeys.forEach(stack => { const index = stack.indexOf(yKey); if (index >= 0) { stack.splice(index, 1); } }); } }); const visibleStacks = []; yKeys.forEach((stack, index) => { if (stack.length > 0) { visibleStacks.push(String(index)); } }); this.groupScale.domain = visibleStacks; this.scheduleData(); } } BarSeries.className = 'BarSeries'; BarSeries.type = 'bar'; __decorate([ observable_1.reactive('dataChange') ], BarSeries.prototype, "flipXY", void 0); __decorate([ observable_1.reactive('dataChange') ], BarSeries.prototype, "fills", void 0); __decorate([ observable_1.reactive('dataChange') ], BarSeries.prototype, "strokes", void 0); __decorate([ observable_1.reactive('update') ], BarSeries.prototype, "fillOpacity", void 0); __decorate([ observable_1.reactive('update') ], BarSeries.prototype, "strokeOpacity", void 0); __decorate([ observable_1.reactive('update') ], BarSeries.prototype, "lineDash", void 0); __decorate([ observable_1.reactive('update') ], BarSeries.prototype, "lineDashOffset", void 0); __decorate([ observable_1.reactive('update') ], BarSeries.prototype, "formatter", void 0); __decorate([ observable_1.reactive('layoutChange') ], BarSeries.prototype, "hideInLegend", void 0); exports.BarSeries = BarSeries; //# sourceMappingURL=barSeries.js.map