sparkline.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /**
  2. * DevExtreme (viz/sparklines/sparkline.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var BaseSparkline = require("./base_sparkline");
  11. var dataValidatorModule = require("../components/data_validator");
  12. var seriesModule = require("../series/base_series");
  13. var MIN_BAR_WIDTH = 1;
  14. var MAX_BAR_WIDTH = 50;
  15. var DEFAULT_BAR_INTERVAL = 4;
  16. var DEFAULT_CANVAS_WIDTH = 250;
  17. var DEFAULT_CANVAS_HEIGHT = 30;
  18. var DEFAULT_POINT_BORDER = 2;
  19. var ALLOWED_TYPES = {
  20. line: true,
  21. spline: true,
  22. stepline: true,
  23. area: true,
  24. steparea: true,
  25. splinearea: true,
  26. bar: true,
  27. winloss: true
  28. };
  29. var _math = Math;
  30. var _abs = _math.abs;
  31. var _round = _math.round;
  32. var _max = _math.max;
  33. var _min = _math.min;
  34. var _isFinite = isFinite;
  35. var vizUtils = require("../core/utils");
  36. var _map = vizUtils.map;
  37. var _normalizeEnum = vizUtils.normalizeEnum;
  38. var _isDefined = require("../../core/utils/type").isDefined;
  39. var _Number = Number;
  40. var _String = String;
  41. function findMinMax(data, valField) {
  42. var firstItem = data[0] || {};
  43. var firstValue = firstItem[valField] || 0;
  44. var min = firstValue;
  45. var max = firstValue;
  46. var minIndexes = [0];
  47. var maxIndexes = [0];
  48. var dataLength = data.length;
  49. var value;
  50. var i;
  51. for (i = 1; i < dataLength; i++) {
  52. value = data[i][valField];
  53. if (value < min) {
  54. min = value;
  55. minIndexes = [i]
  56. } else {
  57. if (value === min) {
  58. minIndexes.push(i)
  59. }
  60. }
  61. if (value > max) {
  62. max = value;
  63. maxIndexes = [i]
  64. } else {
  65. if (value === max) {
  66. maxIndexes.push(i)
  67. }
  68. }
  69. }
  70. if (max === min) {
  71. minIndexes = maxIndexes = []
  72. }
  73. return {
  74. minIndexes: minIndexes,
  75. maxIndexes: maxIndexes
  76. }
  77. }
  78. function parseNumericDataSource(data, argField, valField, ignoreEmptyPoints) {
  79. return _map(data, function(dataItem, index) {
  80. var item = null;
  81. var isDataNumber;
  82. var value;
  83. if (void 0 !== dataItem) {
  84. item = {};
  85. isDataNumber = _isFinite(dataItem);
  86. item[argField] = isDataNumber ? _String(index) : dataItem[argField];
  87. value = isDataNumber ? dataItem : dataItem[valField];
  88. item[valField] = null === value ? ignoreEmptyPoints ? void 0 : value : _Number(value);
  89. item = void 0 !== item[argField] && void 0 !== item[valField] ? item : null
  90. }
  91. return item
  92. })
  93. }
  94. function parseWinlossDataSource(data, argField, valField, target) {
  95. var lowBarValue = -1;
  96. var zeroBarValue = 0;
  97. var highBarValue = 1;
  98. var delta = 1e-4;
  99. return _map(data, function(dataItem) {
  100. var item = {};
  101. item[argField] = dataItem[argField];
  102. if (_abs(dataItem[valField] - target) < delta) {
  103. item[valField] = zeroBarValue
  104. } else {
  105. if (dataItem[valField] > target) {
  106. item[valField] = highBarValue
  107. } else {
  108. item[valField] = lowBarValue
  109. }
  110. }
  111. return item
  112. })
  113. }
  114. function selectPointColor(color, options, index, pointIndexes) {
  115. if (index === pointIndexes.first || index === pointIndexes.last) {
  116. color = options.firstLastColor
  117. }
  118. if ((pointIndexes.min || []).indexOf(index) >= 0) {
  119. color = options.minColor
  120. }
  121. if ((pointIndexes.max || []).indexOf(index) >= 0) {
  122. color = options.maxColor
  123. }
  124. return color
  125. }
  126. function createLineCustomizeFunction(pointIndexes, options) {
  127. return function() {
  128. var color = selectPointColor(void 0, options, this.index, pointIndexes);
  129. return color ? {
  130. visible: true,
  131. border: {
  132. color: color
  133. }
  134. } : {}
  135. }
  136. }
  137. function createBarCustomizeFunction(pointIndexes, options, winlossData) {
  138. return function() {
  139. var index = this.index;
  140. var isWinloss = "winloss" === options.type;
  141. var target = isWinloss ? options.winlossThreshold : 0;
  142. var value = isWinloss ? winlossData[index][options.valueField] : this.value;
  143. var positiveColor = isWinloss ? options.winColor : options.barPositiveColor;
  144. var negativeColor = isWinloss ? options.lossColor : options.barNegativeColor;
  145. return {
  146. color: selectPointColor(value >= target ? positiveColor : negativeColor, options, index, pointIndexes)
  147. }
  148. }
  149. }
  150. var dxSparkline = BaseSparkline.inherit({
  151. _rootClassPrefix: "dxsl",
  152. _rootClass: "dxsl-sparkline",
  153. _themeSection: "sparkline",
  154. _defaultSize: {
  155. width: DEFAULT_CANVAS_WIDTH,
  156. height: DEFAULT_CANVAS_HEIGHT
  157. },
  158. _initCore: function() {
  159. this.callBase();
  160. this._createSeries()
  161. },
  162. _initialChanges: ["DATA_SOURCE"],
  163. _dataSourceChangedHandler: function() {
  164. this._requestChange(["UPDATE"])
  165. },
  166. _updateWidgetElements: function() {
  167. this._updateSeries();
  168. this.callBase()
  169. },
  170. _disposeWidgetElements: function() {
  171. var that = this;
  172. that._series && that._series.dispose();
  173. that._series = that._seriesGroup = that._seriesLabelGroup = null
  174. },
  175. _cleanWidgetElements: function() {
  176. this._seriesGroup.remove();
  177. this._seriesLabelGroup.remove();
  178. this._seriesGroup.clear();
  179. this._seriesLabelGroup.clear()
  180. },
  181. _drawWidgetElements: function() {
  182. if (this._dataIsLoaded()) {
  183. this._drawSeries();
  184. this._drawn()
  185. }
  186. },
  187. _getCorrectCanvas: function() {
  188. var options = this._allOptions;
  189. var canvas = this._canvas;
  190. var halfPointSize = options.pointSize && Math.ceil(options.pointSize / 2) + DEFAULT_POINT_BORDER;
  191. var type = options.type;
  192. if ("bar" !== type && "winloss" !== type && (options.showFirstLast || options.showMinMax)) {
  193. return {
  194. width: canvas.width,
  195. height: canvas.height,
  196. left: canvas.left + halfPointSize,
  197. right: canvas.right + halfPointSize,
  198. top: canvas.top + halfPointSize,
  199. bottom: canvas.bottom + halfPointSize
  200. }
  201. }
  202. return canvas
  203. },
  204. _prepareOptions: function() {
  205. var that = this;
  206. that._allOptions = that.callBase();
  207. that._allOptions.type = _normalizeEnum(that._allOptions.type);
  208. if (!ALLOWED_TYPES[that._allOptions.type]) {
  209. that._allOptions.type = "line"
  210. }
  211. },
  212. _createHtmlElements: function() {
  213. this._seriesGroup = this._renderer.g().attr({
  214. "class": "dxsl-series"
  215. });
  216. this._seriesLabelGroup = this._renderer.g().attr({
  217. "class": "dxsl-series-labels"
  218. })
  219. },
  220. _createSeries: function() {
  221. this._series = new seriesModule.Series({
  222. renderer: this._renderer,
  223. seriesGroup: this._seriesGroup,
  224. labelsGroup: this._seriesLabelGroup,
  225. argumentAxis: this._argumentAxis,
  226. valueAxis: this._valueAxis
  227. }, {
  228. widgetType: "chart",
  229. type: "line"
  230. })
  231. },
  232. _updateSeries: function() {
  233. var that = this;
  234. var singleSeries = that._series;
  235. that._prepareDataSource();
  236. var seriesOptions = that._prepareSeriesOptions();
  237. singleSeries.updateOptions(seriesOptions);
  238. var groupsData = {
  239. groups: [{
  240. series: [singleSeries]
  241. }]
  242. };
  243. groupsData.argumentOptions = {
  244. type: "bar" === seriesOptions.type ? "discrete" : void 0
  245. };
  246. that._simpleDataSource = dataValidatorModule.validateData(that._simpleDataSource, groupsData, that._incidentOccurred, {
  247. checkTypeForAllData: false,
  248. convertToAxisDataType: true,
  249. sortingMethod: true
  250. })[singleSeries.getArgumentField()];
  251. seriesOptions.customizePoint = that._getCustomizeFunction();
  252. singleSeries.updateData(that._simpleDataSource);
  253. singleSeries.createPoints();
  254. that._groupsDataCategories = groupsData.categories
  255. },
  256. _optionChangesMap: {
  257. dataSource: "DATA_SOURCE"
  258. },
  259. _optionChangesOrder: ["DATA_SOURCE"],
  260. _change_DATA_SOURCE: function() {
  261. this._updateDataSource()
  262. },
  263. _prepareDataSource: function() {
  264. var that = this;
  265. var options = that._allOptions;
  266. var argField = options.argumentField;
  267. var valField = options.valueField;
  268. var dataSource = that._dataSourceItems() || [];
  269. var data = parseNumericDataSource(dataSource, argField, valField, that.option("ignoreEmptyPoints"));
  270. if ("winloss" === options.type) {
  271. that._winlossDataSource = data;
  272. that._simpleDataSource = parseWinlossDataSource(data, argField, valField, options.winlossThreshold)
  273. } else {
  274. that._simpleDataSource = data
  275. }
  276. },
  277. _prepareSeriesOptions: function() {
  278. var that = this;
  279. var options = that._allOptions;
  280. var type = "winloss" === options.type ? "bar" : options.type;
  281. return {
  282. visible: true,
  283. argumentField: options.argumentField,
  284. valueField: options.valueField,
  285. color: options.lineColor,
  286. width: options.lineWidth,
  287. widgetType: "chart",
  288. type: type,
  289. opacity: type.indexOf("area") !== -1 ? that._allOptions.areaOpacity : void 0,
  290. point: {
  291. size: options.pointSize,
  292. symbol: options.pointSymbol,
  293. border: {
  294. visible: true,
  295. width: DEFAULT_POINT_BORDER
  296. },
  297. color: options.pointColor,
  298. visible: false,
  299. hoverStyle: {
  300. border: {}
  301. },
  302. selectionStyle: {
  303. border: {}
  304. }
  305. },
  306. border: {
  307. color: options.lineColor,
  308. width: options.lineWidth,
  309. visible: "bar" !== type
  310. }
  311. }
  312. },
  313. _getCustomizeFunction: function() {
  314. var that = this;
  315. var options = that._allOptions;
  316. var dataSource = that._winlossDataSource || that._simpleDataSource;
  317. var drawnPointIndexes = that._getExtremumPointsIndexes(dataSource);
  318. var customizeFunction;
  319. if ("winloss" === options.type || "bar" === options.type) {
  320. customizeFunction = createBarCustomizeFunction(drawnPointIndexes, options, that._winlossDataSource)
  321. } else {
  322. customizeFunction = createLineCustomizeFunction(drawnPointIndexes, options)
  323. }
  324. return customizeFunction
  325. },
  326. _getExtremumPointsIndexes: function(data) {
  327. var that = this;
  328. var options = that._allOptions;
  329. var lastIndex = data.length - 1;
  330. var indexes = {};
  331. that._minMaxIndexes = findMinMax(data, options.valueField);
  332. if (options.showFirstLast) {
  333. indexes.first = 0;
  334. indexes.last = lastIndex
  335. }
  336. if (options.showMinMax) {
  337. indexes.min = that._minMaxIndexes.minIndexes;
  338. indexes.max = that._minMaxIndexes.maxIndexes
  339. }
  340. return indexes
  341. },
  342. _getStick: function() {
  343. return {
  344. stick: "bar" !== this._series.type
  345. }
  346. },
  347. _updateRange: function() {
  348. var that = this;
  349. var series = that._series;
  350. var type = series.type;
  351. var isBarType = "bar" === type;
  352. var isWinlossType = "winloss" === type;
  353. var DEFAULT_VALUE_RANGE_MARGIN = .15;
  354. var DEFAULT_ARGUMENT_RANGE_MARGIN = .1;
  355. var WINLOSS_MAX_RANGE = 1;
  356. var WINLOSS_MIN_RANGE = -1;
  357. var rangeData = series.getRangeData();
  358. var minValue = that._allOptions.minValue;
  359. var hasMinY = _isDefined(minValue) && _isFinite(minValue);
  360. var maxValue = that._allOptions.maxValue;
  361. var hasMaxY = _isDefined(maxValue) && _isFinite(maxValue);
  362. var argCoef;
  363. var valCoef = (rangeData.val.max - rangeData.val.min) * DEFAULT_VALUE_RANGE_MARGIN;
  364. if (isBarType || isWinlossType || "area" === type) {
  365. if (0 !== rangeData.val.min) {
  366. rangeData.val.min -= valCoef
  367. }
  368. if (0 !== rangeData.val.max) {
  369. rangeData.val.max += valCoef
  370. }
  371. } else {
  372. rangeData.val.min -= valCoef;
  373. rangeData.val.max += valCoef
  374. }
  375. if (hasMinY || hasMaxY) {
  376. if (hasMinY && hasMaxY) {
  377. rangeData.val.minVisible = _min(minValue, maxValue);
  378. rangeData.val.maxVisible = _max(minValue, maxValue)
  379. } else {
  380. rangeData.val.minVisible = hasMinY ? _Number(minValue) : void 0;
  381. rangeData.val.maxVisible = hasMaxY ? _Number(maxValue) : void 0
  382. }
  383. if (isWinlossType) {
  384. rangeData.val.minVisible = hasMinY ? _max(rangeData.val.minVisible, WINLOSS_MIN_RANGE) : void 0;
  385. rangeData.val.maxVisible = hasMaxY ? _min(rangeData.val.maxVisible, WINLOSS_MAX_RANGE) : void 0
  386. }
  387. }
  388. if (series.getPoints().length > 1) {
  389. if (isBarType) {
  390. argCoef = (rangeData.arg.max - rangeData.arg.min) * DEFAULT_ARGUMENT_RANGE_MARGIN;
  391. rangeData.arg.min = rangeData.arg.min - argCoef;
  392. rangeData.arg.max = rangeData.arg.max + argCoef
  393. }
  394. }
  395. rangeData.arg.categories = that._groupsDataCategories;
  396. that._ranges = rangeData
  397. },
  398. _getBarWidth: function(pointsCount) {
  399. var that = this;
  400. var canvas = that._canvas;
  401. var intervalWidth = pointsCount * DEFAULT_BAR_INTERVAL;
  402. var rangeWidth = canvas.width - canvas.left - canvas.right - intervalWidth;
  403. var width = _round(rangeWidth / pointsCount);
  404. if (width < MIN_BAR_WIDTH) {
  405. width = MIN_BAR_WIDTH
  406. }
  407. if (width > MAX_BAR_WIDTH) {
  408. width = MAX_BAR_WIDTH
  409. }
  410. return width
  411. },
  412. _correctPoints: function() {
  413. var that = this;
  414. var seriesType = that._allOptions.type;
  415. var seriesPoints = that._series.getPoints();
  416. var pointsLength = seriesPoints.length;
  417. var barWidth;
  418. var i;
  419. if ("bar" === seriesType || "winloss" === seriesType) {
  420. barWidth = that._getBarWidth(pointsLength);
  421. for (i = 0; i < pointsLength; i++) {
  422. seriesPoints[i].correctCoordinates({
  423. width: barWidth,
  424. offset: 0
  425. })
  426. }
  427. }
  428. },
  429. _drawSeries: function() {
  430. var that = this;
  431. if (that._simpleDataSource.length > 0) {
  432. that._correctPoints();
  433. that._series.draw();
  434. that._seriesGroup.append(that._renderer.root)
  435. }
  436. },
  437. _isTooltipEnabled: function() {
  438. return !!this._simpleDataSource.length
  439. },
  440. _getTooltipData: function() {
  441. var that = this;
  442. var options = that._allOptions;
  443. var dataSource = that._winlossDataSource || that._simpleDataSource;
  444. var tooltip = that._tooltip;
  445. if (0 === dataSource.length) {
  446. return {}
  447. }
  448. var minMax = that._minMaxIndexes;
  449. var valueField = options.valueField;
  450. var first = dataSource[0][valueField];
  451. var last = dataSource[dataSource.length - 1][valueField];
  452. var min = _isDefined(minMax.minIndexes[0]) ? dataSource[minMax.minIndexes[0]][valueField] : first;
  453. var max = _isDefined(minMax.maxIndexes[0]) ? dataSource[minMax.maxIndexes[0]][valueField] : first;
  454. var formattedFirst = tooltip.formatValue(first);
  455. var formattedLast = tooltip.formatValue(last);
  456. var formattedMin = tooltip.formatValue(min);
  457. var formattedMax = tooltip.formatValue(max);
  458. var customizeObject = {
  459. firstValue: formattedFirst,
  460. lastValue: formattedLast,
  461. minValue: formattedMin,
  462. maxValue: formattedMax,
  463. originalFirstValue: first,
  464. originalLastValue: last,
  465. originalMinValue: min,
  466. originalMaxValue: max,
  467. valueText: ["Start:", formattedFirst, "End:", formattedLast, "Min:", formattedMin, "Max:", formattedMax]
  468. };
  469. if ("winloss" === options.type) {
  470. customizeObject.originalThresholdValue = options.winlossThreshold;
  471. customizeObject.thresholdValue = tooltip.formatValue(options.winlossThreshold)
  472. }
  473. return customizeObject
  474. }
  475. });
  476. _map(["lineColor", "lineWidth", "areaOpacity", "minColor", "maxColor", "barPositiveColor", "barNegativeColor", "winColor", "lessColor", "firstLastColor", "pointSymbol", "pointColor", "pointSize", "type", "argumentField", "valueField", "winlossThreshold", "showFirstLast", "showMinMax", "ignoreEmptyPoints", "minValue", "maxValue"], function(name) {
  477. dxSparkline.prototype._optionChangesMap[name] = "OPTIONS"
  478. });
  479. require("../../core/component_registrator")("dxSparkline", dxSparkline);
  480. module.exports = dxSparkline;
  481. dxSparkline.addPlugin(require("../core/data_source").plugin);