diff --git a/packages/react-components/src/components/chart/addTrendCursor.ts b/packages/react-components/src/components/chart/addTrendCursor.ts new file mode 100644 index 000000000..b956c749d --- /dev/null +++ b/packages/react-components/src/components/chart/addTrendCursor.ts @@ -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>, + 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>, + 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>, + 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; diff --git a/packages/react-components/src/components/chart/baseChart.tsx b/packages/react-components/src/components/chart/baseChart.tsx index c24891e05..2593c7db5 100644 --- a/packages/react-components/src/components/chart/baseChart.tsx +++ b/packages/react-components/src/components/chart/baseChart.tsx @@ -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' }, @@ -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); @@ -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), diff --git a/packages/react-components/src/components/chart/eChartsConstants.ts b/packages/react-components/src/components/chart/eChartsConstants.ts index 2a8111df8..6e2fbee79 100644 --- a/packages/react-components/src/components/chart/eChartsConstants.ts +++ b/packages/react-components/src/components/chart/eChartsConstants.ts @@ -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; diff --git a/packages/react-components/src/components/chart/tests/addTrendCursor.spec.tsx b/packages/react-components/src/components/chart/tests/addTrendCursor.spec.tsx new file mode 100644 index 000000000..fd8ea2f5c --- /dev/null +++ b/packages/react-components/src/components/chart/tests/addTrendCursor.spec.tsx @@ -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); + } + }); +}); diff --git a/packages/react-components/src/components/chart/Chart.spec.tsx b/packages/react-components/src/components/chart/tests/baseChart.spec.tsx similarity index 96% rename from packages/react-components/src/components/chart/Chart.spec.tsx rename to packages/react-components/src/components/chart/tests/baseChart.spec.tsx index e220013d2..1d71bc5e8 100644 --- a/packages/react-components/src/components/chart/Chart.spec.tsx +++ b/packages/react-components/src/components/chart/tests/baseChart.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; - import { mockTimeSeriesDataQuery } from '@iot-app-kit/testing-util'; import { DataStream } from '@iot-app-kit/core'; -import Chart from './index'; +import Chart from '../index'; import { render } from '@testing-library/react'; const VIEWPORT = { duration: '5m' }; diff --git a/packages/react-components/src/components/chart/tests/getInfo.spec.tsx b/packages/react-components/src/components/chart/tests/getInfo.spec.tsx new file mode 100644 index 000000000..bbd046d53 --- /dev/null +++ b/packages/react-components/src/components/chart/tests/getInfo.spec.tsx @@ -0,0 +1,20 @@ +import { describe, expect } from '@jest/globals'; +import { setXWithBounds } from '../utils/getInfo'; + +describe('Testing Charts getInfo', () => { + const mockSize = { width: 500, height: 500 }; + + describe('setXWithBounds', () => { + it('should return max of width minus margin', () => { + const maxX = setXWithBounds(mockSize, 475); + + expect(maxX).toBe(450); + }); + + it('should return min of margin', () => { + const maxX = setXWithBounds(mockSize, 20); + + expect(maxX).toBe(50); + }); + }); +}); diff --git a/packages/react-components/src/components/chart/useTrendCursors.ts b/packages/react-components/src/components/chart/useTrendCursors.ts index 125c952e3..1540246d6 100644 --- a/packages/react-components/src/components/chart/useTrendCursors.ts +++ b/packages/react-components/src/components/chart/useTrendCursors.ts @@ -1,6 +1,6 @@ import React, { Dispatch, SetStateAction, useEffect } from 'react'; -import { EChartsType, getInstanceByDom } from 'echarts'; -import { addNewTrendCursor } from './utils/getInfo'; +import { EChartsType, getInstanceByDom, SeriesOption } from 'echarts'; +import addNewTrendCursor from './addTrendCursor'; import { Viewport } from '@iot-app-kit/core'; import { InternalGraphicComponentGroupOption, SizeConfig } from './types'; import { MAX_TREND_CURSORS } from './eChartsConstants'; @@ -11,6 +11,9 @@ const useTrendCursors = ( size: SizeConfig, isInCursorAddMode: boolean, setGraphic: Dispatch>, + series: SeriesOption[], + yMax: number, + yMin: number, viewport?: Viewport, theme?: string ) => { @@ -28,7 +31,9 @@ const useTrendCursors = ( chart?.getZr().on('click', (e) => { if (isInCursorAddMode && graphic.length < MAX_TREND_CURSORS) { - setGraphic(addNewTrendCursor(e, size, graphic.length, graphic, setGraphic, viewport, chart)); + setGraphic( + addNewTrendCursor(e, size, graphic.length, graphic, setGraphic, series, yMin, yMax, viewport, chart) + ); } }); @@ -41,7 +46,7 @@ const useTrendCursors = ( chart?.getZr().off('mouseover', () => mouseoverHandler(isInCursorAddMode, chart)); } }; - }, [ref, graphic, size, isInCursorAddMode, setGraphic, viewport, theme]); + }, [ref, graphic, size, isInCursorAddMode, setGraphic, viewport, theme, series, yMin, yMax]); useEffect(() => { console.log(graphic); diff --git a/packages/react-components/src/components/chart/utils/getInfo.spec.tsx b/packages/react-components/src/components/chart/utils/getInfo.spec.tsx deleted file mode 100644 index 36d38afc9..000000000 --- a/packages/react-components/src/components/chart/utils/getInfo.spec.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect } from '@jest/globals'; -import { addNewTrendCursor, setXWithBounds } from './getInfo'; -import { ElementEvent } from 'echarts'; - -describe('Testing Charts getInfo', () => { - const mockSize = { width: 500, height: 500 }; - - describe('addNewTrendCursor', () => { - it('should add a new TC', () => { - const mockEvent = {} as ElementEvent; - const mockSetGraphic = () => jest.fn(); - const mockViewport = { duration: '5m' }; - const newTrendCursor = addNewTrendCursor(mockEvent, mockSize, 0, [], mockSetGraphic, mockViewport); - - expect(newTrendCursor).not.toBeNull(); - expect(newTrendCursor[0].children.length).toBe(3); - }); - - it('on drag should should the TC x co-ordinate', () => { - const mockEvent = {} as ElementEvent; - const mockSetGraphic = () => jest.fn(); - const mockViewport = { duration: '5m' }; - const newTrendCursor = addNewTrendCursor(mockEvent, mockSize, 0, [], mockSetGraphic, 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 mockViewport = { duration: '5m' }; - const newTrendCursor = addNewTrendCursor(mockEvent, mockSize, 0, [], mockSetGraphic, 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); - } - }); - }); - - describe('setXWithBounds', () => { - it('should return max of width minus margin', () => { - const maxX = setXWithBounds(mockSize, 475); - - expect(maxX).toBe(450); - }); - - it('should return min of margin', () => { - const maxX = setXWithBounds(mockSize, 20); - - expect(maxX).toBe(50); - }); - }); -}); diff --git a/packages/react-components/src/components/chart/utils/getInfo.ts b/packages/react-components/src/components/chart/utils/getInfo.ts index 692964777..f4597bee7 100644 --- a/packages/react-components/src/components/chart/utils/getInfo.ts +++ b/packages/react-components/src/components/chart/utils/getInfo.ts @@ -1,23 +1,8 @@ import { DurationViewport, Viewport } from '@iot-app-kit/core/src'; -import { v4 as uuid } from 'uuid'; -import { - DEFAULT_MARGIN, - trendCursorCloseButtonXOffset, - trendCursorCloseButtonYOffset, - trendCursorHeaderBackgroundColor, - trendCursorHeaderColors, - trendCursorHeaderTextColor, - trendCursorHeaderWidth, - trendCursorLineColor, - trendCursorLineWidth, - trendCursorZIndex, -} from '../eChartsConstants'; -import { EChartsType, ElementEvent } from 'echarts'; -import { ChartEventType, InternalGraphicComponentGroupOption, SizeConfig } from '../types'; +import { DEFAULT_MARGIN, Y_AXIS_INTERPOLATED_VALUE_PRECISION } from '../eChartsConstants'; +import { SeriesOption } from 'echarts'; +import { SizeConfig } from '../types'; import { parseDuration } from '../../../utils/time'; -import close from '../close.svg'; -import { Dispatch, SetStateAction } from 'react'; -import { GraphicComponentTextOption } from 'echarts/types/src/component/graphic/GraphicModel'; export const isDurationViewport = (viewport: Viewport): viewport is DurationViewport => (viewport as DurationViewport).duration !== undefined; @@ -63,113 +48,93 @@ export const getTrendCursorHeaderTimestampText = (timestampInMs: number, previou `{timestamp|${new Date(timestampInMs).toLocaleDateString()} ${new Date(timestampInMs).toLocaleTimeString()}}`, ].join('\n'); }; -// 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) -export const addNewTrendCursor = ( - e: ElementEvent, - size: SizeConfig, - count: number, - graphic: InternalGraphicComponentGroupOption[], - setGraphic: Dispatch>, - viewport?: Viewport, - chart?: EChartsType + +// finding the left and right indexes for a given timestamp +// rightIndex === length imples the timestamp lies after the series +// leftIndex < 0 imples the timestamp lies before the series +const getLeftRightIndexes = (data: Array, timestampInMs: number) => { + let rightIndex = data.length; + for (let i = 0; i < data.length; i++) { + const [dataTimestamp] = data[i]; + if (timestampInMs < dataTimestamp) { + rightIndex = i; + break; + } + } + return { leftIndex: rightIndex - 1, rightIndex }; +}; + +const convertValueIntoPixels = (value: number, yMin: number, yMax: number, chartHeightInPixels: number): number => { + const chartHeightInPixelsWoMargin = chartHeightInPixels - 2 * DEFAULT_MARGIN; + const delta = (value * chartHeightInPixelsWoMargin) / (yMax - yMin); + const yAxisInPixels = delta + yMin; + // Need to inverse the Y axis given the 0,0 is the left top corner + return chartHeightInPixels - Number(yAxisInPixels.toFixed(Y_AXIS_INTERPOLATED_VALUE_PRECISION)) - DEFAULT_MARGIN; +}; + +// TODO: update this for bar and step graphs, right now this only works for line graphs +export const calculateTrendCursorsSeriesMakers = ( + series: SeriesOption[], + yMin: number, + yMax: number, + timestampInMs: number, + chartHeightInPixels: number ) => { - const uId = uuid(); - const timestampInMs = calculateTimeStamp(e.offsetX, size.width, viewport); - const boundedX = setXWithBounds(size, e.offsetX ?? 0); - const newTC = { - id: `trendCursor-${uId}`, - $action: 'merge', - type: 'group' as const, - timestampInMs, - children: [ - { - 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); - graphic[graphicIndex].children[1].x = setXWithBounds(size, event.offsetX ?? 0); - graphic[graphicIndex].children[2].x = setXWithBounds(size, event.offsetX ?? 0) + 40; + const trendCursorsSeriesMakers: number[] = []; + series.forEach((s: SeriesOption, seriesIndex) => { + const data = s.data as Array; + // find where the user has moved i.e. find the data indexes within which the TC is dragged / clicked + const { leftIndex, rightIndex } = getLeftRightIndexes(data, timestampInMs); + let value = 0; + + // There is no Left value , so we take the first available value + if (leftIndex < 0) { + value = data[0][1]; + } else if (rightIndex === data.length) { + // There is no right value , so we take the last available value + value = data[data.length - 1][1]; + } else { + // Linear interpolating the value between left and right indexes + const valueMin = data[leftIndex][1]; + const valueMax = data[rightIndex][1]; + const timeMin = data[leftIndex][0]; + const timeMax = data[rightIndex][0]; + const a = (timestampInMs - timeMin) / (timeMax - timeMin); + const delta = valueMax - valueMin === 0 ? 0 : a * (valueMax - valueMin); + value = delta + valueMin; + } + + // Converting the Y axis value to pixels + trendCursorsSeriesMakers[seriesIndex] = convertValueIntoPixels(value, yMin, yMax, chartHeightInPixels); + }); + + return trendCursorsSeriesMakers; +}; + +// adding a 10% to accommodate TC header and rounding it to upper 10 +// TODO: make sure if this is the right way to round up +export const roundUpYAxisMax = (yMax: number) => { + yMax += 0.1 * yMax; + yMax = Math.ceil(yMax / 10) * 10; + return yMax; +}; + +export const calculateYMaxMin = (series: SeriesOption[]) => { + let yMax = Number.MIN_VALUE; + let yMin = 0; - graphic[graphicIndex].children[1].style = { - ...graphic[graphicIndex].children[1].style, - text: getTrendCursorHeaderTimestampText( - timeInMs, - (graphic[graphicIndex].children[1] as GraphicComponentTextOption).style?.text - ), - }; - graphic[graphicIndex].timestampInMs = calculateTimeStamp(e.offsetX ?? 0, size.width, viewport); - chart?.setOption({ graphic }); - setGraphic(graphic); - }, - }, - { - type: 'text', - z: trendCursorZIndex + 1, - id: `text-${uId}`, - x: boundedX, - style: { - y: DEFAULT_MARGIN, - text: getTrendCursorHeaderTimestampText(timestampInMs, `{title|Trend cursor ${count + 1} }`), - lineHeight: 16, - fill: trendCursorHeaderTextColor, - align: 'center', - rich: { - title: { - width: trendCursorHeaderWidth, - backgroundColor: trendCursorHeaderColors[count], - height: 20, - fontSize: 12, - }, - timestamp: { - width: trendCursorHeaderWidth, - backgroundColor: trendCursorHeaderBackgroundColor, - height: 15, - fontSize: 9, - FontWeight: 'bold', - }, - }, - }, - }, - { - id: `image-${uId}`, - type: 'image', - z: trendCursorZIndex + 1, - x: boundedX + trendCursorCloseButtonXOffset, - y: trendCursorCloseButtonYOffset, - style: { - image: close, - }, - 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); - }, - }, - // having the TC header as a different graphic given it will have a different on-click handler - // TODO: need to add the line markers - ], - }; + series.forEach((s: SeriesOption) => { + (s.data as Array).forEach((value) => { + if (value[1] && value[1] > yMax) { + yMax = value[1]; + } + if (value[1] && value[1] < yMin) { + yMin = value[1]; + } + }); + }); - graphic.push(newTC); - return graphic; + // adding a 10% to accommodate TC header and rounding it to upper 10 + yMax = roundUpYAxisMax(yMax); + return { yMin, yMax }; };