UNPKG

@deepsource/charts

Version:

<div align="center"> <img src="https://github.com/frappe/design/blob/master/logos/logo-2019/frappe-charts-logo.png" height="128"> <a href="https://frappe.github.io/charts"> <h2>Frappe Charts</h2> </a> </div>

632 lines (537 loc) 16.3 kB
import BaseChart from './BaseChart'; import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; import { getComponent } from '../objects/ChartComponents'; import { getOffset, fire } from '../utils/dom'; import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals'; import { floatTwo } from '../utils/helpers'; import { makeOverlay, updateOverlay, legendDot } from '../utils/draw'; import { getTopOffset, getLeftOffset, MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO, LINE_CHART_DOT_SIZE } from '../utils/constants'; export default class AxisChart extends BaseChart { constructor(parent, args) { super(parent, args); this.barOptions = args.barOptions || {}; this.lineOptions = args.lineOptions || {}; this.type = args.type || 'line'; this.init = 1; this.setup(); } setMeasures() { if (this.data.datasets.length <= 1) { this.config.showLegend = 0; this.measures.paddings.bottom = 30; } } configure(options) { super.configure(options); options.axisOptions = options.axisOptions || {}; options.tooltipOptions = options.tooltipOptions || {}; this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; this.config.xIsSeries = options.axisOptions.xIsSeries || 0; this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0; this.config.numberFormatter = options.axisOptions.numberFormatter; this.config.yAxisRange = options.axisOptions.yAxisRange || {}, this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; this.config.valuesOverPoints = options.valuesOverPoints; this.config.legendRowHeight = 30; } prepareData(data = this.data) { return dataPrep(data, this.type); } prepareFirstData(data = this.data) { return zeroDataPrep(data); } calc(onlyWidthChange = false) { this.calcXPositions(); if (!onlyWidthChange) { this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); } this.makeDataByIndex(); } calcXPositions() { let s = this.state; let labels = this.data.labels; s.datasetLength = labels.length; s.unitWidth = this.width / (s.datasetLength); // Default, as per bar, and mixed. Only line will be a special case s.xOffset = s.unitWidth / 2; // // For a pure Line Chart // s.unitWidth = this.width/(s.datasetLength - 1); // s.xOffset = 0; s.xAxis = { labels: labels, positions: labels.map((d, i) => floatTwo(s.xOffset + i * s.unitWidth) ) }; } calcYAxisParameters(dataValues, withMinimum = 'false') { const yPts = calcChartIntervals(dataValues, withMinimum, this.config.yAxisRange); const scaleMultiplier = this.height / getValueRange(yPts); const intervalHeight = getIntervalSize(yPts) * scaleMultiplier; const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight); this.state.yAxis = { labels: yPts, positions: yPts.map(d => zeroLine - d * scaleMultiplier), scaleMultiplier: scaleMultiplier, zeroLine: zeroLine, }; // Dependent if above changes this.calcDatasetPoints(); this.calcYExtremes(); this.calcYRegions(); } calcDatasetPoints() { let s = this.state; let scaleAll = values => values.map(val => scale(val, s.yAxis)); s.datasets = this.data.datasets.map((d, i) => { let values = d.values; const cumulativeYs = d.cumulativeYs || []; const cumulativeYPositives = d.cumulativeYPositive || []; const cumulativeYNegatives = d.cumulativeYNegative || []; return { name: d.name && d.name.replace(/<|>|&/g, (char) => char == '&' ? '&amp;' : char == '<' ? '&lt;' : '&gt;'), index: i, chartType: d.chartType, values: values, yPositions: scaleAll(values), cumulativeYs: cumulativeYs, cumulativeYPos: scaleAll(cumulativeYs), cumulativeYPositivePositions: scaleAll(cumulativeYPositives), cumulativeYNegativesPositions: scaleAll(cumulativeYNegatives), }; }); } calcYExtremes() { let s = this.state; if (this.barOptions.stacked) { s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPositivePositions; return; } s.yExtremes = new Array(s.datasetLength).fill(9999); s.datasets.map(d => { d.yPositions.map((pos, j) => { if (pos < s.yExtremes[j]) { s.yExtremes[j] = pos; } }); }); } calcYRegions() { let s = this.state; if (this.data.yMarkers) { this.state.yMarkers = this.data.yMarkers.map(d => { d.position = scale(d.value, s.yAxis); if (!d.options) d.options = {}; return d; }); } if (this.data.yRegions) { this.state.yRegions = this.data.yRegions.map(d => { d.startPos = scale(d.start, s.yAxis); d.endPos = scale(d.end, s.yAxis); if (!d.options) d.options = {}; return d; }); } } getAllYValues() { let allValueLists = []; if (this.barOptions.stacked) { let cumulative = new Array(this.state.datasetLength).fill(0); // stacked charts can have negative values too, those values should be stacked // separately below the axis in it's own plane let cumulativePositive = new Array(this.state.datasetLength).fill(0); let cumulativeNegative = new Array(this.state.datasetLength).fill(0); this.data.datasets.forEach((d, i) => { let values = this.data.datasets[i].values; d.cumulativeYs = cumulative = cumulative.map((c, i) => c + values[i]); // accumulate postives and negatives d.cumulativeYPositive = cumulativePositive = cumulativePositive.map((c, i) => { if (d.chartType === 'line') return c return values[i] > 0 ? c + values[i] : c; }) d.cumulativeYNegative = cumulativeNegative = cumulativeNegative.map((c, i) => { if (d.chartType === 'line') return c return values[i] < 0 ? c + values[i] : c; }) }); // if the chart is stacked, the all values list will have the positive and negative cumulatives to accomodate for both axes allValueLists = [ ...this.data.datasets.map(d => d.cumulativeYPositive), ...this.data.datasets.map(d => d.cumulativeYNegative), ...this.data.datasets.filter(d => d.chartType === 'line').map(d => d.values) ]; } else { allValueLists = this.data.datasets.map(d => d.values); } if (this.data.yMarkers) { allValueLists.push(this.data.yMarkers.map(d => d.value)); } if (this.data.yRegions) { this.data.yRegions.map(d => { allValueLists.push([d.end, d.start]); }); } return [].concat(...allValueLists); } setupComponents() { let componentConfigs = [ [ 'yAxis', { mode: this.config.yAxisMode, width: this.width, shortenNumbers: this.config.shortenYAxisNumbers, numberFormatter: this.config.numberFormatter, }, function () { return this.state.yAxis; }.bind(this) ], [ 'xAxis', { mode: this.config.xAxisMode, height: this.height, // pos: 'right' }, function () { let s = this.state; s.xAxis.calcLabels = getShortenedLabels(this.width, s.xAxis.labels, this.config.xIsSeries); return s.xAxis; }.bind(this) ], [ 'yRegions', { width: this.width, pos: 'right' }, function () { return this.state.yRegions; }.bind(this) ], ]; let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar'); let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line'); let barsConfigs = barDatasets.map(d => { let index = d.index; return [ 'barGraph' + '-' + d.index, { index: index, color: this.colors[index], stacked: this.barOptions.stacked, // same for all datasets valuesOverPoints: this.config.valuesOverPoints, minHeight: this.height * MIN_BAR_PERCENT_HEIGHT, }, function () { let s = this.state; let d = s.datasets[index]; let stacked = this.barOptions.stacked; let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO; let barsWidth = s.unitWidth * (1 - spaceRatio); let barWidth = barsWidth / (stacked ? 1 : barDatasets.length); let xPositions = s.xAxis.positions.map(x => x - barsWidth / 2); if (!stacked) { xPositions = xPositions.map(p => p + barWidth * index); } let labels = new Array(s.datasetLength).fill(''); if (this.config.valuesOverPoints) { if (stacked && d.index === s.datasets.length - 1) { labels = d.cumulativeYs; } else { labels = d.values; } } let offsets = new Array(s.datasetLength).fill(0); if (stacked) { offsets = d.yPositions.map((yPos, index) => { if (d.values[index] > 0) { return yPos - d.cumulativeYPositivePositions[index] } return yPos - d.cumulativeYNegativesPositions[index] }); } return { xPositions: xPositions, yPositions: d.yPositions, offsets: offsets, // values: d.values, labels: labels, zeroLine: s.yAxis.zeroLine, barsWidth: barsWidth, barWidth: barWidth, }; }.bind(this) ]; }); let lineConfigs = lineDatasets.map(d => { let index = d.index; return [ 'lineGraph' + '-' + d.index, { index: index, color: this.colors[index], svgDefs: this.svgDefs, heatline: this.lineOptions.heatline, regionFill: this.lineOptions.regionFill, spline: this.lineOptions.spline, showDots: this.lineOptions.showDots, trailingDot: this.lineOptions.trailingDot, hideDotBorder: this.lineOptions.hideDotBorder, hideLine: this.lineOptions.hideLine, // same for all datasets valuesOverPoints: this.config.valuesOverPoints, }, function () { let s = this.state; let d = s.datasets[index]; let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine ? s.yAxis.positions[0] : s.yAxis.zeroLine; return { xPositions: s.xAxis.positions, yPositions: d.yPositions, values: d.values, zeroLine: minLine, radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE, }; }.bind(this) ]; }); let markerConfigs = [ [ 'yMarkers', { width: this.width, pos: 'right' }, function () { return this.state.yMarkers; }.bind(this) ] ]; componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs); let optionals = ['yMarkers', 'yRegions']; this.dataUnitComponents = []; this.components = new Map(componentConfigs .filter(args => !optionals.includes(args[0]) || this.state[args[0]]) .map(args => { let component = getComponent(...args); if (args[0].includes('lineGraph') || args[0].includes('barGraph')) { this.dataUnitComponents.push(component); } return [args[0], component]; })); } makeDataByIndex() { this.dataByIndex = {}; let s = this.state; let formatX = this.config.formatTooltipX; let formatY = this.config.formatTooltipY; let titles = s.xAxis.labels; titles.map((label, index) => { let values = this.state.datasets.map((set, i) => { let value = set.values[index]; return { title: set.name, value: value, yPos: set.yPositions[index], color: this.colors[i], formatted: formatY ? formatY(value, { name: set.name, index: set.index, values: set.values }) : value, }; }); this.dataByIndex[index] = { label: label, formattedLabel: formatX ? formatX(label) : label, xPos: s.xAxis.positions[index], values: values, yExtreme: s.yExtremes[index], }; }); } bindTooltip() { // NOTE: could be in tooltip itself, as it is a given functionality for its parent this.container.addEventListener('mousemove', (e) => { let m = this.measures; let o = getOffset(this.container); let relX = e.pageX - o.left - getLeftOffset(m); let relY = e.pageY - o.top; if (relY < this.height + getTopOffset(m) && relY > getTopOffset(m)) { this.mapTooltipXPosition(relX); } else { this.tip.hideTip(); } }); } mapTooltipXPosition(relX) { let s = this.state; if (!s.yExtremes) return; let index = getClosestInArray(relX, s.xAxis.positions, true); if (index >= 0) { let dbi = this.dataByIndex[index]; this.tip.setValues( dbi.xPos + this.tip.offset.x, dbi.yExtreme + this.tip.offset.y, { name: dbi.formattedLabel, value: '' }, dbi.values, index ); this.tip.showTip(); } } renderLegend() { let s = this.data; if (s.datasets.length > 1) { super.renderLegend(s.datasets); } } makeLegend(data, index, x_pos, y_pos) { return legendDot( x_pos, y_pos + 5, // Extra offset 12, // size 3, // dot radius this.colors[index], // fill data.name, //label null, // value 8.75, // base_font_size this.config.truncateLegends // truncate legends ); } // Overlay makeOverlay() { if (this.init) { this.init = 0; return; } if (this.overlayGuides) { this.overlayGuides.forEach(g => { let o = g.overlay; o.parentNode.removeChild(o); }); } this.overlayGuides = this.dataUnitComponents.map(c => { return { type: c.unitType, overlay: undefined, units: c.units, }; }); if (this.state.currentIndex === undefined) { this.state.currentIndex = this.state.datasetLength - 1; } // Render overlays this.overlayGuides.map(d => { let currentUnit = d.units[this.state.currentIndex]; d.overlay = makeOverlay[d.type](currentUnit); this.drawArea.appendChild(d.overlay); }); } updateOverlayGuides() { if (this.overlayGuides) { this.overlayGuides.forEach(g => { let o = g.overlay; o.parentNode.removeChild(o); }); } } bindOverlay() { this.parent.addEventListener('data-select', () => { this.updateOverlay(); }); } bindUnits() { this.dataUnitComponents.map(c => { c.units.map(unit => { unit.addEventListener('click', () => { let index = unit.getAttribute('data-point-index'); this.setCurrentDataPoint(index); }); }); }); // Note: Doesn't work as tooltip is absolutely positioned this.tip.container.addEventListener('click', () => { let index = this.tip.container.getAttribute('data-point-index'); this.setCurrentDataPoint(index); }); } updateOverlay() { this.overlayGuides.map(d => { let currentUnit = d.units[this.state.currentIndex]; updateOverlay[d.type](currentUnit, d.overlay); }); } onLeftArrow() { this.setCurrentDataPoint(this.state.currentIndex - 1); } onRightArrow() { this.setCurrentDataPoint(this.state.currentIndex + 1); } getDataPoint(index = this.state.currentIndex) { let s = this.state; let data_point = { index: index, label: s.xAxis.labels[index], values: s.datasets.map(d => d.values[index]) }; return data_point; } setCurrentDataPoint(index) { let s = this.state; index = parseInt(index); if (index < 0) index = 0; if (index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1; if (index === s.currentIndex) return; s.currentIndex = index; fire(this.parent, "data-select", this.getDataPoint()); } // API addDataPoint(label, datasetValues, index = this.state.datasetLength) { super.addDataPoint(label, datasetValues, index); this.data.labels.splice(index, 0, label); this.data.datasets.map((d, i) => { d.values.splice(index, 0, datasetValues[i]); }); this.update(this.data); } removeDataPoint(index = this.state.datasetLength - 1) { if (this.data.labels.length <= 1) { return; } super.removeDataPoint(index); this.data.labels.splice(index, 1); this.data.datasets.map(d => { d.values.splice(index, 1); }); this.update(this.data); } updateDataset(datasetValues, index = 0) { this.data.datasets[index].values = datasetValues; this.update(this.data); } // addDataset(dataset, index) {} // removeDataset(index = 0) {} updateDatasets(datasets) { this.data.datasets.map((d, i) => { if (datasets[i]) { d.values = datasets[i]; } }); this.update(this.data); } // updateDataPoint(dataPoint, index = 0) {} // addDataPoint(dataPoint, index = 0) {} // removeDataPoint(index = 0) {} }