UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

483 lines (476 loc) • 17.7 kB
/** * DevExtreme (esm/viz/sparklines/sparkline.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import BaseSparkline from "./base_sparkline"; import { validateData } from "../components/data_validator"; import { Series } from "../series/base_series"; const MIN_BAR_WIDTH = 1; const MAX_BAR_WIDTH = 50; const DEFAULT_BAR_INTERVAL = 4; const DEFAULT_CANVAS_WIDTH = 250; const DEFAULT_CANVAS_HEIGHT = 30; const DEFAULT_POINT_BORDER = 2; const ALLOWED_TYPES = { line: true, spline: true, stepline: true, area: true, steparea: true, splinearea: true, bar: true, winloss: true }; const _math = Math; const _abs = _math.abs; const _round = _math.round; const _max = _math.max; const _min = _math.min; const _isFinite = isFinite; import { map as _map, normalizeEnum as _normalizeEnum } from "../core/utils"; import { isDefined as _isDefined } from "../../core/utils/type"; const _Number = Number; const _String = String; function findMinMax(data, valField) { const firstItem = data[0] || {}; const firstValue = firstItem[valField] || 0; let min = firstValue; let max = firstValue; let minIndexes = [0]; let maxIndexes = [0]; const dataLength = data.length; let value; let i; for (i = 1; i < dataLength; i++) { value = data[i][valField]; if (value < min) { min = value; minIndexes = [i] } else if (value === min) { minIndexes.push(i) } if (value > max) { max = value; maxIndexes = [i] } else if (value === max) { maxIndexes.push(i) } } if (max === min) { minIndexes = maxIndexes = [] } return { minIndexes: minIndexes, maxIndexes: maxIndexes } } function parseNumericDataSource(data, argField, valField, ignoreEmptyPoints) { return _map(data, (function(dataItem, index) { let item = null; let isDataNumber; let value; if (void 0 !== dataItem) { item = {}; isDataNumber = _isFinite(dataItem); item[argField] = isDataNumber ? _String(index) : dataItem[argField]; value = isDataNumber ? dataItem : dataItem[valField]; item[valField] = null === value ? ignoreEmptyPoints ? void 0 : value : _Number(value); item = void 0 !== item[argField] && void 0 !== item[valField] ? item : null } return item })) } function parseWinlossDataSource(data, argField, valField, target) { return _map(data, (function(dataItem) { const item = {}; item[argField] = dataItem[argField]; if (_abs(dataItem[valField] - target) < 1e-4) { item[valField] = 0 } else if (dataItem[valField] > target) { item[valField] = 1 } else { item[valField] = -1 } return item })) } function selectPointColor(color, options, index, pointIndexes) { if (index === pointIndexes.first || index === pointIndexes.last) { color = options.firstLastColor } if ((pointIndexes.min || []).indexOf(index) >= 0) { color = options.minColor } if ((pointIndexes.max || []).indexOf(index) >= 0) { color = options.maxColor } return color } function createLineCustomizeFunction(pointIndexes, options) { return function() { const color = selectPointColor(void 0, options, this.index, pointIndexes); return color ? { visible: true, border: { color: color } } : {} } } function createBarCustomizeFunction(pointIndexes, options, winlossData) { return function() { const index = this.index; const isWinloss = "winloss" === options.type; const target = isWinloss ? options.winlossThreshold : 0; const value = isWinloss ? winlossData[index][options.valueField] : this.value; const positiveColor = isWinloss ? options.winColor : options.barPositiveColor; const negativeColor = isWinloss ? options.lossColor : options.barNegativeColor; return { color: selectPointColor(value >= target ? positiveColor : negativeColor, options, index, pointIndexes) } } } const dxSparkline = BaseSparkline.inherit({ _rootClassPrefix: "dxsl", _rootClass: "dxsl-sparkline", _themeSection: "sparkline", _defaultSize: { width: 250, height: 30 }, _initCore: function() { this.callBase(); this._createSeries() }, _initialChanges: ["DATA_SOURCE"], _dataSourceChangedHandler: function() { this._requestChange(["UPDATE"]) }, _updateWidgetElements: function() { this._updateSeries(); this.callBase() }, _disposeWidgetElements: function() { this._series && this._series.dispose(); this._series = this._seriesGroup = this._seriesLabelGroup = null }, _cleanWidgetElements: function() { this._seriesGroup.remove(); this._seriesLabelGroup.remove(); this._seriesGroup.clear(); this._seriesLabelGroup.clear(); this._series.removeGraphicElements(); this._series.removePointElements(); this._series.removeBordersGroup() }, _drawWidgetElements: function() { if (this._dataIsLoaded()) { this._drawSeries(); this._drawn() } }, _getCorrectCanvas: function() { const options = this._allOptions; const canvas = this._canvas; const halfPointSize = options.pointSize && Math.ceil(options.pointSize / 2) + 2; const type = options.type; if ("bar" !== type && "winloss" !== type && (options.showFirstLast || options.showMinMax)) { return { width: canvas.width, height: canvas.height, left: canvas.left + halfPointSize, right: canvas.right + halfPointSize, top: canvas.top + halfPointSize, bottom: canvas.bottom + halfPointSize } } return canvas }, _prepareOptions: function() { const that = this; that._allOptions = that.callBase(); that._allOptions.type = _normalizeEnum(that._allOptions.type); if (!ALLOWED_TYPES[that._allOptions.type]) { that._allOptions.type = "line" } }, _createHtmlElements: function() { this._seriesGroup = this._renderer.g().attr({ class: "dxsl-series" }); this._seriesLabelGroup = this._renderer.g().attr({ class: "dxsl-series-labels" }) }, _createSeries: function() { this._series = new Series({ renderer: this._renderer, seriesGroup: this._seriesGroup, labelsGroup: this._seriesLabelGroup, argumentAxis: this._argumentAxis, valueAxis: this._valueAxis, incidentOccurred: this._incidentOccurred }, { widgetType: "chart", type: "line" }) }, _updateSeries: function() { const singleSeries = this._series; this._prepareDataSource(); const seriesOptions = this._prepareSeriesOptions(); singleSeries.updateOptions(seriesOptions); const groupsData = { groups: [{ series: [singleSeries] }] }; groupsData.argumentOptions = { type: "bar" === seriesOptions.type ? "discrete" : void 0 }; this._simpleDataSource = validateData(this._simpleDataSource, groupsData, this._incidentOccurred, { checkTypeForAllData: false, convertToAxisDataType: true, sortingMethod: true })[singleSeries.getArgumentField()]; seriesOptions.customizePoint = this._getCustomizeFunction(); singleSeries.updateData(this._simpleDataSource); singleSeries.createPoints(); this._groupsDataCategories = groupsData.categories }, _optionChangesMap: { dataSource: "DATA_SOURCE" }, _optionChangesOrder: ["DATA_SOURCE"], _change_DATA_SOURCE: function() { this._updateDataSource() }, _prepareDataSource: function() { const that = this; const options = that._allOptions; const argField = options.argumentField; const valField = options.valueField; const dataSource = that._dataSourceItems() || []; const data = parseNumericDataSource(dataSource, argField, valField, that.option("ignoreEmptyPoints")); if ("winloss" === options.type) { that._winlossDataSource = data; that._simpleDataSource = parseWinlossDataSource(data, argField, valField, options.winlossThreshold) } else { that._simpleDataSource = data } }, _prepareSeriesOptions: function() { const options = this._allOptions; const type = "winloss" === options.type ? "bar" : options.type; return { visible: true, argumentField: options.argumentField, valueField: options.valueField, color: options.lineColor, width: options.lineWidth, widgetType: "chart", name: "", type: type, opacity: -1 !== type.indexOf("area") ? this._allOptions.areaOpacity : void 0, point: { size: options.pointSize, symbol: options.pointSymbol, border: { visible: true, width: 2 }, color: options.pointColor, visible: false, hoverStyle: { border: {} }, selectionStyle: { border: {} } }, border: { color: options.lineColor, width: options.lineWidth, visible: "bar" !== type } } }, _getCustomizeFunction: function() { const that = this; const options = that._allOptions; const dataSource = that._winlossDataSource || that._simpleDataSource; const drawnPointIndexes = that._getExtremumPointsIndexes(dataSource); let customizeFunction; if ("winloss" === options.type || "bar" === options.type) { customizeFunction = createBarCustomizeFunction(drawnPointIndexes, options, that._winlossDataSource) } else { customizeFunction = createLineCustomizeFunction(drawnPointIndexes, options) } return customizeFunction }, _getExtremumPointsIndexes: function(data) { const that = this; const options = that._allOptions; const lastIndex = data.length - 1; const indexes = {}; that._minMaxIndexes = findMinMax(data, options.valueField); if (options.showFirstLast) { indexes.first = 0; indexes.last = lastIndex } if (options.showMinMax) { indexes.min = that._minMaxIndexes.minIndexes; indexes.max = that._minMaxIndexes.maxIndexes } return indexes }, _getStick: function() { return { stick: "bar" !== this._series.type } }, _updateRange: function() { const series = this._series; const type = series.type; const isBarType = "bar" === type; const isWinlossType = "winloss" === type; const rangeData = series.getRangeData(); const minValue = this._allOptions.minValue; const hasMinY = _isDefined(minValue) && _isFinite(minValue); const maxValue = this._allOptions.maxValue; const hasMaxY = _isDefined(maxValue) && _isFinite(maxValue); let argCoef; const valCoef = .15 * (rangeData.val.max - rangeData.val.min); if (isBarType || isWinlossType || "area" === type) { if (0 !== rangeData.val.min) { rangeData.val.min -= valCoef } if (0 !== rangeData.val.max) { rangeData.val.max += valCoef } } else { rangeData.val.min -= valCoef; rangeData.val.max += valCoef } if (hasMinY || hasMaxY) { if (hasMinY && hasMaxY) { rangeData.val.minVisible = _min(minValue, maxValue); rangeData.val.maxVisible = _max(minValue, maxValue) } else { rangeData.val.minVisible = hasMinY ? _Number(minValue) : void 0; rangeData.val.maxVisible = hasMaxY ? _Number(maxValue) : void 0 } if (isWinlossType) { rangeData.val.minVisible = hasMinY ? _max(rangeData.val.minVisible, -1) : void 0; rangeData.val.maxVisible = hasMaxY ? _min(rangeData.val.maxVisible, 1) : void 0 } } if (series.getPoints().length > 1) { if (isBarType) { argCoef = .1 * (rangeData.arg.max - rangeData.arg.min); rangeData.arg.min = rangeData.arg.min - argCoef; rangeData.arg.max = rangeData.arg.max + argCoef } } rangeData.arg.categories = this._groupsDataCategories; this._ranges = rangeData }, _getBarWidth: function(pointsCount) { const canvas = this._canvas; const intervalWidth = 4 * pointsCount; const rangeWidth = canvas.width - canvas.left - canvas.right - intervalWidth; let width = _round(rangeWidth / pointsCount); if (width < 1) { width = 1 } if (width > 50) { width = 50 } return width }, _correctPoints: function() { const that = this; const seriesType = that._allOptions.type; const seriesPoints = that._series.getPoints(); const pointsLength = seriesPoints.length; let barWidth; let i; if ("bar" === seriesType || "winloss" === seriesType) { barWidth = that._getBarWidth(pointsLength); for (i = 0; i < pointsLength; i++) { seriesPoints[i].correctCoordinates({ width: barWidth, offset: 0 }) } } }, _drawSeries: function() { const that = this; if (that._simpleDataSource.length > 0) { that._correctPoints(); that._series.draw(); that._seriesGroup.append(that._renderer.root) } }, _isTooltipEnabled: function() { return !!this._simpleDataSource.length }, _getTooltipData: function() { const options = this._allOptions; const dataSource = this._winlossDataSource || this._simpleDataSource; const tooltip = this._tooltip; if (0 === dataSource.length) { return {} } const minMax = this._minMaxIndexes; const valueField = options.valueField; const first = dataSource[0][valueField]; const last = dataSource[dataSource.length - 1][valueField]; const min = _isDefined(minMax.minIndexes[0]) ? dataSource[minMax.minIndexes[0]][valueField] : first; const max = _isDefined(minMax.maxIndexes[0]) ? dataSource[minMax.maxIndexes[0]][valueField] : first; const formattedFirst = tooltip.formatValue(first); const formattedLast = tooltip.formatValue(last); const formattedMin = tooltip.formatValue(min); const formattedMax = tooltip.formatValue(max); const customizeObject = { firstValue: formattedFirst, lastValue: formattedLast, minValue: formattedMin, maxValue: formattedMax, originalFirstValue: first, originalLastValue: last, originalMinValue: min, originalMaxValue: max, valueText: ["Start:", formattedFirst, "End:", formattedLast, "Min:", formattedMin, "Max:", formattedMax] }; if ("winloss" === options.type) { customizeObject.originalThresholdValue = options.winlossThreshold; customizeObject.thresholdValue = tooltip.formatValue(options.winlossThreshold) } return customizeObject } }); _map(["lossColor", "lineColor", "lineWidth", "areaOpacity", "minColor", "maxColor", "barPositiveColor", "barNegativeColor", "winColor", "lessColor", "firstLastColor", "pointSymbol", "pointColor", "pointSize", "type", "argumentField", "valueField", "winlossThreshold", "showFirstLast", "showMinMax", "ignoreEmptyPoints", "minValue", "maxValue"], (function(name) { dxSparkline.prototype._optionChangesMap[name] = "OPTIONS" })); import componentRegistrator from "../../core/component_registrator"; componentRegistrator("dxSparkline", dxSparkline); export default dxSparkline; import { plugin } from "../core/data_source"; dxSparkline.addPlugin(plugin);