Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimise plot lines #234

Merged
merged 9 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 87 additions & 101 deletions src/YagrCore/plugins/plotLines/plotLines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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} from '../../utils/common';
import {calculateFromTo} from './utils';

const MAX_X_SCALE_LINE_OFFSET = 0;
const DRAW_MAP = {
Expand All @@ -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<string, 'draw' | 'drawClear' | 'drawAxes' | 'drawSeries'> = {
'012': 'draw',
'102': 'draw',
Expand All @@ -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<string, PlotLineConfig>();

return function (yagr: Yagr) {
const drawOrder = yagr.config.chart.appearance?.drawOrder;
Expand All @@ -54,12 +49,22 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug

const hook = HOOKS_MAP[drawIndicies] || 'drawClear';

function getLineId(line: PlotLineConfig, scale?: string): string {
if (line.id) {
return line.id;
}
const lineWithoutId = Array.from(plotLines.entries()).find(
([_, l]) => deepIsEqual(l, line) && (!scale || line.scale === scale),
catakot marked this conversation as resolved.
Show resolved Hide resolved
)?.[0];
return lineWithoutId || `${Math.random()}`;
catakot marked this conversation as resolved.
Show resolved Hide resolved
}

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;
}
Expand All @@ -74,48 +79,11 @@ 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;
}
const isBand = Array.isArray(value);
const [from, to] = calculateFromTo(value, scale, timeline, u);
catakot marked this conversation as resolved.
Show resolved Hide resolved

if (scaleCfg.max !== undefined && val > scaleCfg.max) {
return scaleCfg.max;
}
}

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) {
Expand All @@ -130,21 +98,14 @@ export default function plotLinesPlugin(options: PlotLineOptions): PlotLinesPlug
}
}
} else {
const from = u.valToPos(value, scale, true);
const pConf = plotLineConfig as PLineConfig;

ctx.beginPath();

if (scale === DEFAULT_X_SCALE) {
/** Workaround to ensure that plot line will not be drawn over axes */
catakot marked this conversation as resolved.
Show resolved Hide resolved
const last = u.data[0][u.data[0].length - 1] as number;
const lastValue = u.valToPos(last, scale, true);
if (from - lastValue > MAX_X_SCALE_LINE_OFFSET) {
const last = u.valToPos(u.data[0][u.data[0].length - 1] as number, scale, true);
if (from - last > MAX_X_SCALE_LINE_OFFSET) {
continue;
}

ctx.moveTo(from, top);

ctx.lineTo(from, height + top);
} else {
ctx.moveTo(left, from);
Expand All @@ -153,10 +114,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();
}
}
Expand All @@ -170,55 +133,78 @@ 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[], scale?: string | undefined) {
for (const lineToRemove of plotLinesToRemove) {
const lineId = getLineId(lineToRemove, scale);
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<string>();

const removes = plotLines.filter((p) => {
return !hasPlotLine(newPlotLines!, p);
for (const newLine of newPlotLines) {
const lineId = getLineId(newLine, scale);
plotLines.set(lineId, newLine);
existingKeys.add(lineId);
}

// 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 || `${Math.random()}`, {...plotLine, scale});
}
}
}
});
}
},
hooks: {
// @TODO Add feature to draw plot lines over series
Expand Down
43 changes: 43 additions & 0 deletions src/YagrCore/plugins/plotLines/utils/calculateFromTo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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[] | number,
scale: string,
timeline: number[] | UPlot.TypedArray,
u: UPlot,
): number[] {
const isBand = Array.isArray(value);
return isBand
? 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))
: [u.valToPos(value, scale, true), 0];
}
31 changes: 31 additions & 0 deletions src/YagrCore/plugins/plotLines/utils/getPosition.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
1 change: 1 addition & 0 deletions src/YagrCore/plugins/plotLines/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {calculateFromTo} from './calculateFromTo';
Loading