Skip to content

Commit

Permalink
feat(react-component): adding TC markers
Browse files Browse the repository at this point in the history
  • Loading branch information
mnischay committed Jul 17, 2023
1 parent 7c6a017 commit 4105adb
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 201 deletions.
213 changes: 213 additions & 0 deletions packages/react-components/src/components/chart/addTrendCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { v4 as uuid } from 'uuid';
import {
DEFAULT_MARGIN,
trendCursorCloseButtonXOffset,
trendCursorCloseButtonYOffset,
trendCursorHeaderBackgroundColor,
trendCursorHeaderColors,
trendCursorHeaderTextColor,
trendCursorHeaderWidth,
trendCursorLineColor,
trendCursorLineWidth,
trendCursorMarkerRadius,
trendCursorZIndex,
} from './eChartsConstants';
import { EChartsType, ElementEvent, LineSeriesOption, SeriesOption } from 'echarts';
import { ChartEventType, InternalGraphicComponentGroupOption, SizeConfig } from './types';
import close from './close.svg';
import { Dispatch, SetStateAction } from 'react';
import {
GraphicComponentImageOption,
GraphicComponentTextOption,
} from 'echarts/types/src/component/graphic/GraphicModel';
import { Viewport } from '@iot-app-kit/core';
import {
calculateTimeStamp,
calculateTrendCursorsSeriesMakers,
getTrendCursorHeaderTimestampText,
setXWithBounds,
} from './utils/getInfo';

// this function return the TC line and the ondrag handles the user dragging action
const addTCLine = (
uId: string,
graphic: InternalGraphicComponentGroupOption[],
size: SizeConfig,
boundedX: number,
series: SeriesOption[],
yMax: number,
yMin: number,
e: ChartEventType,
setGraphic: Dispatch<SetStateAction<InternalGraphicComponentGroupOption[]>>,
viewport?: Viewport,
chart?: EChartsType
) => ({
type: 'line',
z: trendCursorZIndex,
id: `line-${uId}`,
draggable: 'horizontal' as const,
shape: {
x1: boundedX,
x2: boundedX,
y1: DEFAULT_MARGIN,
y2: size.height - DEFAULT_MARGIN,
},
style: {
stroke: trendCursorLineColor,
lineWidth: trendCursorLineWidth,
},
ondrag: (event: ChartEventType) => {
const graphicIndex = graphic.findIndex((g) => g.children[0].id === event.target.id);
const timeInMs = calculateTimeStamp(event.offsetX ?? 0, size.width, viewport);

// update the x of header and close button
graphic[graphicIndex].children[1].x = setXWithBounds(size, event.offsetX ?? 0);
graphic[graphicIndex].children[2].x = setXWithBounds(size, event.offsetX ?? 0) + trendCursorCloseButtonXOffset;

// update the timestamp on the header
graphic[graphicIndex].children[1].style = {
...graphic[graphicIndex].children[1].style,
text: getTrendCursorHeaderTimestampText(
timeInMs,
(graphic[graphicIndex].children[1] as GraphicComponentTextOption).style?.text
),
};
// add the timestamp to graphic for future use
graphic[graphicIndex].timestampInMs = calculateTimeStamp(e.offsetX ?? 0, size.width, viewport);

// calculate the new Y for the series markers
const onDragYAxisMarkers = calculateTrendCursorsSeriesMakers(series, yMax, yMin, timeInMs, size.height);

// update the Y of the series markers
// childIndex --> purpose
// -----------------------------
// 0 --> line
// 1 --> TC header
// 2 --> close button
// from index 3 --> series markers
for (let i = 0; i < onDragYAxisMarkers.length; i++) {
graphic[graphicIndex].children[i + 3].y = onDragYAxisMarkers[i];
graphic[graphicIndex].children[i + 3].x = event.offsetX ?? 0;
}
// update echarts
chart?.setOption({ graphic });

// update component state
setGraphic(graphic);
},
});

const addTCHeader = (
uId: string,
boundedX: number,
timestampInMs: number,
tcCount: number
): GraphicComponentTextOption => ({
type: 'text',
z: trendCursorZIndex + 1,
id: `text-${uId}`,
x: boundedX,
style: {
y: DEFAULT_MARGIN,
text: getTrendCursorHeaderTimestampText(timestampInMs, `{title|Trend cursor ${tcCount + 1} }`),
lineHeight: 16,
fill: trendCursorHeaderTextColor,
align: 'center',
rich: {
title: {
width: trendCursorHeaderWidth,
backgroundColor: trendCursorHeaderColors[tcCount],
height: 20,
fontSize: 12,
},
timestamp: {
width: trendCursorHeaderWidth,
backgroundColor: trendCursorHeaderBackgroundColor,
height: 15,
fontSize: 9,
fontWeight: 'bold',
},
},
},
});

const addTCDeleteButton = (
uId: string,
boundedX: number,
graphic: InternalGraphicComponentGroupOption[],
setGraphic: Dispatch<SetStateAction<InternalGraphicComponentGroupOption[]>>,
chart?: EChartsType
): GraphicComponentImageOption => ({
id: `image-${uId}`,
type: 'image',
z: trendCursorZIndex + 1,
x: boundedX + trendCursorCloseButtonXOffset,
y: trendCursorCloseButtonYOffset,
style: {
image: close as unknown as string,
},
onmousedown: (event: ChartEventType) => {
const graphicIndex = graphic.findIndex((g) => g.children[2].id === event.target.id);
graphic[graphicIndex].$action = 'remove';
graphic[graphicIndex].children = []; // Echarts will throw error if children are not empty
chart?.setOption({ graphic });
graphic.splice(graphicIndex, 1);
setGraphic(graphic);
},
});

const addTCMarkers = (uId: string, boundedX: number, yAxisMarkers: number[], series: SeriesOption[]) =>
yAxisMarkers.map((marker, index) => ({
id: `circle-${index}-${uId}`,
type: 'circle',
z: trendCursorZIndex + 1,
x: boundedX,
y: marker,
shape: {
r: trendCursorMarkerRadius,
},
style: {
fill: (series[index] as LineSeriesOption)?.lineStyle?.color,
},
}));
// this returns a Graphic element of Echarts (https://echarts.apache.org/en/option.html#graphic)
// A Trend cursor is a custom Graphic group element,
// which has a line and other elements which gets rendered on the screen.
// for now, we are storing the timestamp of the trend cursor in graphic element
// which will eventually be moved to state(redux)
const addNewTrendCursor = (
e: ElementEvent,
size: SizeConfig,
count: number,
graphic: InternalGraphicComponentGroupOption[],
setGraphic: Dispatch<SetStateAction<InternalGraphicComponentGroupOption[]>>,
series: SeriesOption[],
yMax: number,
yMin: number,
viewport?: Viewport,
chart?: EChartsType
) => {
const uId = uuid();
// TODO: test this once echarts live mode is supported
const timestampInMs = calculateTimeStamp(e.offsetX, size.width, viewport);
const boundedX = setXWithBounds(size, e.offsetX ?? 0);
// TODO: test this once echarts live mode is supported
const yAxisMarkers = calculateTrendCursorsSeriesMakers(series, yMax, yMin, timestampInMs, size.height);
const newTC = {
id: `trendCursor-${uId}`,
$action: 'merge',
type: 'group' as const,
timestampInMs,
children: [
addTCLine(uId, graphic, size, boundedX, series, yMax, yMin, e, setGraphic, viewport, chart),
addTCHeader(uId, boundedX, timestampInMs, count) as GraphicComponentTextOption,
addTCDeleteButton(uId, boundedX, graphic, setGraphic, chart) as GraphicComponentImageOption,
...addTCMarkers(uId, boundedX, yAxisMarkers, series),
],
};

graphic.push(newTC);
return graphic;
};

export default addNewTrendCursor;
32 changes: 23 additions & 9 deletions packages/react-components/src/components/chart/baseChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { convertYAxis } from './converters/convertAxis';
import { convertSeriesAndYAxis, reduceSeriesAndYAxis } from './converters/convertSeriesAndYAxis';
import { HotKeys, KeyMap } from 'react-hotkeys';
import useTrendCursors from './useTrendCursors';
import { calculateYMaxMin } from './utils/getInfo';

const keyMap: KeyMap = {
commandDown: { sequence: 'command', action: 'keydown' },
Expand All @@ -22,14 +23,16 @@ const Chart = ({ viewport, queries, size, ...options }: ChartOptions) => {
const { axis } = options;
const defaultSeries: SeriesOption[] = [];
const defaultYAxis: YAXisComponentOption[] = [convertYAxis(axis)];
// Need series data for the calculation of Y for rendering a circle at TS and series lines intersections
const { series, yAxis } = useMemo(
() =>
dataStreams
.map(convertSeriesAndYAxis(options as ChartOptions))
.reduce(reduceSeriesAndYAxis, { series: defaultSeries, yAxis: defaultYAxis }),
[dataStreams]
);

const { series, yAxis, yMin, yMax } = useMemo(() => {
const { series, yAxis } = dataStreams
.map(convertSeriesAndYAxis(options as ChartOptions))
.reduce(reduceSeriesAndYAxis, { series: defaultSeries, yAxis: defaultYAxis });
const { yMax, yMin } = calculateYMaxMin(series);
const updatedYAxis = yAxis.map((y) => ({ ...y, min: yMin, max: yMax }));
return { series, yAxis: updatedYAxis, yMin, yMax };
}, [dataStreams]);

const [trendCursors, setTrendCursors] = useState(options.graphic ?? []);
const [isInCursorAddMode, setIsInCursorAddMode] = useState(false);

Expand All @@ -53,7 +56,18 @@ const Chart = ({ viewport, queries, size, ...options }: ChartOptions) => {
theme: options?.theme,
});

useTrendCursors(ref, trendCursors, size, isInCursorAddMode, setTrendCursors, viewport, options.theme);
useTrendCursors(
ref,
trendCursors,
size,
isInCursorAddMode,
setTrendCursors,
series,
yMax,
yMin,
viewport,
options.theme
);

const handlers = {
commandDown: () => setIsInCursorAddMode(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ export const trendCursorHeaderTextColor = 'white';
export const trendCursorHeaderBackgroundColor = 'black';
export const trendCursorCloseButtonYOffset = DEFAULT_MARGIN + 2.5;
export const trendCursorCloseButtonXOffset = 40;

export const Y_AXIS_INTERPOLATED_VALUE_PRECISION = 3;
export const trendCursorMarkerRadius = 5;
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect } from '@jest/globals';
import { ElementEvent, SeriesOption } from 'echarts';
import addNewTrendCursor from '../addTrendCursor';

describe('addNewTrendCursor', () => {
const mockSize = { width: 500, height: 500 };
const mockSeries = [
{
name: 'Average Wind Speed',
data: [
[1689264600000, 22.939564631713747],
[1689264900000, 24.054178935438895],
[1689265200000, 20.840328700172748],
[1689265500000, 17.627425014582514],
[1689265800000, 17.521569204159785],
],
type: 'line',
step: false,
symbol: 'circle',
symbolSize: 4,
itemStyle: {
color: '#2ea597',
},
lineStyle: {
color: '#2ea597',
type: 'solid',
width: 2,
},
yAxisIndex: 0,
},
] as SeriesOption[];
const mockViewport = {
start: new Date('2023-07-13T16:00:00.000Z'),
end: new Date('2023-07-13T16:30:00.000Z'),
};
it('should add a new TC', () => {
const mockEvent = {} as ElementEvent;
const mockSetGraphic = () => jest.fn();
const newTrendCursor = addNewTrendCursor(
mockEvent,
mockSize,
0,
[],
mockSetGraphic,
mockSeries,
50,
0,
mockViewport
);

expect(newTrendCursor).not.toBeNull();
expect(newTrendCursor[0].children.length).toBe(4);
});

it('on drag to left of the line should provide the first value of line', () => {
const mockEvent = {} as ElementEvent;
const mockSetGraphic = () => jest.fn();
const newTrendCursor = addNewTrendCursor(
mockEvent,
mockSize,
0,
[],
mockSetGraphic,
mockSeries,
50,
0,
mockViewport
);

if (newTrendCursor[0]!.children[0]!.ondrag) {
newTrendCursor[0]!.children[0]!.ondrag({
target: { id: newTrendCursor[0].children[0].id },
offsetX: 51,
} as never);
expect(newTrendCursor[0].children[3].x).toBe(51);
}
});

it('on drag should should the TC x co-ordinate', () => {
const mockEvent = {} as ElementEvent;
const mockSetGraphic = () => jest.fn();
const newTrendCursor = addNewTrendCursor(
mockEvent,
mockSize,
0,
[],
mockSetGraphic,
mockSeries,
50,
0,
mockViewport
);

if (newTrendCursor[0]!.children[0]!.ondrag) {
newTrendCursor[0]!.children[0]!.ondrag({
target: { id: newTrendCursor[0].children[0].id },
offsetX: 100,
} as never);
expect(newTrendCursor[0].children[1].x).toBe(100);
}
});

it('on delete should should the TC x co-ordinate', () => {
const mockEvent = {} as ElementEvent;
const mockSetGraphic = () => jest.fn();
const newTrendCursor = addNewTrendCursor(
mockEvent,
mockSize,
0,
[],
mockSetGraphic,
mockSeries,
50,
0,
mockViewport
);

if (newTrendCursor[0]!.children[2]!.onmousedown) {
newTrendCursor[0]!.children[2]!.onmousedown({
target: { id: newTrendCursor[0].children[2].id },
} as never);
expect(newTrendCursor.length).toBe(0);
}
});
});
Loading

0 comments on commit 4105adb

Please sign in to comment.