diff --git a/packages/react-components/src/components/chart/baseChart.tsx b/packages/react-components/src/components/chart/baseChart.tsx index 016926b9c..8fc06a821 100644 --- a/packages/react-components/src/components/chart/baseChart.tsx +++ b/packages/react-components/src/components/chart/baseChart.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useECharts, useResizeableEChart, @@ -9,9 +9,8 @@ import { import { ChartOptions } from './types'; import { useVisualizedDataStreams } from './hooks/useVisualizedDataStreams'; import { useConvertedOptions } from './converters/convertOptions'; -import { ElementEvent } from 'echarts'; import { useSeriesAndYAxis } from './converters/convertSeriesAndYAxis'; -import { HotKeys, KeyMap } from 'react-hotkeys'; +import { HotKeys } from 'react-hotkeys'; import useTrendCursors from './hooks/useTrendCursors'; import { Resizable } from 'react-resizable'; import Legend from './legend/legend'; @@ -21,11 +20,9 @@ import { useChartId } from './hooks/useChartId'; import { useChartSetOptionSettings } from './hooks/useChartSetOptionSettings'; import './chart.css'; - -const keyMap: KeyMap = { - commandDown: { sequence: 'command', action: 'keydown' }, - commandUp: { sequence: 'command', action: 'keyup' }, -}; +import { useXAxis } from './hooks/useXAxis'; +import { useContextMenu } from './hooks/useContextMenu'; +import { useViewportToMS } from './hooks/useViewportToMS'; /** * Developer Notes: @@ -36,7 +33,10 @@ const keyMap: KeyMap = { * 2. get datastreams using useVisualizedDataStreams * 3. use datastreams / chart options to compute datastructures * needed to implement various features and adapt them to the echarts api - * 4. set all of the options in echarts using useEChartOptions + * 4. set all the options in echarts using useEChartOptions + * 5. do not make use of setOptions in the individual feature, useEChartOptions should be the only place. + * Exception: when deleting an item or in general when removing some elements you may need to use setOptions + * */ /** @@ -51,52 +51,40 @@ const Chart = ({ viewport, queries, size = { width: 500, height: 500 }, ...optio // convert TimeSeriesDataQuery to TimeSeriesData const { isLoading, dataStreams } = useVisualizedDataStreams(queries, viewport); - // Setup resize container and calculate size for echart + // Setup resize container and calculate size for echarts const { width, height, chartWidth, onResize, minConstraints, maxConstraints } = useResizeableEChart(chartRef, size); - // apply group to echart + // apply group to echarts useGroupableEChart(chartRef, options.groupId); - // apply loading animation to echart + // apply loading animation to echarts useLoadableEChart(chartRef, isLoading); // calculate style settings for all datastreams const [styleSettingsMap] = useChartStyleSettings(dataStreams, options); - const [trendCursors, setTrendCursors] = useState(options.graphic ?? []); - const [isInCursorAddMode, setIsInCursorAddMode] = useState(false); - // TECHDEBT: let's try to refactor contet menu state into some hook associated with the component - const [showContextMenu, setShowContextMenu] = useState(false); - const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 }); - - const handleContextMenu = (e: ElementEvent) => { - setContextMenuPos({ x: e.offsetX, y: e.offsetY }); - setShowContextMenu(!showContextMenu); - e.stop(); - }; - // adapt datastreams into echarts series and yAxis data const { series, yAxis } = useSeriesAndYAxis(dataStreams, { styleSettings: styleSettingsMap, axis: options.axis }); + const { handleContextMenu, showContextMenu, contextMenuPos, setShowContextMenu, keyMap } = useContextMenu(); + + // const viewportInMs = viewportToMs(viewport); + const viewportInMs = useViewportToMS(viewport); + + const xAxis = useXAxis(viewportInMs, options.axis); + // this will handle all the Trend Cursors operations - const { onContextMenuClickHandler } = useTrendCursors({ + const { onContextMenuClickHandler, trendCursors, hotKeyHandlers } = useTrendCursors({ ref, - graphic: trendCursors, + initialGraphic: options.graphic, size: { width: chartWidth, height }, - isInCursorAddMode, - setGraphic: setTrendCursors, series, chartId, - viewport, + viewportInMs, groupId: options.groupId, onContextMenu: handleContextMenu, }); - const handlers = { - commandDown: () => setIsInCursorAddMode(true), - commandUp: () => setIsInCursorAddMode(false), - }; - const menuOptionClickHandler = ({ action }: { action: Action; e: React.MouseEvent }) => { onContextMenuClickHandler({ action, posX: contextMenuPos.x }); setShowContextMenu(false); @@ -111,13 +99,14 @@ const Chart = ({ viewport, queries, size = { width: 500, height: 500 }, ...optio // determine the set option settings const settings = useChartSetOptionSettings(dataStreams); - // set all of the options on the echarts instance + // set all the options on the echarts instance useEChartOptions( chartRef, { ...convertedOptions, series, yAxis, + xAxis, graphic: trendCursors, }, settings @@ -133,7 +122,7 @@ const Chart = ({ viewport, queries, size = { width: 500, height: 500 }, ...optio minConstraints={minConstraints} maxConstraints={maxConstraints} > - +
{/*TODO: should not show when in dashboard */} {showContextMenu && ( diff --git a/packages/react-components/src/components/chart/converters/convertAxis.ts b/packages/react-components/src/components/chart/converters/convertAxis.ts index 757d33eb5..7fd562c30 100644 --- a/packages/react-components/src/components/chart/converters/convertAxis.ts +++ b/packages/react-components/src/components/chart/converters/convertAxis.ts @@ -1,11 +1,6 @@ -import { XAXisComponentOption, YAXisComponentOption } from 'echarts'; +import { YAXisComponentOption } from 'echarts'; import { ChartAxisOptions } from '../types'; -import { DEFAULT_X_AXIS, DEFAULT_Y_AXIS } from '../eChartsConstants'; - -export const convertXAxis = (axis: ChartAxisOptions | undefined): XAXisComponentOption => ({ - ...DEFAULT_X_AXIS, - show: axis?.showX ?? DEFAULT_X_AXIS.show, -}); +import { DEFAULT_Y_AXIS } from '../eChartsConstants'; export const convertYAxis = (axis: ChartAxisOptions | undefined): YAXisComponentOption => ({ ...DEFAULT_Y_AXIS, diff --git a/packages/react-components/src/components/chart/converters/convertOptions.ts b/packages/react-components/src/components/chart/converters/convertOptions.ts index b7f0f833d..d690a7ae7 100644 --- a/packages/react-components/src/components/chart/converters/convertOptions.ts +++ b/packages/react-components/src/components/chart/converters/convertOptions.ts @@ -2,7 +2,6 @@ import { ChartOptions } from '../types'; import { EChartsOption } from 'echarts'; import { DEFAULT_DATA_ZOOM } from '../eChartsConstants'; import { convertLegend } from './convertLegend'; -import { convertXAxis } from './convertAxis'; import { convertGrid } from './convertGrid'; import { convertTooltip } from './convertTooltip'; import { useMemo } from 'react'; @@ -17,17 +16,20 @@ type ConvertChartOptions = Pick< * @returns adapted echarts options */ export const convertOptions = (options: ConvertChartOptions): EChartsOption => { - const { backgroundColor, axis, gestures, legend, significantDigits, title } = options; + const { backgroundColor, gestures, legend, significantDigits, title } = options; return { title: { text: title, // options.seriesLength === 0 ? 'No data present' : '', }, backgroundColor, - xAxis: [convertXAxis(axis)], grid: convertGrid(legend), dataZoom: gestures ? DEFAULT_DATA_ZOOM : undefined, legend: convertLegend(legend), tooltip: convertTooltip(significantDigits), + // TODO: test the below values to have a smooth transition especially with 10 seconds viewport these are placeholder values + animationEasing: 'linear', + animationEasingUpdate: 'linear', + animationDurationUpdate: 1500, }; }; diff --git a/packages/react-components/src/components/chart/converters/convertStyles.spec.tsx b/packages/react-components/src/components/chart/converters/convertStyles.spec.tsx index c09dfd904..4bfd3f030 100644 --- a/packages/react-components/src/components/chart/converters/convertStyles.spec.tsx +++ b/packages/react-components/src/components/chart/converters/convertStyles.spec.tsx @@ -1,6 +1,3 @@ -// const { result } = renderHook(() => useGridSettings(), { -// wrapper: ({ children }) => , -// }); import React from 'react'; import { renderHook } from '@testing-library/react'; import { getChartStyleSettingsFromMap, useChartStyleSettings } from './convertStyles'; diff --git a/packages/react-components/src/components/chart/converters/converters.spec.ts b/packages/react-components/src/components/chart/converters/converters.spec.ts index b2f92447c..504c7e3d5 100644 --- a/packages/react-components/src/components/chart/converters/converters.spec.ts +++ b/packages/react-components/src/components/chart/converters/converters.spec.ts @@ -1,6 +1,6 @@ import { mockTimeSeriesDataQuery } from '@iot-app-kit/testing-util'; import { ChartLegend } from '../types'; -import { convertXAxis, convertYAxis } from './convertAxis'; +import { convertYAxis } from './convertAxis'; import { convertDataPoint } from './convertDataPoint'; import { convertGrid } from './convertGrid'; import { convertLegend } from './convertLegend'; @@ -36,146 +36,142 @@ const QUERIES = mockTimeSeriesDataQuery([ }, ]); -it('converts axis to eCharts axis', async () => { - const convertedXAxis = convertXAxis(MOCK_AXIS); - const convertedYAxis = convertYAxis(MOCK_AXIS); +describe('testing converters', () => { + it('converts axis to eCharts axis', async () => { + const convertedYAxis = convertYAxis(MOCK_AXIS); - expect(convertedXAxis).toHaveProperty('show', true); - expect(convertedXAxis).toHaveProperty('type', 'time'); + expect(convertedYAxis).toHaveProperty('type', 'value'); + expect(convertedYAxis).toHaveProperty('name', MOCK_AXIS.yAxisLabel); + expect(convertedYAxis).toHaveProperty('show', true); + }); - expect(convertedYAxis).toHaveProperty('type', 'value'); - expect(convertedYAxis).toHaveProperty('name', MOCK_AXIS.yAxisLabel); - expect(convertedYAxis).toHaveProperty('show', true); -}); + it('converts data points to echarts data points', async () => { + const convertedDataPoints = MOCK_DATA_POINTS.map(convertDataPoint); + expect(convertedDataPoints).toStrictEqual([ + [1630005300000, 100], + [1634005300000, 50], + [1630005300000, 100], + [1634005300000, 50], + ]); + }); -it('converts data points to echarts data points', async () => { - const convertedDataPoints = MOCK_DATA_POINTS.map(convertDataPoint); - expect(convertedDataPoints).toStrictEqual([ - [1630005300000, 100], - [1634005300000, 50], - [1630005300000, 100], - [1634005300000, 50], - ]); -}); + it('converts grid to echarts grid', async () => { + const convertedGrid = convertGrid(MOCK_LEGEND); -it('converts grid to echarts grid', async () => { - const convertedGrid = convertGrid(MOCK_LEGEND); + expect(convertedGrid).toHaveProperty('bottom', 50); + expect(convertedGrid).toHaveProperty('top', 50); + expect(convertedGrid).toHaveProperty('containLabel', false); + }); - expect(convertedGrid).toHaveProperty('bottom', 50); - expect(convertedGrid).toHaveProperty('top', 50); - expect(convertedGrid).toHaveProperty('containLabel', false); -}); + it('converts legend to echarts legend', async () => { + const convertedLegend = convertLegend(MOCK_LEGEND); -it('converts legend to echarts legend', async () => { - const convertedLegend = convertLegend(MOCK_LEGEND); + expect(convertedLegend).toHaveProperty('show', true); + expect(convertedLegend).toHaveProperty('orient', 'horizontal'); + expect(convertedLegend).toHaveProperty('backgroundColor', 'white'); + }); - expect(convertedLegend).toHaveProperty('show', true); - expect(convertedLegend).toHaveProperty('orient', 'horizontal'); - expect(convertedLegend).toHaveProperty('backgroundColor', 'white'); -}); + it('converts chart options to echarts options', async () => { + const convertedOptions = convertOptions({ + backgroundColor: 'white', + axis: MOCK_AXIS, + legend: MOCK_LEGEND, + significantDigits: 2, + }); -it('converts chart options to echarts options', async () => { - const convertedOptions = convertOptions({ - backgroundColor: 'white', - axis: MOCK_AXIS, - legend: MOCK_LEGEND, - significantDigits: 2, + expect(convertedOptions).toHaveProperty('backgroundColor', 'white'); + expect(convertedOptions).toHaveProperty('grid.bottom', 50); + }); + it('converts empty series data to echarts data', async () => { + const options = { + resolution: 0, + queries: [], + seriesLength: 0, + backgroundColor: 'white', + axis: MOCK_AXIS, + legend: MOCK_LEGEND, + significantDigits: 2, + }; + + const chartOptions = { + styleSettings: { 'abc-1': { color: 'red' } }, + defaultVisualizationType: 'step-middle', + } as const; + const datastream = { ...options, data: [], id: 'abc-1' }; + const styles = convertStyles(chartOptions)(datastream); + const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); + const result = convertedSeriesAndYAxisFunc(datastream); + + expect(result.series.data).toBeArrayOfSize(0); + expect(result).toHaveProperty('series.itemStyle.color', '#688ae8'); + expect(result).toHaveProperty('series.step', 'middle'); }); - expect(convertedOptions).toHaveProperty('backgroundColor', 'white'); - expect(convertedOptions).toHaveProperty('xAxis[0].show', true); - expect(convertedOptions).toHaveProperty('xAxis[0].type', 'time'); - expect(convertedOptions).toHaveProperty('grid.bottom', 50); -}); -it('converts empty series data to echarts data', async () => { - const options = { - resolution: 0, - queries: [], - seriesLength: 0, - backgroundColor: 'white', - axis: MOCK_AXIS, - legend: MOCK_LEGEND, - significantDigits: 2, - }; - - const chartOptions = { - styleSettings: { 'abc-1': { color: 'red' } }, - defaultVisualizationType: 'step-middle', - } as const; - const datastream = { ...options, data: [], id: 'abc-1' }; - const styles = convertStyles(chartOptions)(datastream); - const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); - const result = convertedSeriesAndYAxisFunc(datastream); - - expect(result.series.data).toBeArrayOfSize(0); - expect(result).toHaveProperty('series.itemStyle.color', '#688ae8'); - expect(result).toHaveProperty('series.step', 'middle'); -}); - -it('converts non empty series data to echarts data', async () => { - const options = { - resolution: 0, - queries: [QUERIES], - seriesLength: 1, - backgroundColor: 'white', - axis: MOCK_AXIS, - legend: MOCK_LEGEND, - significantDigits: 2, - }; - const chartOptions = { - defaultVisualizationType: 'step-start', - styleSettings: { 'abc-1': { color: 'red' } }, - } as const; - const datastream = { ...options, data: [], id: 'abc-1' }; - const styles = convertStyles(chartOptions)(datastream); - const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); - const result = convertedSeriesAndYAxisFunc(datastream); - - expect(result.series.data).toBeArrayOfSize(0); - expect(result).toHaveProperty('series.data', []); - expect(result).toHaveProperty('series.step', 'start'); -}); + it('converts non empty series data to echarts data', async () => { + const options = { + resolution: 0, + queries: [QUERIES], + seriesLength: 1, + backgroundColor: 'white', + axis: MOCK_AXIS, + legend: MOCK_LEGEND, + significantDigits: 2, + }; + const chartOptions = { + defaultVisualizationType: 'step-start', + styleSettings: { 'abc-1': { color: 'red' } }, + } as const; + const datastream = { ...options, data: [], id: 'abc-1' }; + const styles = convertStyles(chartOptions)(datastream); + const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); + const result = convertedSeriesAndYAxisFunc(datastream); + + expect(result.series.data).toBeArrayOfSize(0); + expect(result).toHaveProperty('series.data', []); + expect(result).toHaveProperty('series.step', 'start'); + }); -it('converts non empty series data with no default vis type to echarts data', async () => { - const options = { - resolution: 0, - queries: [QUERIES], - seriesLength: 1, - backgroundColor: 'white', - axis: MOCK_AXIS, - legend: MOCK_LEGEND, - significantDigits: 2, - }; - - const chartOptions = { - styleSettings: { 'abc-1': { color: 'red' } }, - } as const; - const datastream = { ...options, data: [], id: 'abc-1' }; - const styles = convertStyles(chartOptions)(datastream); - const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); - const result = convertedSeriesAndYAxisFunc(datastream); - - expect(result.series.data).toBeArrayOfSize(0); - expect(result.series.name).toBeUndefined(); - expect(result).toHaveProperty('series.step', false); -}); + it('converts non empty series data with no default vis type to echarts data', async () => { + const options = { + resolution: 0, + queries: [QUERIES], + seriesLength: 1, + backgroundColor: 'white', + axis: MOCK_AXIS, + legend: MOCK_LEGEND, + significantDigits: 2, + }; + + const chartOptions = { + styleSettings: { 'abc-1': { color: 'red' } }, + } as const; + const datastream = { ...options, data: [], id: 'abc-1' }; + const styles = convertStyles(chartOptions)(datastream); + const convertedSeriesAndYAxisFunc = convertSeriesAndYAxis(styles); + const result = convertedSeriesAndYAxisFunc(datastream); + + expect(result.series.data).toBeArrayOfSize(0); + expect(result.series.name).toBeUndefined(); + expect(result).toHaveProperty('series.step', false); + }); -it('converts tooltip', async () => { - const convertedTooltip = convertTooltip(2); + it('converts tooltip', async () => { + const convertedTooltip = convertTooltip(2); - expect(convertedTooltip).toHaveProperty('trigger', 'axis'); - expect(convertedTooltip.valueFormatter).toBeFunction(); + expect(convertedTooltip).toHaveProperty('trigger', 'axis'); + expect(convertedTooltip.valueFormatter).toBeFunction(); - const valueFormatter = convertedTooltip.valueFormatter; - if (valueFormatter) expect(valueFormatter(300)).toBe('300'); -}); + const valueFormatter = convertedTooltip.valueFormatter; + if (valueFormatter) expect(valueFormatter(300)).toBe('300'); + }); -it('converts tooltip with value array', async () => { - const convertedTooltip = convertTooltip(2); + it('converts tooltip with value array', async () => { + const convertedTooltip = convertTooltip(2); - expect(convertedTooltip).toHaveProperty('trigger', 'axis'); - expect(convertedTooltip.valueFormatter).toBeFunction(); + expect(convertedTooltip).toHaveProperty('trigger', 'axis'); + expect(convertedTooltip.valueFormatter).toBeFunction(); - const valueFormatter = convertedTooltip.valueFormatter; - if (valueFormatter) expect(valueFormatter([300, 10, 20000])).toBe('300, 10, 20000'); + const valueFormatter = convertedTooltip.valueFormatter; + if (valueFormatter) expect(valueFormatter([300, 10, 20000])).toBe('300, 10, 20000'); + }); }); diff --git a/packages/react-components/src/components/chart/eChartsConstants.ts b/packages/react-components/src/components/chart/eChartsConstants.ts index 901b050fc..46cef57db 100644 --- a/packages/react-components/src/components/chart/eChartsConstants.ts +++ b/packages/react-components/src/components/chart/eChartsConstants.ts @@ -53,6 +53,10 @@ export const DEFAULT_DATA_ZOOM: DataZoomComponentOption = { moveOnMouseMove: true, moveOnMouseWheel: false, }; +// this is the chart live mode refresh rate, this should be inline with the animation props +// https://echarts.apache.org/en/option.html#animation +// packages/react-components/src/components/chart/converters , line 30 +export const LIVE_MODE_REFRESH_RATE_MS = 1000; // Trend Cursor constants export const TREND_CURSOR_HEADER_COLORS = ['#DA7596', '#2EA597', '#688AE8', '#A783E1', '#E07941']; diff --git a/packages/react-components/src/components/chart/hooks/useChartSetOptionSettings.ts b/packages/react-components/src/components/chart/hooks/useChartSetOptionSettings.ts index cc65c57c7..95a5d978e 100644 --- a/packages/react-components/src/components/chart/hooks/useChartSetOptionSettings.ts +++ b/packages/react-components/src/components/chart/hooks/useChartSetOptionSettings.ts @@ -16,7 +16,6 @@ export const useChartSetOptionSettings = (datastreams: DataStream[]) => { * see setOption api for more information on settings * https://echarts.apache.org/en/api.html#echartsInstance.setOption * - * NOTE: we can use this hook to refactor trend cursor set option also */ const settings = datastreamsDepsRef.current !== datastreamsDeps ? { replaceMerge: ['series'] } : undefined; datastreamsDepsRef.current = datastreamsDeps; diff --git a/packages/react-components/src/components/chart/hooks/useContextMenu.ts b/packages/react-components/src/components/chart/hooks/useContextMenu.ts new file mode 100644 index 000000000..e98870ea5 --- /dev/null +++ b/packages/react-components/src/components/chart/hooks/useContextMenu.ts @@ -0,0 +1,18 @@ +import { useState } from 'react'; +import { ElementEvent } from 'echarts'; +import { KeyMap } from 'react-hotkeys'; + +export const useContextMenu = () => { + const [showContextMenu, setShowContextMenu] = useState(false); + const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 }); + const handleContextMenu = (e: ElementEvent) => { + setContextMenuPos({ x: e.offsetX, y: e.offsetY }); + setShowContextMenu(!showContextMenu); + e.stop(); + }; + const keyMap: KeyMap = { + commandDown: { sequence: 't', action: 'keydown' }, + commandUp: { sequence: 't', action: 'keyup' }, + }; + return { handleContextMenu, showContextMenu, contextMenuPos, setShowContextMenu, keyMap }; +}; diff --git a/packages/react-components/src/components/chart/hooks/useTrendCursors.ts b/packages/react-components/src/components/chart/hooks/useTrendCursors.ts index da4c4452b..6b70e79f0 100644 --- a/packages/react-components/src/components/chart/hooks/useTrendCursors.ts +++ b/packages/react-components/src/components/chart/hooks/useTrendCursors.ts @@ -3,23 +3,25 @@ import useDataStore from '../../../store'; import handleResize from '../utils/handleResize'; import handleSync from '../utils/handleSync'; import useTrendCursorsEvents from './useTrendCursorsEvents'; +import { useState } from 'react'; +import { handleViewport } from '../utils/handleViewport'; const useTrendCursors = ({ ref, - graphic, size, - isInCursorAddMode, - setGraphic, series, chartId, - viewport, + viewportInMs, groupId, onContextMenu, + initialGraphic, }: UseTrendCursorsProps) => { // for debugging purposes console.log(`useTrendCursors for chart id : ${chartId}`); const isInSyncMode = useDataStore((state) => (groupId ? !!state.trendCursorGroups[groupId] : false)); + const [graphic, setGraphic] = useState(initialGraphic ?? []); + const [isInCursorAddMode, setIsInCursorAddMode] = useState(false); // hook for handling all user events const { onContextMenuClickHandler } = useTrendCursorsEvents({ @@ -28,7 +30,7 @@ const useTrendCursors = ({ size, isInCursorAddMode, setGraphic, - viewport, + viewportInMs, series, isInSyncMode, groupId, @@ -36,12 +38,19 @@ const useTrendCursors = ({ }); // for handling the resize of chart - handleResize({ series, size, graphic, setGraphic, viewport, ref }); + handleResize({ series, size, graphic, setGraphic, viewportInMs, ref }); // handling the trend cursor sync mode - handleSync({ ref, isInSyncMode, graphic, setGraphic, viewport, series, size, groupId }); + handleSync({ ref, isInSyncMode, graphic, setGraphic, viewportInMs, series, size, groupId }); - return { onContextMenuClickHandler }; + const hotKeyHandlers = { + commandDown: () => setIsInCursorAddMode(true), + commandUp: () => setIsInCursorAddMode(false), + }; + + handleViewport({ graphic, setGraphic, viewportInMs, size }); + + return { onContextMenuClickHandler, hotKeyHandlers, trendCursors: graphic }; }; export default useTrendCursors; diff --git a/packages/react-components/src/components/chart/hooks/useTrendCursorsEvents.ts b/packages/react-components/src/components/chart/hooks/useTrendCursorsEvents.ts index 698d8d12c..9b4a33acc 100644 --- a/packages/react-components/src/components/chart/hooks/useTrendCursorsEvents.ts +++ b/packages/react-components/src/components/chart/hooks/useTrendCursorsEvents.ts @@ -17,7 +17,7 @@ const useTrendCursorsEvents = ({ size, isInCursorAddMode, setGraphic, - viewport, + viewportInMs, series, isInSyncMode, groupId, @@ -41,7 +41,7 @@ const useTrendCursorsEvents = ({ const seriesRef = useRef(series); const sizeRef = useRef(size); const isInCursorAddModeRef = useRef(isInCursorAddMode); - const viewportRef = useRef(viewport); + const viewportInMsRef = useRef(viewportInMs); const isInSyncModeRef = useRef(isInSyncMode); const graphicRef = useRef(graphic); const setGraphicRef = useRef(setGraphic); @@ -50,20 +50,20 @@ const useTrendCursorsEvents = ({ // these properties will be updated in every render so that the event handlers below is not re-rendered everytime useEffect(() => { seriesRef.current = series; - viewportRef.current = viewport; + viewportInMsRef.current = viewportInMs; isInCursorAddModeRef.current = isInCursorAddMode; isInSyncModeRef.current = isInSyncMode; graphicRef.current = graphic; sizeRef.current = size; setGraphicRef.current = setGraphic; - }, [series, size, isInCursorAddMode, setGraphic, viewport, isInSyncMode, graphic]); + }, [series, size, isInCursorAddMode, setGraphic, viewportInMs, isInSyncMode, graphic]); // shared add function between the context menu and on click action const addNewTrendCursor = ({ posX, ignoreHotKey }: { posX: number; ignoreHotKey: boolean }) => { // when adding through the context menu, we can ignore the hot key press if ((ignoreHotKey || isInCursorAddModeRef.current) && graphicRef.current.length < MAX_TREND_CURSORS) { if (isInSyncModeRef.current) { - const timestampInMs = calculateTimeStamp(posX, sizeRef.current.width, viewportRef.current); + const timestampInMs = calculateTimeStamp(posX, sizeRef.current.width, viewportInMsRef.current); addTrendCursorsToSyncState({ groupId: groupId ?? '', tcId: `trendCursor-${uuid()}`, @@ -75,7 +75,7 @@ const useTrendCursorsEvents = ({ size: sizeRef.current, tcHeaderColorIndex: trendCursorStaticIndex++, series: seriesRef.current, - viewport: viewportRef.current, + viewportInMs: viewportInMsRef.current, x: posX, ref, }); @@ -105,7 +105,7 @@ const useTrendCursorsEvents = ({ } }; const deleteTrendCursorByPosition = (clickedPosX: number) => { - const timestampOfClick = calculateTimeStamp(clickedPosX, sizeRef.current.width, viewportRef.current); + const timestampOfClick = calculateTimeStamp(clickedPosX, sizeRef.current.width, viewportInMsRef.current); const toBeDeletedGraphicIndex = calculateNearestTcIndex(graphicRef.current, timestampOfClick); deleteTrendCursor(toBeDeletedGraphicIndex); }; @@ -140,7 +140,7 @@ const useTrendCursorsEvents = ({ chart?.getZr().on('drag', (event) => { if (event.target.id.toString().startsWith('line')) { const graphicIndex = graphicRef.current.findIndex((g) => g.children[0].id === event.target.id); - const timeInMs = calculateTimeStamp(event.offsetX ?? 0, sizeRef.current.width, viewportRef.current); + const timeInMs = calculateTimeStamp(event.offsetX ?? 0, sizeRef.current.width, viewportInMsRef.current); if (isInSyncModeRef.current) { updateTrendCursorsInSyncState({ groupId: groupId ?? '', @@ -182,7 +182,7 @@ const useTrendCursorsEvents = ({ }, [ref]); const copyTrendCursorData = (posX: number) => { - const timestampOfClick = calculateTimeStamp(posX, sizeRef.current.width, viewportRef.current); + const timestampOfClick = calculateTimeStamp(posX, sizeRef.current.width, viewportInMsRef.current); const toBeCopiedGraphicIndex = calculateNearestTcIndex(graphicRef.current, timestampOfClick); // using copy-to-clipboard library to copy in a Excel sheet pastable format copy(formatCopyData(graphicRef.current[toBeCopiedGraphicIndex], seriesRef.current), { format: 'text/plain' }); diff --git a/packages/react-components/src/components/chart/hooks/useViewportToMS.ts b/packages/react-components/src/components/chart/hooks/useViewportToMS.ts new file mode 100644 index 000000000..051954786 --- /dev/null +++ b/packages/react-components/src/components/chart/hooks/useViewportToMS.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; +import { isDurationViewport, convertViewportToMs } from '../utils/getInfo'; +import { Viewport } from '@iot-app-kit/core'; +import { LIVE_MODE_REFRESH_RATE_MS } from '../eChartsConstants'; + +export const useViewportToMS = (viewport?: Viewport) => { + const [inMS, setInMS] = useState(convertViewportToMs(viewport)); + + const viewportString = JSON.stringify(viewport); + useEffect(() => { + let interval: NodeJS.Timer; + if (viewport && isDurationViewport(viewport)) { + interval = setInterval(() => { + setInMS(convertViewportToMs(viewport)); + }, LIVE_MODE_REFRESH_RATE_MS); + } else { + setInMS(convertViewportToMs(viewport)); + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [viewportString]); + + return inMS; +}; diff --git a/packages/react-components/src/components/chart/hooks/useXAxis.ts b/packages/react-components/src/components/chart/hooks/useXAxis.ts new file mode 100644 index 000000000..27a3ce7fc --- /dev/null +++ b/packages/react-components/src/components/chart/hooks/useXAxis.ts @@ -0,0 +1,17 @@ +import { ChartAxisOptions, ViewportInMs } from '../types'; +import { DEFAULT_X_AXIS } from '../eChartsConstants'; +import { XAXisOption } from 'echarts/types/dist/shared'; +import { useMemo } from 'react'; + +export const useXAxis = (viewportInMs: ViewportInMs, axis?: ChartAxisOptions): XAXisOption => { + return useMemo( + () => ({ + show: axis?.showX ?? DEFAULT_X_AXIS.show, + type: 'time' as const, + splitNumber: 6, + min: viewportInMs.initial, + max: viewportInMs.end, + }), + [viewportInMs] + ); +}; diff --git a/packages/react-components/src/components/chart/tests/getInfo.spec.tsx b/packages/react-components/src/components/chart/tests/getInfo.spec.tsx index 28392006b..a33fb4057 100644 --- a/packages/react-components/src/components/chart/tests/getInfo.spec.tsx +++ b/packages/react-components/src/components/chart/tests/getInfo.spec.tsx @@ -1,5 +1,6 @@ import { describe, expect } from '@jest/globals'; import { calculateXFromTimestamp, setXWithBounds } from '../utils/getInfo'; +import { mockViewportInMs } from './getTrendCursor.spec'; describe('Testing Charts getInfo', () => { const mockSize = { width: 500, height: 500 }; @@ -19,13 +20,8 @@ describe('Testing Charts getInfo', () => { }); describe('calculateXFromTimestamp', () => { - const mockViewport = { - start: new Date('2023-07-13T16:00:00.000Z'), - end: new Date('2023-07-13T16:30:00.000Z'), - }; - it('should return new x for a given timestamp', () => { - const maxX = calculateXFromTimestamp(1689265200000, mockSize, mockViewport); + const maxX = calculateXFromTimestamp(1689265200000, mockSize, mockViewportInMs); expect(Number(maxX.toPrecision(2))).toBe(320); }); diff --git a/packages/react-components/src/components/chart/tests/getTrendCursor.spec.tsx b/packages/react-components/src/components/chart/tests/getTrendCursor.spec.tsx index c59d9a95b..b7e3fb25c 100644 --- a/packages/react-components/src/components/chart/tests/getTrendCursor.spec.tsx +++ b/packages/react-components/src/components/chart/tests/getTrendCursor.spec.tsx @@ -1,7 +1,7 @@ import { describe, expect } from '@jest/globals'; import { ElementEvent, SeriesOption } from 'echarts'; import { getNewTrendCursor, onDragUpdateTrendCursor } from '../utils/getTrendCursor'; -import { calculateXFromTimestamp } from '../utils/getInfo'; +import { calculateXFromTimestamp, convertViewportToMs } from '../utils/getInfo'; import { createRef } from 'react'; export const mockSeries = [ @@ -29,10 +29,12 @@ export const mockSeries = [ yAxisIndex: 0, }, ] as SeriesOption[]; + export const mockViewport = { start: new Date('2023-07-13T16:00:00.000Z'), end: new Date('2023-07-13T16:30:00.000Z'), }; +export const mockViewportInMs = convertViewportToMs(mockViewport); export const mockSize = { width: 500, height: 500 }; export const mockRef = createRef(); describe('Testing getNewTrendCursor file', () => { @@ -46,7 +48,7 @@ describe('Testing getNewTrendCursor file', () => { size: mockSize, tcHeaderColorIndex: 0, series: mockSeries, - viewport: mockViewport, + viewportInMs: mockViewportInMs, ref: mockRef, }); @@ -63,7 +65,7 @@ describe('Testing getNewTrendCursor file', () => { size: mockSize, tcHeaderColorIndex: 0, series: mockSeries, - viewport: mockViewport, + viewportInMs: mockViewportInMs, ref: mockRef, }); @@ -71,7 +73,7 @@ describe('Testing getNewTrendCursor file', () => { onDragUpdateTrendCursor({ graphic: newTrendCursor, - posX: calculateXFromTimestamp(timestamp, mockSize, mockViewport), + posX: calculateXFromTimestamp(timestamp, mockSize, mockViewportInMs), timeInMs: timestamp, series: mockSeries, size: mockSize, diff --git a/packages/react-components/src/components/chart/tests/handleResize.spec.tsx b/packages/react-components/src/components/chart/tests/handleResize.spec.tsx index a7bf5a90f..c090e444b 100644 --- a/packages/react-components/src/components/chart/tests/handleResize.spec.tsx +++ b/packages/react-components/src/components/chart/tests/handleResize.spec.tsx @@ -1,5 +1,5 @@ import { describe, expect } from '@jest/globals'; -import { mockRef, mockSeries, mockSize, mockViewport } from './getTrendCursor.spec'; +import { mockRef, mockSeries, mockSize, mockViewportInMs } from './getTrendCursor.spec'; import { renderHook } from '@testing-library/react'; import handleResize from '../utils/handleResize'; import { InternalGraphicComponentGroupOption } from '../types'; @@ -16,7 +16,7 @@ describe('handleResize', () => { ], series: mockSeries, setGraphic: setGraphicStub, - viewport: mockViewport, + viewportInMs: mockViewportInMs, size: mockSize, groupId: 'group1', ref: mockRef, diff --git a/packages/react-components/src/components/chart/tests/handleSync.spec.tsx b/packages/react-components/src/components/chart/tests/handleSync.spec.tsx index 6165702fb..f21529e34 100644 --- a/packages/react-components/src/components/chart/tests/handleSync.spec.tsx +++ b/packages/react-components/src/components/chart/tests/handleSync.spec.tsx @@ -1,6 +1,6 @@ import { describe, expect } from '@jest/globals'; import handleSync from '../utils/handleSync'; -import { mockSeries, mockSize, mockViewport } from './getTrendCursor.spec'; +import { mockSeries, mockSize, mockViewportInMs } from './getTrendCursor.spec'; import { createRef } from 'react'; import { renderHook } from '@testing-library/react'; import useDataStore from '../../../store'; @@ -13,7 +13,7 @@ describe('handleSync', () => { graphic: [], series: mockSeries, setGraphic: setGraphicStub, - viewport: mockViewport, + viewportInMs: mockViewportInMs, yMax: 30, yMin: 0, size: mockSize, diff --git a/packages/react-components/src/components/chart/tests/useTrendCursorsEvents.spec.tsx b/packages/react-components/src/components/chart/tests/useTrendCursorsEvents.spec.tsx index 3aa74055b..c4a0069df 100644 --- a/packages/react-components/src/components/chart/tests/useTrendCursorsEvents.spec.tsx +++ b/packages/react-components/src/components/chart/tests/useTrendCursorsEvents.spec.tsx @@ -1,7 +1,7 @@ import { describe } from '@jest/globals'; import { render, renderHook } from '@testing-library/react'; import useTrendCursorsEvents from '../hooks/useTrendCursorsEvents'; -import { mockSeries, mockSize, mockViewport } from './getTrendCursor.spec'; +import { mockSeries, mockSize, mockViewportInMs } from './getTrendCursor.spec'; import { useECharts } from '../../../hooks/useECharts'; import React from 'react'; import { InternalGraphicComponentGroupOption } from '../types'; @@ -20,7 +20,7 @@ describe('useTrendCursorsEvents', () => { setGraphic: mockSetGraphic, graphic: [], size: mockSize, - viewport: mockViewport, + viewportInMs: mockViewportInMs, series: mockSeries, isInSyncMode: false, onContextMenu: jest.fn(), @@ -39,7 +39,7 @@ describe('useTrendCursorsEvents', () => { setGraphic: mockSetGraphic, graphic: [], size: mockSize, - viewport: mockViewport, + viewportInMs: mockViewportInMs, series: mockSeries, isInSyncMode: false, onContextMenu: jest.fn(), @@ -59,7 +59,7 @@ describe('useTrendCursorsEvents', () => { setGraphic: mockSetGraphic, graphic: [{ timestampInMs: 1689264600000 } as InternalGraphicComponentGroupOption], size: mockSize, - viewport: mockViewport, + viewportInMs: mockViewportInMs, series: mockSeries, isInSyncMode: false, onContextMenu: jest.fn(), diff --git a/packages/react-components/src/components/chart/types.ts b/packages/react-components/src/components/chart/types.ts index 1915e3b76..6b324f641 100644 --- a/packages/react-components/src/components/chart/types.ts +++ b/packages/react-components/src/components/chart/types.ts @@ -93,17 +93,24 @@ export type ChartOptions = { fontSettings?: SimpleFontSettings; legend?: ChartLegend; significantDigits?: number; - graphic?: InternalGraphicComponentGroupOption[]; // This needs to be removed from the public api + graphic?: InternalGraphicComponentGroupOption[]; theme?: string; groupId?: string; id?: string; }; +export interface ViewportInMs { + end: number; + initial: number; + widthInMs: number; + isDurationViewport: boolean; +} + export interface TrendCursorProps { graphic: InternalGraphicComponentGroupOption[]; size: SizeConfig; setGraphic: Dispatch>; - viewport?: Viewport; + viewportInMs: ViewportInMs; series: SeriesOption[]; groupId?: string; } @@ -120,11 +127,15 @@ export interface UseSyncProps extends TrendCursorProps { isInSyncMode: boolean; } -export interface UseTrendCursorsProps extends TrendCursorProps { +export interface UseTrendCursorsProps { ref: React.RefObject; - isInCursorAddMode: boolean; chartId?: string; onContextMenu: (e: ElementEvent) => void; + initialGraphic?: InternalGraphicComponentGroupOption[]; + viewportInMs: ViewportInMs; + series: SeriesOption[]; + groupId?: string; + size: SizeConfig; } export interface GetNewTrendCursorProps { @@ -132,7 +143,7 @@ export interface GetNewTrendCursorProps { size: SizeConfig; tcHeaderColorIndex: number; series: SeriesOption[]; - viewport?: Viewport; + viewportInMs: ViewportInMs; tcId?: string; x?: number; timestamp?: number; diff --git a/packages/react-components/src/components/chart/utils/getInfo.ts b/packages/react-components/src/components/chart/utils/getInfo.ts index 06b216ec6..d57a1ecb5 100644 --- a/packages/react-components/src/components/chart/utils/getInfo.ts +++ b/packages/react-components/src/components/chart/utils/getInfo.ts @@ -1,7 +1,7 @@ import { DurationViewport, Viewport } from '@iot-app-kit/core/src'; import { DEFAULT_MARGIN, Y_AXIS_INTERPOLATED_VALUE_PRECISION } from '../eChartsConstants'; import { getInstanceByDom, LineSeriesOption, SeriesOption } from 'echarts'; -import { InternalGraphicComponentGroupOption, SizeConfig } from '../types'; +import { InternalGraphicComponentGroupOption, SizeConfig, ViewportInMs } from '../types'; import { parseDuration } from '../../../utils/time'; import React from 'react'; @@ -11,15 +11,19 @@ export const isDurationViewport = (viewport: Viewport): viewport is DurationView // TODO: test this once echarts live mode is supported // the width here represents the width of the view port in milli seconds // and initial is the start timestamp of the viewport -export const viewportToMs = (viewport?: Viewport) => { - if (viewport && isDurationViewport(viewport)) { - return { widthInMs: parseDuration(viewport.duration), initial: Date.now() - parseDuration(viewport.duration) }; +export const convertViewportToMs = (viewport?: Viewport) => { + const isDuration = !!viewport && isDurationViewport(viewport); + if (isDuration) { + const duration = parseDuration(viewport.duration); + return { widthInMs: duration, initial: Date.now() - duration, end: Date.now(), isDurationViewport: isDuration }; } else { const start = new Date(viewport?.start ?? 0).getTime(); const end = new Date(viewport?.end ?? 0).getTime(); return { widthInMs: end - start, initial: start, + end, + isDurationViewport: isDuration, }; } }; @@ -27,8 +31,8 @@ export const viewportToMs = (viewport?: Viewport) => { // this function calculated the timestamp of the location of the user click. // the timestamp is calculated based on the viewport and X value of the click point[x, y] // this is a simple linear interpolation -export const calculateTimeStamp = (xInPixel: number, widthInPixel: number, viewport?: Viewport) => { - const { widthInMs, initial } = viewportToMs(viewport); +export const calculateTimeStamp = (xInPixel: number, widthInPixel: number, viewportInMs: ViewportInMs) => { + const { widthInMs, initial } = viewportInMs; // TODO: this results in a decimal , needs to decide precision const delta = (widthInMs * (xInPixel - DEFAULT_MARGIN)) / (widthInPixel - 2 * DEFAULT_MARGIN); return delta + initial; @@ -133,8 +137,8 @@ export const roundUpYAxisMax = (yMax: number) => { }; // this calculated the new X in pixels when the chart is resized. -export const calculateXFromTimestamp = (timestampInMs: number, size: SizeConfig, viewport?: Viewport) => { - const { widthInMs, initial } = viewportToMs(viewport); +export const calculateXFromTimestamp = (timestampInMs: number, size: SizeConfig, viewportInMs: ViewportInMs) => { + const { widthInMs, initial } = viewportInMs; // TODO: precision should be updated here const x = ((size.width - 100) * (timestampInMs - initial)) / widthInMs; diff --git a/packages/react-components/src/components/chart/utils/getTrendCursor.ts b/packages/react-components/src/components/chart/utils/getTrendCursor.ts index 2e9e3b50c..dd83bb93b 100644 --- a/packages/react-components/src/components/chart/utils/getTrendCursor.ts +++ b/packages/react-components/src/components/chart/utils/getTrendCursor.ts @@ -201,7 +201,7 @@ export const getNewTrendCursor = ({ size, tcHeaderColorIndex, series, - viewport, + viewportInMs, tcId, x, timestamp, @@ -210,7 +210,7 @@ export const getNewTrendCursor = ({ const posX = e?.offsetX ?? x ?? 0; const uId = tcId ? tcId.split('trendCursor-')[1] : uuid(); // TODO: test this once echarts live mode is supported - const timestampInMs = timestamp ?? calculateTimeStamp(posX, size.width, viewport); + const timestampInMs = timestamp ?? calculateTimeStamp(posX, size.width, viewportInMs); const boundedX = setXWithBounds(size, posX); // TODO: test this once echarts live mode is supported @@ -242,5 +242,6 @@ export const getNewTrendCursor = ({ addTCDeleteButton(uId), ...addTCMarkers(uId, trendCursorsSeriesMakersInPixels, series), ], + transition: 'all' as const, }; }; diff --git a/packages/react-components/src/components/chart/utils/handleResize.ts b/packages/react-components/src/components/chart/utils/handleResize.ts index 52c4f97c5..6a90798d1 100644 --- a/packages/react-components/src/components/chart/utils/handleResize.ts +++ b/packages/react-components/src/components/chart/utils/handleResize.ts @@ -8,7 +8,7 @@ const handleResize = ({ series, graphic, setGraphic, - viewport, + viewportInMs, ref, }: TrendCursorProps & { ref: React.RefObject }) => { const prevSize = useRef(size); @@ -28,7 +28,7 @@ const handleResize = ({ // if width has changed, update X values if (size.width !== prevSize.current.width) { // updating x of the graphic - g.x = calculateXFromTimestamp(g.timestampInMs, size, viewport); + g.x = calculateXFromTimestamp(g.timestampInMs, size, viewportInMs); } return g; diff --git a/packages/react-components/src/components/chart/utils/handleSync.ts b/packages/react-components/src/components/chart/utils/handleSync.ts index 1befb9005..48b5f0ed7 100644 --- a/packages/react-components/src/components/chart/utils/handleSync.ts +++ b/packages/react-components/src/components/chart/utils/handleSync.ts @@ -5,7 +5,7 @@ import { calculateXFromTimestamp } from './getInfo'; import useDataStore from '../../../store'; import { UseSyncProps } from '../types'; -const handleSync = ({ ref, isInSyncMode, graphic, setGraphic, viewport, series, size, groupId }: UseSyncProps) => { +const handleSync = ({ ref, isInSyncMode, graphic, setGraphic, viewportInMs, series, size, groupId }: UseSyncProps) => { const syncedTrendCursors = useDataStore((state) => state.trendCursorGroups[groupId ?? '']); if (ref.current && isInSyncMode && syncedTrendCursors) { @@ -29,8 +29,8 @@ const handleSync = ({ ref, isInSyncMode, graphic, setGraphic, viewport, series, size, tcHeaderColorIndex: (syncedTrendCursors ?? {})[tcId].tcHeaderColorIndex, series, - viewport, - x: calculateXFromTimestamp(timestamp, size, viewport), + viewportInMs, + x: calculateXFromTimestamp(timestamp, size, viewportInMs), ref, }) ); @@ -42,7 +42,7 @@ const handleSync = ({ ref, isInSyncMode, graphic, setGraphic, viewport, series, toBeUpdated.forEach((updateTC) => { graphic[updateTC.index] = onDragUpdateTrendCursor({ graphic: graphic[updateTC.index], - posX: calculateXFromTimestamp(updateTC.newTimestamp, size, viewport), + posX: calculateXFromTimestamp(updateTC.newTimestamp, size, viewportInMs), timeInMs: updateTC.newTimestamp, size, series, diff --git a/packages/react-components/src/components/chart/utils/handleViewport.ts b/packages/react-components/src/components/chart/utils/handleViewport.ts new file mode 100644 index 000000000..16e2daef4 --- /dev/null +++ b/packages/react-components/src/components/chart/utils/handleViewport.ts @@ -0,0 +1,35 @@ +import { InternalGraphicComponentGroupOption, SizeConfig, ViewportInMs } from '../types'; +import { Dispatch, SetStateAction, useRef } from 'react'; +import { calculateXFromTimestamp } from './getInfo'; +import { DEFAULT_MARGIN } from '../eChartsConstants'; + +interface handleViewportProps { + graphic: InternalGraphicComponentGroupOption[]; + setGraphic: Dispatch>; + viewportInMs: ViewportInMs; + size: SizeConfig; +} +export const handleViewport = ({ graphic, setGraphic, size, viewportInMs }: handleViewportProps) => { + const xAxisViewportInMsMinRef = useRef(viewportInMs.initial); + const xAxisViewportInMsMaxRef = useRef(viewportInMs.end); + + if ( + viewportInMs.end !== xAxisViewportInMsMaxRef.current || + viewportInMs.initial !== xAxisViewportInMsMinRef.current + ) { + const newG = graphic.map((g) => { + const x = calculateXFromTimestamp(g.timestampInMs, size, viewportInMs); + if (x < DEFAULT_MARGIN) { + // hiding the TC + g.ignore = true; + } else { + g.x = x; + } + return g; + }); + + setGraphic(newG); + } + xAxisViewportInMsMinRef.current = viewportInMs.initial; + xAxisViewportInMsMaxRef.current = viewportInMs.end; +};