From f648831d68bffa4a16832bb72b5d1aa7058f3943 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Thu, 27 Jun 2024 06:54:22 -0400 Subject: [PATCH] feat: Optimize time series data format [PT-187867019] --- src/lara-app/utils/download-csv.test.ts | 20 +++++----- src/lara-app/utils/download-csv.ts | 20 ++++++---- src/mobile-app/components/sensor.tsx | 39 +++++++++++-------- src/sensors/device-sensor.ts | 6 +-- src/sensors/devices/device.ts | 6 +-- src/sensors/devices/gdx-sensor.ts | 19 +++------ src/sensors/mock-sensor.ts | 35 +++++++---------- src/sensors/sensor.ts | 16 +------- src/shared/components/data-table-field.tsx | 35 ++++++++++------- .../components/data-table-sparkgraph.tsx | 16 ++++---- src/shared/utils/time-series.ts | 23 +++++++++++ 11 files changed, 124 insertions(+), 111 deletions(-) create mode 100644 src/shared/utils/time-series.ts diff --git a/src/lara-app/utils/download-csv.test.ts b/src/lara-app/utils/download-csv.test.ts index 9e7e4e5..b5ae239 100644 --- a/src/lara-app/utils/download-csv.test.ts +++ b/src/lara-app/utils/download-csv.test.ts @@ -1,3 +1,4 @@ +import { TimeSeriesDataKey, TimeSeriesMetadataKey } from "../../shared/utils/time-series"; import { IExperiment, IExperimentData } from "../../shared/experiment-types"; import { downloadCSV, getFilename, getRows, getTimeKey, sortNaturally } from "./download-csv"; @@ -54,7 +55,7 @@ const timeSeriesExperiment: IExperiment = { experimentData: { items: { properties: { - timeSeries: { + [TimeSeriesDataKey]: { title: "Results" }, label: { @@ -68,7 +69,7 @@ const timeSeriesExperiment: IExperiment = { formUiSchema: { experimentData: { "ui:dataTableOptions": { - sensorFields: ["timeSeries"] + sensorFields: [TimeSeriesDataKey] } } } @@ -78,12 +79,11 @@ const timeSeriesExperiment: IExperiment = { const timeSeriesData: IExperimentData = { timestamp: 987654321, experimentData: [ - {timeSeries: [{time: 0, value: 1}, {time: 1, value: 2}], label: "Label #1"}, - {timeSeries: [{time: 1, value: 3}, {time: 2, value: 4}], label: "Label #2"} + {[TimeSeriesDataKey]: [1, 2], [TimeSeriesMetadataKey]: { measurementPeriod: 1000}, label: "Label #1"}, + {[TimeSeriesDataKey]: [3, 4], [TimeSeriesMetadataKey]: { measurementPeriod: 2000}, label: "Label #2"} ] }; - describe("download csv functions", () => { describe("getTimeKey()", () => { @@ -126,22 +126,22 @@ describe("download csv functions", () => { expect(getRows(timeSeriesExperiment, timeSeriesData)).toStrictEqual([ { "Time": "0", - "Row 1 Results": 1, - "Row 2 Results": "", + "Row 1 Results": "1", + "Row 2 Results": "3", "Row 1 Label": "Label #1", "Row 2 Label": "Label #2", }, { "Time": "1", - "Row 1 Results": 2, - "Row 2 Results": 3, + "Row 1 Results": "2", + "Row 2 Results": "", "Row 1 Label": "Label #1", "Row 2 Label": "Label #2", }, { "Time": "2", "Row 1 Results": "", - "Row 2 Results": 4, + "Row 2 Results": "4", "Row 1 Label": "Label #1", "Row 2 Label": "Label #2", }, diff --git a/src/lara-app/utils/download-csv.ts b/src/lara-app/utils/download-csv.ts index f259868..b0005c4 100644 --- a/src/lara-app/utils/download-csv.ts +++ b/src/lara-app/utils/download-csv.ts @@ -1,10 +1,11 @@ import {unparse} from "papaparse"; -import { IDataTableRow, IDataTableTimeData } from "../../shared/components/data-table-field"; +import { IDataTableRow } from "../../shared/components/data-table-field"; import { IExperiment, IExperimentData } from "../../shared/experiment-types"; import { isFunctionSymbol, handleSpecialValue } from "../../shared/utils/handle-special-value"; +import { ITimeSeriesMetadata, TimeSeriesDataKey, TimeSeriesMetadataKey } from "../../shared/utils/time-series"; -type TimeValues = Record; +type TimeValues = Record; type OtherValues = Record; type TimeSeriesRow = {timeValues: TimeValues, otherValues: OtherValues}; @@ -36,10 +37,10 @@ export const getRows = (experiment: IExperiment, data: IExperimentData) => { }, {}); const rawRows: IDataTableRow[] = data.experimentData; - const isTimeSeries = (experiment.schema.formUiSchema?.experimentData?.["ui:dataTableOptions"]?.sensorFields || []).indexOf("timeSeries") !== -1; + const isTimeSeries = (experiment.schema.formUiSchema?.experimentData?.["ui:dataTableOptions"]?.sensorFields || []).indexOf(TimeSeriesDataKey) !== -1; if (isTimeSeries) { - const nonTimeSeriesTitles = Object.keys(titleMap).filter(key => key !== "timeSeries"); + const nonTimeSeriesTitles = Object.keys(titleMap).filter(key => key !== TimeSeriesDataKey); const timeKeys = new Set(); const timeSeriesRows: TimeSeriesRow[] = []; @@ -51,10 +52,13 @@ export const getRows = (experiment: IExperiment, data: IExperimentData) => { otherValues[titleMap[key]] = getRowValue(key, rawRow, rawRows); }); - const timeSeries = (rawRow.timeSeries ?? []) as IDataTableTimeData[]; - timeSeries.forEach(({time, value}) => { + const timeSeries = (rawRow[TimeSeriesDataKey] ?? []) as number[]; + const {measurementPeriod} = (rawRow[TimeSeriesMetadataKey] ?? {measurementPeriod: 0}) as ITimeSeriesMetadata; + const timeDelta = measurementPeriod / 1000; + timeSeries.forEach((value, index) => { + const time = index * timeDelta; const timeKey = getTimeKey(time); - timeValues[timeKey] = value; + timeValues[timeKey] = String(value); timeKeys.add(timeKey); }); @@ -64,7 +68,7 @@ export const getRows = (experiment: IExperiment, data: IExperimentData) => { // unroll each time/value reading into its own row const unrolledRows: Record[] = []; - const timeSeriesKey = titleMap.timeSeries ?? "Time Series Value"; + const timeSeriesKey = titleMap[TimeSeriesDataKey] ?? "Time Series Value"; sortedTimeKeys.forEach(timeKey => { const row: Record = {Time: timeKey}; timeSeriesRows.forEach(({timeValues}, rowIndex) => { diff --git a/src/mobile-app/components/sensor.tsx b/src/mobile-app/components/sensor.tsx index f52bfab..c72bc38 100644 --- a/src/mobile-app/components/sensor.tsx +++ b/src/mobile-app/components/sensor.tsx @@ -1,13 +1,13 @@ import React, { useState, useRef, useMemo, useEffect } from "react"; import classNames from "classnames"; import { SensorValue } from "./sensor-value"; -import { Sensor, IConnectDevice, SelectDeviceFn, ITimeSeriesCapabilities, MaxNumberOfTimeSeriesValues, ISensorValues } from "../../sensors/sensor"; +import { Sensor, IConnectDevice, SelectDeviceFn, ISensorValues } from "../../sensors/sensor"; +import { ITimeSeriesCapabilities, ITimeSeriesMetadata, MaxNumberOfTimeSeriesValues, getTimeSeriesMetadata } from "../../shared/utils/time-series"; import { useSensor } from "../hooks/use-sensor"; import { MenuComponent, MenuItemComponent } from "../../shared/components/menu"; import { inCordova } from "../../shared/utils/in-cordova"; import { SensorStrength } from "./sensor-strength"; import { Icon } from "../../shared/components/icon"; -import { IDataTableTimeData } from "../../shared/components/data-table-field"; import DataTableSparkGraph from "../../shared/components/data-table-sparkgraph"; import css from "./sensor.module.scss"; @@ -80,29 +80,33 @@ export const SensorComponent: React.FC = ({sensor, manual const cancelSelectDevice = useRef(); // generate a sliding window of time series values - const latestValuesRef = useRef([]); + const timeSeriesValuesRef = useRef([]); useEffect(() => { if (!sensor.connected || !isTimeSeries || !timeSeriesCapabilities) { - latestValuesRef.current = []; + timeSeriesValuesRef.current = []; return; } const valueKey = timeSeriesCapabilities.valueKey as keyof ISensorValues; - const timeDelta = sensor.pollInterval / 1000; - const hasValues = latestValuesRef.current.length > 0; - let latestTime = hasValues ? latestValuesRef.current[latestValuesRef.current.length - 1].time : 0; - let nextTime = hasValues ? latestTime + timeDelta : 0; - if (latestTime >= maxTime) { - nextTime = maxTime; + const measurementPeriod = sensor.pollInterval / 1000; + let latestTime = timeSeriesValuesRef.current.length * measurementPeriod; + if (latestTime > maxTime) { latestTime = maxTime; - latestValuesRef.current = latestValuesRef.current.slice(1).map(({value}, index) => { - return {value, time: index * timeDelta}; - }); + timeSeriesValuesRef.current = timeSeriesValuesRef.current.slice(1); } - latestValuesRef.current.push({value: values[valueKey] ?? 0, time: nextTime}); - latestValuesRef.current[0].capabilities = timeSeriesCapabilities; + timeSeriesValuesRef.current.push(values[valueKey] ?? 0); }, [isTimeSeries, sensor, timeSeriesCapabilities, values]); + const timeSeriesMetadataRef = useRef(undefined); + useEffect(() => { + if (!isTimeSeries || !timeSeriesCapabilities) { + timeSeriesMetadataRef.current = undefined; + return; + } + + timeSeriesMetadataRef.current = {...getTimeSeriesMetadata(timeSeriesCapabilities), measurementPeriod: sensor.pollInterval}; + }, [isTimeSeries, timeSeriesCapabilities]); + const timeSeriesPeriods = useMemo(() => { if (!timeSeriesCapabilities) { return []; @@ -222,7 +226,7 @@ export const SensorComponent: React.FC = ({sensor, manual }; const renderTimeSeries = () => { - if (!timeSeriesCapabilities) { + if (!timeSeriesCapabilities || !timeSeriesMetadataRef.current) { return
; } @@ -240,7 +244,8 @@ export const SensorComponent: React.FC = ({sensor, manual void): () => void { + public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void { if (this.device) { return this.device.collectTimeSeries(measurementPeriod, selectableSensorId, callback); } diff --git a/src/sensors/devices/device.ts b/src/sensors/devices/device.ts index 1a867b9..92551cb 100644 --- a/src/sensors/devices/device.ts +++ b/src/sensors/devices/device.ts @@ -1,5 +1,5 @@ -import { IDataTableTimeData } from "../../shared/components/data-table-field"; -import { ISensorCapabilities, ISensorValues, SensorCapabilityKey, AllCapabilityKeys, ITimeSeriesCapabilities } from "../sensor"; +import { ISensorCapabilities, ISensorValues, SensorCapabilityKey, AllCapabilityKeys } from "../sensor"; +import { ITimeSeriesCapabilities } from "../../shared/utils/time-series"; export interface IDeviceOptions { name: string; @@ -51,7 +51,7 @@ export class Device { return []; // set in each device } - public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: IDataTableTimeData[]) => void): () => void { + public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void { throw new Error("collectTimeSeries() method not overridden!"); } diff --git a/src/sensors/devices/gdx-sensor.ts b/src/sensors/devices/gdx-sensor.ts index 59dcdfd..f060956 100644 --- a/src/sensors/devices/gdx-sensor.ts +++ b/src/sensors/devices/gdx-sensor.ts @@ -1,7 +1,7 @@ import { Device, ISelectableSensorInfo } from "./device"; import godirect from "@vernier/godirect/dist/godirect.min.cjs"; -import { ISensorCapabilities, ISensorValues, ITimeSeriesCapabilities } from "../sensor"; -import { IDataTableTimeData } from "../../shared/components/data-table-field"; +import { ISensorCapabilities, ISensorValues } from "../sensor"; +import { ITimeSeriesCapabilities } from "../../shared/utils/time-series"; const goDirectServiceUUID = "d91714ef-28b9-4f91-ba16-f0d9a604f112"; @@ -76,10 +76,8 @@ export class GDXSensorDevice extends Device { return this.gdxDevice.sensors.map((s: any) => ({name: s.name, internalId: s.number})); } - public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: IDataTableTimeData[]) => void): () => void { - let capabilities = this.timeSeriesCapabilities(selectableSensorId); - - if (!this.gdxDevice || !capabilities) { + public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void { + if (!this.gdxDevice) { return () => { // noop }; @@ -89,16 +87,11 @@ export class GDXSensorDevice extends Device { let time = 0; const delta = measurementPeriod / 1000; - const values: IDataTableTimeData[] = []; - capabilities = {...capabilities, measurementPeriod}; + const values: number[] = []; const handleChange = (sensor: any) => { if (sensor.number === selectableSensorId) { - if (values.length === 0) { - values.push({time, value: sensor.value, capabilities}); - } else { - values.push({time, value: sensor.value}); - } + values.push(sensor.value); callback(values); time += delta; } diff --git a/src/sensors/mock-sensor.ts b/src/sensors/mock-sensor.ts index d468fbf..830d46b 100644 --- a/src/sensors/mock-sensor.ts +++ b/src/sensors/mock-sensor.ts @@ -1,6 +1,7 @@ -import { IDataTableTimeData } from "../shared/components/data-table-field"; +import { TimeSeriesDataKey, getTimeSeriesMetadata } from "../shared/utils/time-series"; import { ISelectableSensorInfo } from "./devices/device"; -import { Sensor, ISensorOptions, ISensorValues, IPollOptions, IConnectOptions, ITimeSeriesCapabilities } from "./sensor"; +import { Sensor, ISensorOptions, ISensorValues, IPollOptions, IConnectOptions } from "./sensor"; +import { ITimeSeriesCapabilities } from "../shared/utils/time-series"; type MockValueDirection = "up" | "down"; interface IStartingSensorValues extends Required { @@ -46,19 +47,19 @@ export class MockSensor extends Sensor { illuminance: options.minValues?.illuminance || 0, temperature: options.minValues?.temperature || -18, humidity: options.minValues?.humidity || 0, - timeSeries: options.minValues?.timeSeries || -5, + timeSeries: options.minValues?.[TimeSeriesDataKey] || -5, }; this.maxMockValues = { illuminance: options.maxValues?.illuminance || 10000, temperature: options.maxValues?.temperature || 38, humidity: options.maxValues?.humidity || 90, - timeSeries: options.maxValues?.timeSeries || 5, + timeSeries: options.maxValues?.[TimeSeriesDataKey] || 5, }; this.mockValues = { illuminance: this.randomInRange("illuminance"), temperature: this.randomInRange("temperature"), humidity: this.randomInRange("humidity"), - timeSeries: this.randomInRange("timeSeries"), + timeSeries: this.randomInRange(TimeSeriesDataKey), ...options.startingValues, }; this.mockValueDirections = { @@ -127,8 +128,8 @@ export class MockSensor extends Sensor { this.setNextRandomMockValue({measurement: "temperature", increment: 0.2}); } - values.timeSeries = this.mockValues.timeSeries; - this.setNextRandomMockValue({measurement: "timeSeries", increment: 0.2}); + values[TimeSeriesDataKey] = this.mockValues[TimeSeriesDataKey]; + this.setNextRandomMockValue({measurement: TimeSeriesDataKey, increment: 0.2}); return Promise.resolve(values); } @@ -140,7 +141,7 @@ export class MockSensor extends Sensor { minMeasurementPeriod: 10, defaultMeasurementPeriod, measurement: "Fake Value", - valueKey: "timeSeries", + valueKey: TimeSeriesDataKey, units: "N/A", minValue: -5, maxValue: 5, @@ -154,22 +155,14 @@ export class MockSensor extends Sensor { ]; } - public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: IDataTableTimeData[]) => void): () => void { - let time = 0; - const delta = measurementPeriod / 1000; - const values: IDataTableTimeData[] = []; - const capabilities = {...this.timeSeriesCapabilities(selectableSensorId), measurementPeriod}; + public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void { + const values: number[] = []; const callCallback = () => { - const value = this.mockValues.timeSeries; - this.setNextRandomMockValue({measurement: "timeSeries", increment: 0.5}); - if (values.length === 0) { - values.push({time, value, capabilities}); - } else { - values.push({time, value}); - } + const value = this.mockValues[TimeSeriesDataKey]; + this.setNextRandomMockValue({measurement: TimeSeriesDataKey, increment: 0.5}); + values.push(value); callback(values); - time += delta; }; callCallback(); diff --git a/src/sensors/sensor.ts b/src/sensors/sensor.ts index 36b4edb..dec5f7e 100644 --- a/src/sensors/sensor.ts +++ b/src/sensors/sensor.ts @@ -1,6 +1,6 @@ import EventEmitter from "eventemitter3"; -import { IDataTableTimeData } from "../shared/components/data-table-field"; import { ISelectableSensorInfo } from "./devices/device"; +import { ITimeSeriesCapabilities } from "../shared/utils/time-series"; export interface ISensorCapabilities { illuminance?: boolean; @@ -79,18 +79,6 @@ export interface IConnectOptions { onDevicesFound: OnDevicesFoundFn; } -export interface ITimeSeriesCapabilities { - measurementPeriod: number; - minMeasurementPeriod: number; - defaultMeasurementPeriod: number; - measurement: string; - valueKey: string; - units: string; - minValue: number; - maxValue: number; -} - -export const MaxNumberOfTimeSeriesValues = 1000; export class Sensor extends EventEmitter { protected _deviceName: string | undefined; @@ -164,7 +152,7 @@ export class Sensor extends EventEmitter { } } - public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: IDataTableTimeData[]) => void): () => void { + public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void { throw new Error("collectTimeSeries() method not overridden!"); } diff --git a/src/shared/components/data-table-field.tsx b/src/shared/components/data-table-field.tsx index 64dc148..25bec6f 100644 --- a/src/shared/components/data-table-field.tsx +++ b/src/shared/components/data-table-field.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef, useMemo } from "react"; import classNames from "classnames"; import { FieldProps } from "react-jsonschema-form"; import { MockSensor } from "../../sensors/mock-sensor"; -import { ISensorCapabilities, ISensorConnectionEventData, ITimeSeriesCapabilities, MaxNumberOfTimeSeriesValues, SensorCapabilityKey, SensorEvent } from "../../sensors/sensor"; +import { ISensorCapabilities, ISensorConnectionEventData, SensorCapabilityKey, SensorEvent } from "../../sensors/sensor"; +import { ITimeSeriesCapabilities, ITimeSeriesMetadata, MaxNumberOfTimeSeriesValues, TimeSeriesMetadataKey, getTimeSeriesMetadata } from "../utils/time-series"; import { SensorComponent } from "../../mobile-app/components/sensor"; import { IVortexFormContext } from "./form"; import { getURLParam } from "../utils/get-url-param"; @@ -15,6 +16,7 @@ import { tableKeyboardNav } from "../utils/table-keyboard-nav"; import { confirm, alert } from "../utils/dialogs"; import { handleSpecialValue, isFunctionSymbol } from "../utils/handle-special-value"; import DataTableSparkGraph from "./data-table-sparkgraph"; +import { TimeSeriesDataKey } from "../utils/time-series"; import css from "./data-table-field.module.scss"; @@ -58,8 +60,7 @@ interface IDataTableDataSchema { }; } -export type IDataTableTimeData = {time: number, value: number, capabilities?: ITimeSeriesCapabilities}; -export type IDataTableRowData = string | number | IDataTableTimeData[] | undefined; +export type IDataTableRowData = string | number | number[] | ITimeSeriesMetadata | undefined; // Form data accepted by this component. export interface IDataTableRow { @@ -128,7 +129,7 @@ const castToExpectedTypes = (fieldDefinition: {[propName: string]: IDataTableFie newData.push(newRow); Object.keys(row).forEach(propName => { const rawValue = formData[rowIdx][propName]; - newRow[propName] = fieldDefinition[propName].type === "number" && !isNaN(Number(rawValue)) ? Number(rawValue) : rawValue; + newRow[propName] = fieldDefinition[propName]?.type === "number" && !isNaN(Number(rawValue)) ? Number(rawValue) : rawValue; }); }); return newData; @@ -166,7 +167,7 @@ export const DataTableField: React.FC = props => { const uiSchema: IFormUiSchema = props.uiSchema as IFormUiSchema; const experimentFilters = uiSchema["ui:dataTableOptions"]?.filters || []; const sensorFields = uiSchema["ui:dataTableOptions"]?.sensorFields || []; - const isTimeSeries = sensorFields.indexOf("timeSeries") !== -1; + const isTimeSeries = sensorFields.indexOf(TimeSeriesDataKey) !== -1; // Sensor instance can be provided in form context or it'll be created using filters or sensorFields as capabilities. const sensor = formContext.experimentConfig?.useSensors && sensorFields.length > 0 ? (formContext.sensor || getSensor(sensorFields, experimentFilters)) : null; const sensorOutput = useSensor(sensor); @@ -182,8 +183,10 @@ export const DataTableField: React.FC = props => { const stopTimeSeriesFnRef = useRef<(() => void)|undefined>(undefined); const timeSeriesRecordingRowRef = useRef(undefined); const maxTime = useMemo(() => { - const result = isTimeSeries ? formData.reduce((acc, {timeSeries}) => { - return Math.max(acc, Array.isArray(timeSeries) ? timeSeries[timeSeries.length - 1].time : 0); + const result = isTimeSeries ? formData.reduce((acc, row) => { + const timeSeries = row[TimeSeriesDataKey]; + const {measurementPeriod} = (row[TimeSeriesMetadataKey] ?? {measurementPeriod: 0}) as ITimeSeriesMetadata; + return Math.max(acc, Array.isArray(timeSeries) ? (measurementPeriod / 1000) * timeSeries.length : 0); }, 0) : 0; return result; }, [isTimeSeries, formData]); @@ -245,7 +248,7 @@ export const DataTableField: React.FC = props => { } const propType = fieldDefinition[propName].type; - if (propName === "timeSeries") { + if (propName === TimeSeriesDataKey) { // no validation on time series data } else if ((propType === "number") || (propType === "integer")) { const {minimum, maximum} = fieldDefinition[propName] as IDataTableNumberInputField; @@ -369,9 +372,11 @@ export const DataTableField: React.FC = props => { setInputDisabled?.(true); + const timeSeriesMetadata = getTimeSeriesMetadata(timeSeriesCapabilities); + stopTimeSeriesFnRef.current = sensor.collectTimeSeries(timeSeriesCapabilities.measurementPeriod, selectableSensorId, (values) => { const newData = formData.slice(); - newData[rowIdx] = {timeSeries: values}; + newData[rowIdx] = {timeSeries: values, timeSeriesMetadata}; if (values.length <= MaxNumberOfTimeSeriesValues) { setFormData(newData); } @@ -514,9 +519,12 @@ export const DataTableField: React.FC = props => { }); let contents; - if (name === "timeSeries") { - const values: IDataTableTimeData[] = value || []; - const graphTitle = values.length > 0 ? `${Math.round(values[values.length - 1].time)} sec` : ""; + if (name === TimeSeriesDataKey) { + const values: number[] = value || []; + const metadata = (row[TimeSeriesMetadataKey] ?? {measurementPeriod: 0}); + const {measurementPeriod} = metadata; + const time = values.length * (measurementPeriod / 1000); + const graphTitle = values.length > 0 ? `${Math.round(time)} sec` : ""; contents =
@@ -524,7 +532,8 @@ export const DataTableField: React.FC = props => {
; diff --git a/src/shared/components/data-table-sparkgraph.tsx b/src/shared/components/data-table-sparkgraph.tsx index a67a346..13beb80 100644 --- a/src/shared/components/data-table-sparkgraph.tsx +++ b/src/shared/components/data-table-sparkgraph.tsx @@ -1,7 +1,7 @@ import React, { useRef } from "react"; -import { IDataTableTimeData } from "./data-table-field"; import css from "./data-table-sparkgraph.module.scss"; +import { ITimeSeriesMetadata } from "../utils/time-series"; const leaderRadius = 3; const leaderStokeWidth = 1.5; @@ -15,14 +15,15 @@ interface ISparkGraphPoint { interface IProps { width: number; height: number; - values: IDataTableTimeData[]; + values: number[]; + metadata: ITimeSeriesMetadata; maxTime: number; minTime?: number; showAxes?: boolean; redrawSignal?: number; } -export default function DataTableSparkGraph({width, height, values, minTime, maxTime, showAxes, redrawSignal}: IProps) { +export default function DataTableSparkGraph({width, height, values, metadata, minTime, maxTime, showAxes, redrawSignal}: IProps) { const innerWidth = width - (leaderMargin * 2); const innerHeight = height - (leaderMargin * 2); const innerLeft = leaderMargin; @@ -45,16 +46,15 @@ export default function DataTableSparkGraph({width, height, values, minTime, max || (maxTime !== lastMaxTimeRef.current) || (redrawSignal !== lastRedrawSignalRef.current); if (graphChanged) { - const capabilities = values[0]?.capabilities; - - const minValue = capabilities?.minValue ?? values.reduce((acc, cur) => Math.min(cur.value, acc), Infinity); - const maxValue = capabilities?.maxValue ?? values.reduce((acc, cur) => Math.max(cur.value, acc), -Infinity); + const minValue = metadata.minValue ?? values.reduce((acc, cur) => Math.min(cur, acc), Infinity); + const maxValue = metadata.maxValue ?? values.reduce((acc, cur) => Math.max(cur, acc), -Infinity); let x: number = 0; let y: number = 0; - polyLinePoints = values.map(({value, time}) => { + polyLinePoints = values.map((value, index) => { value = Math.max(minValue, Math.min(value, maxValue)); + const time = index * (metadata.measurementPeriod / 1000); x = innerLeft + (innerWidth * (time / Math.max(maxTime, (minTime ?? 10)))); y = innerLeft + (innerHeight - (((value - minValue) / (maxValue - minValue)) * innerHeight)); return `${x},${y}`; diff --git a/src/shared/utils/time-series.ts b/src/shared/utils/time-series.ts new file mode 100644 index 0000000..024d3fc --- /dev/null +++ b/src/shared/utils/time-series.ts @@ -0,0 +1,23 @@ +export interface ITimeSeriesMetadata { + measurement: string; + measurementPeriod: number; + units: string; + minValue: number; + maxValue: number; +} + +export interface ITimeSeriesCapabilities extends ITimeSeriesMetadata { + minMeasurementPeriod: number; + defaultMeasurementPeriod: number; + valueKey: string; +} + +export const TimeSeriesDataKey = "timeSeries"; +export const TimeSeriesMetadataKey = "timeSeriesMetadata"; +export const MaxNumberOfTimeSeriesValues = 1000; + +export const getTimeSeriesMetadata = (capabilities: ITimeSeriesCapabilities): ITimeSeriesMetadata => { + const {measurement, measurementPeriod, units, minValue, maxValue} = capabilities; + return {measurement, measurementPeriod, units, minValue, maxValue}; +}; +