diff --git a/package-lock.json b/package-lock.json index c82bab8..3e0ed51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3324,6 +3324,17 @@ "node": ">= 10.0.0" } }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -10950,24 +10961,6 @@ "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -12049,6 +12042,25 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12831,6 +12843,17 @@ "node": ">=0.8" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -15187,16 +15210,6 @@ "node": ">= 4" } }, - "node_modules/uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -18321,6 +18334,14 @@ "url": "0.10.3", "uuid": "3.3.2", "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } } }, "aws-sign2": { @@ -24213,12 +24234,6 @@ "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", "dev": true }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -25064,6 +25079,14 @@ "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true + } } }, "prelude-ls": { @@ -25664,6 +25687,12 @@ "psl": "^1.1.28", "punycode": "^2.1.1" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -27495,12 +27524,6 @@ "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", "dev": true }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/src/YagrCore/plugins/plotLines/plotLines.ts b/src/YagrCore/plugins/plotLines/plotLines.ts index 4227f99..92cbf42 100644 --- a/src/YagrCore/plugins/plotLines/plotLines.ts +++ b/src/YagrCore/plugins/plotLines/plotLines.ts @@ -3,8 +3,9 @@ import UPlot, {Plugin} from 'uplot'; import {DEFAULT_X_SCALE, DEFAULT_CANVAS_PIXEL_RATIO} from '../../defaults'; import {PLineConfig, PlotLineConfig, YagrPlugin} from '../../types'; import {DrawOrderKey} from '../../utils/types'; -import {deepIsEqual} from '../../utils/common'; import {PBandConfig} from 'src/types'; +import {deepIsEqual, genId} from '../../utils/common'; +import {calculateFromTo} from './utils'; const MAX_X_SCALE_LINE_OFFSET = 0; const DRAW_MAP = { @@ -13,12 +14,6 @@ const DRAW_MAP = { plotLines: 2, }; -function hasPlotLine(list: PlotLineConfig[], p: PlotLineConfig) { - return list.some((pl) => { - return deepIsEqual(pl, p); - }); -} - const HOOKS_MAP: Record = { '012': 'draw', '102': 'draw', @@ -45,7 +40,7 @@ export interface PlotLineOptions { * Axis should be bound to scale. */ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlugin { - let plotLines: PlotLineConfig[] = []; + let plotLines = new Map(); return function (yagr: Yagr) { const drawOrder = yagr.config.chart.appearance?.drawOrder; @@ -54,12 +49,20 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug const hook = HOOKS_MAP[drawIndicies] || 'drawClear'; + function getLineId(line: PlotLineConfig): string { + if (line.id) { + return line.id; + } + const lineWithoutId = Array.from(plotLines.entries()).find(([_, l]) => deepIsEqual(l, line))?.[0]; + return lineWithoutId || genId(); + } + function renderPlotLines(u: UPlot) { const {ctx} = u; const {height, top, width, left} = u.bbox; const timeline = u.data[0]; - for (const plotLineConfig of plotLines) { + for (const plotLineConfig of plotLines.values()) { if (!plotLineConfig.scale) { continue; } @@ -74,48 +77,13 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug const {scale, value} = plotLineConfig; - if (Array.isArray(value)) { - /** This weird code should handles unexpected Inifinities in values */ - const [fromValue, toValue] = value.map((val) => { - if (Math.abs(val) !== Infinity) { - if (scale === DEFAULT_X_SCALE) { - if (val < timeline[0]) { - return timeline[0]; - } - - if (val > timeline[timeline.length - 1]) { - return timeline[timeline.length - 1]; - } - } else { - const scaleCfg = u.scales[scale]; - if (scaleCfg.min !== undefined && val < scaleCfg.min) { - return scaleCfg.min; - } - - if (scaleCfg.max !== undefined && val > scaleCfg.max) { - return scaleCfg.max; - } - } + const isBand = Array.isArray(value); + const [from, to] = isBand + ? calculateFromTo(value, scale, timeline, u) + : [u.valToPos(value, scale, true), 0]; - return val; - } - - const pos = - val > 0 - ? scale === DEFAULT_X_SCALE - ? u.width - : 0 - : scale === DEFAULT_X_SCALE - ? 0 - : u.height; - - return u.posToVal(pos, scale); - }); - - const from = u.valToPos(fromValue, scale, true); - const to = u.valToPos(toValue, scale, true); + if (isBand) { const accent = (plotLineConfig as PBandConfig).accent; - if (scale === DEFAULT_X_SCALE) { ctx.fillRect(from, top, to - from, height); if (accent) { @@ -130,7 +98,6 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug } } } else { - const from = u.valToPos(value, scale, true); const pConf = plotLineConfig as PLineConfig; ctx.beginPath(); @@ -144,7 +111,6 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug } ctx.moveTo(from, top); - ctx.lineTo(from, height + top); } else { ctx.moveTo(left, from); @@ -153,10 +119,12 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug ctx.lineWidth = pConf.width || DEFAULT_CANVAS_PIXEL_RATIO; ctx.strokeStyle = pConf.color || '#000'; - pConf.dash && ctx.setLineDash(pConf.dash); - ctx.closePath(); + if (pConf.dash) { + ctx.setLineDash(pConf.dash); + } ctx.stroke(); } + ctx.restore(); } } @@ -170,55 +138,81 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug } : renderPlotLines; - const plugin = { - get: () => plotLines, - clear: (scale?: string) => { - plotLines = scale - ? plotLines.filter((p) => { - return p.scale !== scale; - }) - : []; - }, - remove: (plotLinesToRemove: PlotLineConfig[]) => { - plotLines = plotLines.filter((p) => { - return !hasPlotLine(plotLinesToRemove, p); - }); - }, - add: (additionalPlotLines: PlotLineConfig[], scale?: string) => { - for (const p of additionalPlotLines) { - plotLines.push(scale ? {scale, ...p} : p); - } - }, - update: (newPlotLines?: PlotLineConfig[], scale?: string) => { - if (!newPlotLines || newPlotLines.length === 0) { - plugin.clear(scale); - return; - } + function addPlotLines(additionalPlotLines: PlotLineConfig[]) { + for (const line of additionalPlotLines) { + const lineId = getLineId(line); + plotLines.set(lineId, line); + } + } - const additions = newPlotLines!.filter((p) => { - return !hasPlotLine(plotLines, p); - }); + function removePlotLines(plotLinesToRemove: PlotLineConfig[]) { + for (const lineToRemove of plotLinesToRemove) { + const lineId = getLineId(lineToRemove); + plotLines.delete(lineId); + } + } + function getPlotLines(): PlotLineConfig[] { + return Array.from(plotLines.values()); + } + + function updatePlotLines(newPlotLines?: PlotLineConfig[], scale?: string) { + if (!newPlotLines || newPlotLines.length === 0) { + clearPlotLines(scale); + return; + } + + const existingKeys = new Set(); + + for (const newLine of newPlotLines) { + const lineId = getLineId(newLine); + plotLines.set(lineId, newLine); + existingKeys.add(lineId); + } - const removes = plotLines.filter((p) => { - return !hasPlotLine(newPlotLines!, p); + // Delete not actual lines from map + for (const [key, line] of plotLines.entries()) { + if ((!scale || line.scale === scale) && !existingKeys.has(key)) { + plotLines.delete(key); + } + } + } + + function clearPlotLines(scale?: string) { + if (scale) { + plotLines.forEach((line, key) => { + if (line.scale === scale) { + plotLines.delete(key); + } }); + } else { + plotLines.clear(); + } + } - additions.length && plugin.add(additions, scale); - removes.length && plugin.remove(removes); - }, + const plugin = { + get: getPlotLines, + clear: clearPlotLines, + remove: removePlotLines, + add: addPlotLines, + update: updatePlotLines, uplot: { opts: () => { const config = yagr.config; - plotLines = []; - - /** Collecting plot lines from config axes for plotLines plugin */ - Object.entries(config.axes).forEach(([scale, axisConfig]) => { - if (axisConfig.plotLines) { - axisConfig.plotLines.forEach((plotLine) => { - plotLines.push({...plotLine, scale}); - }); + plotLines = new Map(); + + for (const scale in config.axes) { + if (config.axes.hasOwnProperty(scale)) { + const axisConfig = config.axes[scale]; + if (axisConfig.plotLines) { + for (const plotLine of axisConfig.plotLines) { + plotLines.set(plotLine.id || genId(), { + ...plotLine, + scale, + }); + } + } } - }); + } }, hooks: { // @TODO Add feature to draw plot lines over series diff --git a/src/YagrCore/plugins/plotLines/utils/calculateFromTo.ts b/src/YagrCore/plugins/plotLines/utils/calculateFromTo.ts new file mode 100644 index 0000000..f43d374 --- /dev/null +++ b/src/YagrCore/plugins/plotLines/utils/calculateFromTo.ts @@ -0,0 +1,40 @@ +import {DEFAULT_X_SCALE} from '../../../defaults'; +import UPlot from 'uplot'; + +export function getPosition(val: number, scale: string, width: number, height: number): number { + if (val > 0 && scale === DEFAULT_X_SCALE) { + return width; + } + if (val > 0 && scale !== DEFAULT_X_SCALE) { + return 0; + } + if (val <= 0 && scale === DEFAULT_X_SCALE) { + return 0; + } + + return height; +} + +export function calculateFromTo( + value: number[], + scale: string, + timeline: number[] | UPlot.TypedArray, + u: UPlot, +): number[] { + return value + .map((val) => { + if (Math.abs(val) !== Infinity) { + if (scale === DEFAULT_X_SCALE) { + return Math.min(Math.max(val, timeline[0]), timeline[timeline.length - 1]); + } else { + const scaleCfg = u.scales[scale]; + return Math.min(Math.max(val, scaleCfg.min ?? val), scaleCfg.max ?? val); + } + } + + const pos = getPosition(val, scale, u.width, u.height); + + return u.posToVal(pos, scale); + }) + .map((val) => u.valToPos(val, scale, true)); +} diff --git a/src/YagrCore/plugins/plotLines/utils/getPosition.test.ts b/src/YagrCore/plugins/plotLines/utils/getPosition.test.ts new file mode 100644 index 0000000..2d86814 --- /dev/null +++ b/src/YagrCore/plugins/plotLines/utils/getPosition.test.ts @@ -0,0 +1,31 @@ +import {getPosition} from './calculateFromTo'; +import {DEFAULT_X_SCALE} from '../../../defaults'; + +describe('getPosition', () => { + const width = 1000; + const height = 500; + + it('should return width when val > 0 and scale is DEFAULT_X_SCALE', () => { + const result = getPosition(10, DEFAULT_X_SCALE, width, height); + expect(result).toBe(width); + }); + + it('should return 0 when val > 0 and scale is not DEFAULT_X_SCALE', () => { + const result = getPosition(10, 'some_other_scale', width, height); + expect(result).toBe(0); + }); + + it('should return 0 when val <= 0 and scale is DEFAULT_X_SCALE', () => { + const result1 = getPosition(0, DEFAULT_X_SCALE, width, height); + const result2 = getPosition(-1, DEFAULT_X_SCALE, width, height); + expect(result1).toBe(0); + expect(result2).toBe(0); + }); + + it('should return height when val <= 0 and scale is not DEFAULT_X_SCALE', () => { + const result1 = getPosition(0, 'some_other_scale', width, height); + const result2 = getPosition(-1, 'some_other_scale', width, height); + expect(result1).toBe(height); + expect(result2).toBe(height); + }); +}); diff --git a/src/YagrCore/plugins/plotLines/utils/index.ts b/src/YagrCore/plugins/plotLines/utils/index.ts new file mode 100644 index 0000000..21f4356 --- /dev/null +++ b/src/YagrCore/plugins/plotLines/utils/index.ts @@ -0,0 +1 @@ +export {calculateFromTo} from './calculateFromTo'; diff --git a/src/YagrCore/utils/common.ts b/src/YagrCore/utils/common.ts index 04f2986..942d333 100644 --- a/src/YagrCore/utils/common.ts +++ b/src/YagrCore/utils/common.ts @@ -268,7 +268,8 @@ const interpolateImpl = ( return result; }; -export const genId = () => Math.random().toString(36).substr(2, 9).replace(/^\d+/, ''); +// https://stackoverflow.com/a/53116778 +export const genId = () => Date.now().toString(36) + Math.random().toString(36).substring(2); /** * Processing data series to: