UNPKG

ag-charts-community

Version:

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

518 lines 19.5 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 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 observable_1 = require("../../../util/observable"); const ticks_1 = require("../../../util/ticks"); const sanitize_1 = require("../../../util/sanitize"); const value_1 = require("../../../util/value"); var HistogramSeriesNodeTag; (function (HistogramSeriesNodeTag) { HistogramSeriesNodeTag[HistogramSeriesNodeTag["Bin"] = 0] = "Bin"; HistogramSeriesNodeTag[HistogramSeriesNodeTag["Label"] = 1] = "Label"; })(HistogramSeriesNodeTag || (HistogramSeriesNodeTag = {})); class HistogramSeriesLabel extends label_1.Label { } __decorate([ observable_1.reactive('change') ], HistogramSeriesLabel.prototype, "formatter", void 0); const defaultBinCount = 10; const aggregationFunctions = { count: bin => bin.data.length, sum: (bin, yKey) => bin.data.reduce((acc, datum) => acc + datum[yKey], 0), mean: (bin, yKey) => aggregationFunctions.sum(bin, yKey) / aggregationFunctions.count(bin, yKey) }; class HistogramBin { constructor([domainMin, domainMax]) { this.data = []; this.aggregatedValue = 0; this.frequency = 0; this.domain = [domainMin, domainMax]; } ; addDatum(datum) { this.data.push(datum); this.frequency++; } get domainWidth() { const [domainMin, domainMax] = this.domain; return domainMax - domainMin; } get relativeHeight() { return this.aggregatedValue / this.domainWidth; } calculateAggregatedValue(aggregationName, yKey) { if (!yKey) { // not having a yKey forces us into a frequency plot aggregationName = 'count'; } const aggregationFunction = aggregationFunctions[aggregationName]; this.aggregatedValue = aggregationFunction(this, yKey); } getY(areaPlot) { return areaPlot ? this.relativeHeight : this.aggregatedValue; } } exports.HistogramBin = HistogramBin; class HistogramSeriesTooltip extends series_1.SeriesTooltip { } __decorate([ observable_1.reactive('change') ], HistogramSeriesTooltip.prototype, "renderer", void 0); exports.HistogramSeriesTooltip = HistogramSeriesTooltip; class HistogramSeries extends cartesianSeries_1.CartesianSeries { constructor() { super(); // Need to put column and label nodes into separate groups, because even though label nodes are // created after the column nodes, this only guarantees that labels will always be on top of columns // on the first run. If on the next run more columns are added, they might clip the labels // rendered during the previous run. this.rectGroup = this.pickGroup.appendChild(new group_1.Group()); this.textGroup = this.group.appendChild(new group_1.Group()); this.rectSelection = selection_1.Selection.select(this.rectGroup).selectAll(); this.textSelection = selection_1.Selection.select(this.textGroup).selectAll(); this.binnedData = []; this.xDomain = []; this.yDomain = []; this.label = new HistogramSeriesLabel(); this.seriesItemEnabled = true; this.tooltip = new HistogramSeriesTooltip(); this.fill = undefined; this.stroke = undefined; this.fillOpacity = 1; this.strokeOpacity = 1; this.lineDash = [0]; this.lineDashOffset = 0; this.directionKeys = { [chartAxis_1.ChartAxisDirection.X]: ['xKey'], [chartAxis_1.ChartAxisDirection.Y]: ['yKey'] }; this._xKey = ''; this._areaPlot = false; this._bins = undefined; this._aggregation = 'count'; this._binCount = undefined; this._xName = ''; this._yKey = ''; this._yName = ''; this._strokeWidth = 1; this.label.enabled = false; this.label.addEventListener('change', this.scheduleUpdate, this); } getKeys(direction) { const { directionKeys } = this; const keys = directionKeys && directionKeys[direction]; const values = []; if (keys) { keys.forEach(key => { const value = this[key]; if (value) { if (Array.isArray(value)) { values.push(...value); } else { values.push(value); } } }); } return values; } set xKey(value) { if (this._xKey !== value) { this._xKey = value; this.scheduleData(); } } get xKey() { return this._xKey; } set areaPlot(c) { this._areaPlot = c; this.scheduleData(); } get areaPlot() { return this._areaPlot; } set bins(bins) { this._bins = bins; this.scheduleData(); } get bins() { return this._bins; } set aggregation(aggregation) { this._aggregation = aggregation; this.scheduleData(); } get aggregation() { return this._aggregation; } set binCount(binCount) { this._binCount = binCount; this.scheduleData(); } get binCount() { return this._binCount; } set xName(value) { if (this._xName !== value) { this._xName = value; this.scheduleUpdate(); } } get xName() { return this._xName; } set yKey(yKey) { this._yKey = yKey; this.seriesItemEnabled = true; this.scheduleData(); } get yKey() { return this._yKey; } set yName(values) { this._yName = values; this.scheduleData(); } get yName() { return this._yName; } 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(); } setColors(fills, strokes) { this.fill = fills[0]; this.stroke = strokes[0]; } // During processData phase, used to unify different ways of the user specifying // the bins. Returns bins in format[[min1, max1], [min2, max2], ... ]. deriveBins() { const { bins, binCount } = this; if (!this.data) { return []; } if (bins) { return bins; } const xData = this.data.map(datum => datum[this.xKey]); const xDomain = this.fixNumericExtent(array_1.extent(xData, value_1.isContinuous), 'x'); const binStarts = ticks_1.default(xDomain[0], xDomain[1], this.binCount || defaultBinCount); const binSize = ticks_1.tickStep(xDomain[0], xDomain[1], this.binCount || defaultBinCount); const firstBinEnd = binStarts[0]; const expandStartToBin = n => [n, n + binSize]; return [ [firstBinEnd - binSize, firstBinEnd], ...binStarts.map(expandStartToBin) ]; } placeDataInBins(data) { const { xKey } = this; const derivedBins = this.deriveBins(); // creating a sorted copy allows binning in O(n) rather than O(n²) // but at the expense of more temporary memory const sortedData = data.slice().sort((a, b) => { if (a[xKey] < b[xKey]) { return -1; } if (a[xKey] > b[xKey]) { return 1; } return 0; }); let currentBin = 0; const bins = [new HistogramBin(derivedBins[0])]; loop: for (let i = 0, ln = sortedData.length; i < ln; i++) { const datum = sortedData[i]; while (datum[xKey] > derivedBins[currentBin][1]) { currentBin++; const bin = derivedBins[currentBin]; if (!bin) { break loop; } bins.push(new HistogramBin(bin)); } bins[currentBin].addDatum(datum); } bins.forEach(b => b.calculateAggregatedValue(this._aggregation, this.yKey)); return bins; } get xMax() { return this.data && this.data.reduce((acc, datum) => { return Math.max(acc, datum[this.xKey]); }, Number.NEGATIVE_INFINITY); } processData() { const { xKey, data } = this; this.binnedData = this.placeDataInBins(xKey && data ? data : []); const yData = this.binnedData.map(b => b.getY(this.areaPlot)); const yMinMax = array_1.extent(yData, value_1.isContinuous); this.yDomain = this.fixNumericExtent([0, yMinMax ? yMinMax[1] : 1], 'y'); const firstBin = this.binnedData[0]; const lastBin = this.binnedData[this.binnedData.length - 1]; const xMin = firstBin.domain[0]; const xMax = lastBin.domain[1]; this.xDomain = [xMin, xMax]; this.fireEvent({ type: 'dataProcessed' }); return true; } getDomain(direction) { if (direction === chartAxis_1.ChartAxisDirection.X) { return this.xDomain; } else { return this.yDomain; } } fireNodeClickEvent(event, datum) { this.fireEvent({ type: 'nodeClick', event, series: this, datum: datum.datum, xKey: this.xKey }); } update() { this.updatePending = false; this.updateSelections(); this.updateNodes(); } updateSelections() { if (!this.nodeDataPending) { return; } this.nodeDataPending = false; const nodeData = this.createNodeData(); this.updateRectSelection(nodeData); this.updateTextSelection(nodeData); } updateNodes() { this.group.visible = this.visible; this.updateRectNodes(); this.updateTextNodes(); } createNodeData() { const { xAxis, yAxis } = this; if (!this.seriesItemEnabled || !xAxis || !yAxis) { return []; } const { scale: xScale } = xAxis; const { scale: yScale } = yAxis; const { fill, stroke, strokeWidth } = this; const nodeData = []; const defaultLabelFormatter = (params) => String(params.value); const { label: { formatter: labelFormatter = defaultLabelFormatter, fontStyle: labelFontStyle, fontWeight: labelFontWeight, fontSize: labelFontSize, fontFamily: labelFontFamily, color: labelColor } } = this; this.binnedData.forEach(binOfData => { const { aggregatedValue: total, frequency, domain: [xDomainMin, xDomainMax], relativeHeight } = binOfData; const xMinPx = xScale.convert(xDomainMin), xMaxPx = xScale.convert(xDomainMax), // note: assuming can't be negative: y = this.areaPlot ? relativeHeight : (this.yKey ? total : frequency), yZeroPx = yScale.convert(0), yMaxPx = yScale.convert(y), w = xMaxPx - xMinPx, h = Math.abs(yMaxPx - yZeroPx); const selectionDatumLabel = y !== 0 ? { text: labelFormatter({ value: binOfData.aggregatedValue }), fontStyle: labelFontStyle, fontWeight: labelFontWeight, fontSize: labelFontSize, fontFamily: labelFontFamily, fill: labelColor, x: xMinPx + w / 2, y: yMaxPx + h / 2 } : undefined; nodeData.push({ series: this, datum: binOfData, // since each selection is an aggregation of multiple data. x: xMinPx, y: yMaxPx, width: w, height: h, fill: fill, stroke: stroke, strokeWidth: strokeWidth, label: selectionDatumLabel, }); }); return nodeData; } updateRectSelection(nodeData) { const updateRects = this.rectSelection.setData(nodeData); updateRects.exit.remove(); const enterRects = updateRects.enter.append(rect_1.Rect).each(rect => { rect.tag = HistogramSeriesNodeTag.Bin; rect.crisp = true; }); this.rectSelection = updateRects.merge(enterRects); } updateRectNodes() { if (!this.chart) { return; } const { fillOpacity, strokeOpacity, shadow, 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) => { const isDatumHighlighted = datum === highlightedDatum; const strokeWidth = isDatumHighlighted && highlightedDatumStrokeWidth !== undefined ? highlightedDatumStrokeWidth : this.getStrokeWidth(datum.strokeWidth, datum); rect.x = datum.x; rect.y = datum.y; rect.width = datum.width; rect.height = datum.height; rect.fill = isDatumHighlighted && highlightedFill !== undefined ? highlightedFill : datum.fill; rect.stroke = isDatumHighlighted && highlightedStroke !== undefined ? highlightedStroke : datum.stroke; rect.fillOpacity = fillOpacity; rect.strokeOpacity = strokeOpacity; rect.strokeWidth = strokeWidth; rect.lineDash = this.lineDash; rect.lineDashOffset = this.lineDashOffset; rect.fillShadow = shadow; rect.zIndex = isDatumHighlighted ? series_1.Series.highlightedZIndex : index; rect.visible = datum.height > 0; // prevent stroke from rendering for zero height columns rect.opacity = this.getOpacity(datum); }); } updateTextSelection(nodeData) { const updateTexts = this.textSelection.setData(nodeData); updateTexts.exit.remove(); const enterTexts = updateTexts.enter.append(text_1.Text).each(text => { text.tag = HistogramSeriesNodeTag.Label; text.pointerEvents = node_1.PointerEvents.None; text.textAlign = 'center'; text.textBaseline = 'middle'; }); this.textSelection = updateTexts.merge(enterTexts); } updateTextNodes() { const labelEnabled = this.label.enabled; this.textSelection.each((text, datum) => { const label = datum.label; if (label && labelEnabled) { text.text = label.text; text.x = label.x; text.y = label.y; text.fontStyle = label.fontStyle; text.fontWeight = label.fontWeight; text.fontSize = label.fontSize; text.fontFamily = label.fontFamily; text.fill = label.fill; text.visible = true; } else { text.visible = false; } }); } getTooltipHtml(nodeDatum) { const { xKey, yKey, xAxis, yAxis } = this; if (!xKey || !xAxis || !yAxis) { return ''; } const { xName, yName, fill: color, tooltip, aggregation } = this; const { renderer: tooltipRenderer } = tooltip; const bin = nodeDatum.datum; const { aggregatedValue, frequency, domain: [rangeMin, rangeMax] } = bin; const title = `${sanitize_1.sanitizeHtml(xName || xKey)}: ${xAxis.formatDatum(rangeMin)} - ${xAxis.formatDatum(rangeMax)}`; let content = yKey ? `<b>${sanitize_1.sanitizeHtml(yName || yKey)} (${aggregation})</b>: ${yAxis.formatDatum(aggregatedValue)}<br>` : ''; content += `<b>Frequency</b>: ${frequency}`; const defaults = { title, backgroundColor: color, content }; if (tooltipRenderer) { return chart_1.toTooltipHtml(tooltipRenderer({ datum: bin, xKey, xValue: bin.domain, xName, yKey, yValue: bin.aggregatedValue, yName, color }), defaults); } return chart_1.toTooltipHtml(defaults); } listSeriesItems(legendData) { const { id, data, yKey, yName, seriesItemEnabled, fill, stroke, fillOpacity, strokeOpacity } = this; if (data && data.length) { legendData.push({ id, itemId: yKey, enabled: seriesItemEnabled, label: { text: yName || yKey || 'Frequency' }, marker: { fill: fill || 'rgba(0, 0, 0, 0)', stroke: stroke || 'rgba(0, 0, 0, 0)', fillOpacity: fillOpacity, strokeOpacity: strokeOpacity } }); } } toggleSeriesItem(itemId, enabled) { if (itemId === this.yKey) { this.seriesItemEnabled = enabled; } this.scheduleData(); } } HistogramSeries.className = 'HistogramSeries'; HistogramSeries.type = 'histogram'; __decorate([ observable_1.reactive('dataChange') ], HistogramSeries.prototype, "fill", void 0); __decorate([ observable_1.reactive('dataChange') ], HistogramSeries.prototype, "stroke", void 0); __decorate([ observable_1.reactive('layoutChange') ], HistogramSeries.prototype, "fillOpacity", void 0); __decorate([ observable_1.reactive('layoutChange') ], HistogramSeries.prototype, "strokeOpacity", void 0); __decorate([ observable_1.reactive('update') ], HistogramSeries.prototype, "lineDash", void 0); __decorate([ observable_1.reactive('update') ], HistogramSeries.prototype, "lineDashOffset", void 0); exports.HistogramSeries = HistogramSeries; //# sourceMappingURL=histogramSeries.js.map