Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Optimize time series data format [PT-187867019] #153

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/lara-app/utils/download-csv.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -54,7 +55,7 @@ const timeSeriesExperiment: IExperiment = {
experimentData: {
items: {
properties: {
timeSeries: {
[TimeSeriesDataKey]: {
title: "Results"
},
label: {
Expand All @@ -68,7 +69,7 @@ const timeSeriesExperiment: IExperiment = {
formUiSchema: {
experimentData: {
"ui:dataTableOptions": {
sensorFields: ["timeSeries"]
sensorFields: [TimeSeriesDataKey]
}
}
}
Expand All @@ -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()", () => {
Expand Down Expand Up @@ -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",
},
Expand Down
20 changes: 12 additions & 8 deletions src/lara-app/utils/download-csv.ts
Original file line number Diff line number Diff line change
@@ -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<string, number|undefined>;
type TimeValues = Record<string, string|undefined>;
type OtherValues = Record<string, any>;
type TimeSeriesRow = {timeValues: TimeValues, otherValues: OtherValues};

Expand Down Expand Up @@ -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<string>();
const timeSeriesRows: TimeSeriesRow[] = [];

Expand All @@ -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);
});

Expand All @@ -64,7 +68,7 @@ export const getRows = (experiment: IExperiment, data: IExperimentData) => {

// unroll each time/value reading into its own row
const unrolledRows: Record<string,any>[] = [];
const timeSeriesKey = titleMap.timeSeries ?? "Time Series Value";
const timeSeriesKey = titleMap[TimeSeriesDataKey] ?? "Time Series Value";
sortedTimeKeys.forEach(timeKey => {
const row: Record<string, any> = {Time: timeKey};
timeSeriesRows.forEach(({timeValues}, rowIndex) => {
Expand Down
39 changes: 22 additions & 17 deletions src/mobile-app/components/sensor.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -80,29 +80,33 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
const cancelSelectDevice = useRef<CancelDeviceFn|undefined>();

// generate a sliding window of time series values
const latestValuesRef = useRef<IDataTableTimeData[]>([]);
const timeSeriesValuesRef = useRef<number[]>([]);
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<ITimeSeriesMetadata|undefined>(undefined);
useEffect(() => {
if (!isTimeSeries || !timeSeriesCapabilities) {
timeSeriesMetadataRef.current = undefined;
return;
}

timeSeriesMetadataRef.current = {...getTimeSeriesMetadata(timeSeriesCapabilities), measurementPeriod: sensor.pollInterval};
}, [isTimeSeries, timeSeriesCapabilities]);

const timeSeriesPeriods = useMemo(() => {
if (!timeSeriesCapabilities) {
return [];
Expand Down Expand Up @@ -222,7 +226,7 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
};

const renderTimeSeries = () => {
if (!timeSeriesCapabilities) {
if (!timeSeriesCapabilities || !timeSeriesMetadataRef.current) {
return <div className={css.timeSeriesValue} />;
}

Expand All @@ -240,7 +244,8 @@ export const SensorComponent: React.FC<ISensorComponentProps> = ({sensor, manual
<DataTableSparkGraph
width={25}
height={50}
values={latestValuesRef.current}
values={timeSeriesValuesRef.current}
metadata={timeSeriesMetadataRef.current}
minTime={0}
maxTime={maxTime}
showAxes={true}
Expand Down
6 changes: 2 additions & 4 deletions src/sensors/device-sensor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Sensor, ISensorValues, ISensorOptions, ISensorCapabilities, ISetConnectedOptions, IPollOptions, IConnectOptions, ITimeSeriesCapabilities } from "./sensor";

import { Sensor, ISensorValues, ISensorOptions, ISetConnectedOptions, IPollOptions, IConnectOptions } from "./sensor";
import { Device, ISelectableSensorInfo } from "./devices/device";
import { SensorTag2Device } from "./devices/sensor-tag-cc2650";
import { SensorTagCC1350Device } from "./devices/sensor-tag-cc1350";
import { MultiSensorDevice } from "./devices/multi-sensor";
import { GDXSensorDevice } from "./devices/gdx-sensor";
import { logInfo } from "../shared/utils/log";
import { inCordova } from "../shared/utils/in-cordova";
import { IDataTableTimeData } from "../shared/components/data-table-field";

declare global {
// tslint:disable-next-line:interface-name
Expand Down Expand Up @@ -123,7 +121,7 @@ export class DeviceSensor extends Sensor {
});
}

public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, selectableSensorId: any, callback: (values: number[]) => void): () => void {
if (this.device) {
return this.device.collectTimeSeries(measurementPeriod, selectableSensorId, callback);
}
Expand Down
6 changes: 3 additions & 3 deletions src/sensors/devices/device.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!");
}

Expand Down
19 changes: 6 additions & 13 deletions src/sensors/devices/gdx-sensor.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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
};
Expand All @@ -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;
}
Expand Down
35 changes: 14 additions & 21 deletions src/sensors/mock-sensor.ts
Original file line number Diff line number Diff line change
@@ -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<ISensorValues> {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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,
Expand All @@ -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();
Expand Down
Loading
Loading