devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
483 lines (476 loc) • 17.7 kB
JavaScript
/**
* 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);