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: Added time series data display [PT-186741112] #141

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
4 changes: 2 additions & 2 deletions src/sensors/device-sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ export class DeviceSensor extends Sensor {
});
}

public collectTimeSeries(timeSeriesCapabilities: ITimeSeriesCapabilities, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, callback: (values: IDataTableTimeData[]) => void): () => void {
if (this.device) {
return this.device.collectTimeSeries(timeSeriesCapabilities, callback);
return this.device.collectTimeSeries(measurementPeriod, callback);
}
return () => {
// noop
Expand Down
2 changes: 1 addition & 1 deletion src/sensors/devices/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class Device {
return undefined; // set in each device
}

public collectTimeSeries(options: ITimeSeriesCapabilities, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, callback: (values: IDataTableTimeData[]) => void): () => void {
throw new Error("collectTimeSeries() method not overridden!");
}

Expand Down
23 changes: 13 additions & 10 deletions src/sensors/devices/gdx-sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ISensorCapabilities, ISensorValues, ITimeSeriesCapabilities } from "../
import { IDataTableTimeData } from "../../shared/components/data-table-field";

const goDirectServiceUUID = "d91714ef-28b9-4f91-ba16-f0d9a604f112";
const measurementPeriod = 100;
const defaultMeasurementPeriod = 100;

// NOTE: to add a new sensor using the existing global capabilities add the prefix to the array
// and update the mapping of the sensor name to the capability below. More work will be
Expand Down Expand Up @@ -44,28 +44,31 @@ export class GDXSensorDevice extends Device {
return this._timeSeriesCapabilities;
}

public collectTimeSeries(timeSeriesCapabilities: ITimeSeriesCapabilities, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, callback: (values: IDataTableTimeData[]) => void): () => void {
const sensor = this.gdxDevice?.sensors.find((s: any) => s.enabled);

if (!this.gdxDevice || !sensor) {
if (!this.gdxDevice || !sensor || !this.timeSeriesCapabilities) {
return () => {
// noop
};
}

let time = 0;
const delta = timeSeriesCapabilities.measurementPeriod / 1000;
const delta = measurementPeriod / 1000;
const values: IDataTableTimeData[] = [];

this.gdxDevice.stop();
const capabilities = {...this.timeSeriesCapabilities, measurementPeriod};

const handleChange = () => {
values.push({time, value: sensor.value});
if (values.length === 0) {
values.push({time, value: sensor.value, capabilities});
} else {
values.push({time, value: sensor.value});
}
callback(values);
time += delta;
};
sensor.on("value-changed", handleChange);
this.gdxDevice.start(timeSeriesCapabilities.measurementPeriod);
this.gdxDevice.start(measurementPeriod);

return () => {
sensor.off("value-changed", handleChange);
Expand Down Expand Up @@ -99,7 +102,7 @@ export class GDXSensorDevice extends Device {
const measurement: string = firstSensor.name;
const valueKey = measurement.toLowerCase();
this._timeSeriesCapabilities = {
measurementPeriod,
measurementPeriod: defaultMeasurementPeriod,
measurement,
valueKey,
units: firstSensor.unit,
Expand All @@ -111,7 +114,7 @@ export class GDXSensorDevice extends Device {
}

this.gdxDevice = gdxDevice;
this.gdxDevice.start(measurementPeriod);
this.gdxDevice.start(defaultMeasurementPeriod);

resolve();
}).catch(reject);
Expand Down
21 changes: 13 additions & 8 deletions src/sensors/mock-sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ export class MockSensor extends Sensor {
illuminance: options.minValues?.illuminance || 0,
temperature: options.minValues?.temperature || -18,
humidity: options.minValues?.humidity || 0,
timeSeries: options.minValues?.timeSeries || -50,
timeSeries: options.minValues?.timeSeries || -5,
};
this.maxMockValues = {
illuminance: options.maxValues?.illuminance || 10000,
temperature: options.maxValues?.temperature || 38,
humidity: options.maxValues?.humidity || 90,
timeSeries: options.maxValues?.timeSeries || 50,
timeSeries: options.maxValues?.timeSeries || 5,
};
this.mockValues = {
illuminance: this.randomInRange("illuminance"),
Expand Down Expand Up @@ -132,26 +132,31 @@ export class MockSensor extends Sensor {
return Promise.resolve(values);
}

public get timeSeriesCapabilities(): ITimeSeriesCapabilities|undefined {
public get timeSeriesCapabilities(): ITimeSeriesCapabilities {
return {
measurementPeriod: 100,
measurement: "Fake Value",
valueKey: "timeSeries",
units: "N/A",
minValue: -50,
maxValue: 50,
minValue: -5,
maxValue: 5,
};
}

public collectTimeSeries({measurementPeriod}: ITimeSeriesCapabilities, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, callback: (values: IDataTableTimeData[]) => void): () => void {
let time = 0;
const delta = measurementPeriod / 1000;
const values: IDataTableTimeData[] = [];
const capabilities = {...this.timeSeriesCapabilities, measurementPeriod};

const callCallback = () => {
const value = this.mockValues.timeSeries;
this.setNextRandomMockValue({measurement: "timeSeries", increment: 0.2});
values.push({time, value});
this.setNextRandomMockValue({measurement: "timeSeries", increment: 0.5});
if (values.length === 0) {
values.push({time, value, capabilities});
} else {
values.push({time, value});
}
callback(values);
time += delta;
};
Expand Down
2 changes: 1 addition & 1 deletion src/sensors/sensor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class Sensor extends EventEmitter<SensorEvent> {
}
}

public collectTimeSeries(options: ITimeSeriesCapabilities, callback: (values: IDataTableTimeData[]) => void): () => void {
public collectTimeSeries(measurementPeriod: number, callback: (values: IDataTableTimeData[]) => void): () => void {
throw new Error("collectTimeSeries() method not overridden!");
}

Expand Down
60 changes: 33 additions & 27 deletions src/shared/components/data-table-field.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React, { useState, useEffect, useRef, useMemo } from "react";
import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { FieldProps } from "react-jsonschema-form";
import { MockSensor } from "../../sensors/mock-sensor";
import { ISensorCapabilities, ISensorConnectionEventData, ITimeSeriesCapabilities, SensorCapabilityKey, SensorEvent } from "../../sensors/sensor";
import { ISensorCapabilities, ISensorConnectionEventData, ITimeSeriesCapabilities, MaxNumberOfTimeSeriesValues, SensorCapabilityKey, SensorEvent } from "../../sensors/sensor";
import { SensorComponent } from "../../mobile-app/components/sensor";
import { IVortexFormContext } from "./form";
import { getURLParam } from "../utils/get-url-param";
import { DeviceSensor } from "../../sensors/device-sensor";
import { JSONSchema7 } from "json-schema";
import { IFormUiSchema } from "../experiment-types";
import { Icon } from "./icon";
import { Icon, IconName } from "./icon";
import { useSensor } from "../../mobile-app/hooks/use-sensor";
import { tableKeyboardNav } from "../utils/table-keyboard-nav";
import { confirm, alert } from "../utils/dialogs";
import { handleSpecialValue, isFunctionSymbol } from "../utils/handle-special-value";
import css from "./data-table-field.module.scss";
import DataTableSparkGraph from "./data-table-sparkgraph";

const defPrecision = 2;

Expand Down Expand Up @@ -52,7 +53,7 @@ interface IDataTableDataSchema {
};
}

export type IDataTableTimeData = {time: number, value: string|number};
export type IDataTableTimeData = {time: number, value: number, capabilities?: ITimeSeriesCapabilities};
export type IDataTableRowData = string | number | IDataTableTimeData[] | undefined;

// Form data accepted by this component.
Expand Down Expand Up @@ -175,6 +176,12 @@ export const DataTableField: React.FC<FieldProps> = props => {
const waitForSensorIntervalRef = useRef(0);
const stopTimeSeriesFnRef = useRef<(() => void)|undefined>(undefined);
const timeSeriesRecordingRowRef = useRef<number|undefined>(undefined);
const maxNumTimeSeriesValues = useMemo(() => {
const result = isTimeSeries ? formData.reduce<number>((acc, {timeSeries}) => {
return Math.max(acc, Array.isArray(timeSeries) ? timeSeries.length : 0);
}, 0) : 0;
return result;
}, [isTimeSeries, formData]);

// listen for prop changes from uploads
useEffect(() => {
Expand Down Expand Up @@ -326,14 +333,19 @@ export const DataTableField: React.FC<FieldProps> = props => {
};

const recordTimeSeries = () => {
if (!sensor || !timeSeriesCapabilities) {
if (!sensor) {
return;
}

stopTimeSeriesFnRef.current = sensor.collectTimeSeries(timeSeriesCapabilities, (values) => {
stopTimeSeriesFnRef.current = sensor.collectTimeSeries(100, (values) => {
const newData = formData.slice();
newData[rowIdx] = {timeSeries: values};
setFormData(newData);
if (values.length <= MaxNumberOfTimeSeriesValues) {
setFormData(newData);
}
if (values.length === MaxNumberOfTimeSeriesValues) {
onSensorStopTimeSeries(newData);
}
});
timeSeriesRecordingRowRef.current = rowIdx;
};
Expand All @@ -353,11 +365,13 @@ export const DataTableField: React.FC<FieldProps> = props => {
}
};

const onSensorStopTimeSeries = () => {
stopTimeSeriesFnRef.current?.();
stopTimeSeriesFnRef.current = undefined;
timeSeriesRecordingRowRef.current = undefined;
saveData(formData);
const onSensorStopTimeSeries = (finalData: IDataTableRow[]) => {
if (stopTimeSeriesFnRef.current) {
stopTimeSeriesFnRef.current?.();
stopTimeSeriesFnRef.current = undefined;
timeSeriesRecordingRowRef.current = undefined;
saveData(finalData);
}
};

const renderRow = (row: { [k: string]: any }, rowIdx: number) => {
Expand All @@ -375,8 +389,9 @@ export const DataTableField: React.FC<FieldProps> = props => {
});
const recordingTimeSeries = stopTimeSeriesFnRef.current !== undefined;
const rowActive = recordingTimeSeries ? (sensorCanRecord && timeSeriesRecordingRowRef.current === rowIdx) : sensorCanRecord;
const iconName = sensorFieldsBlank ? "record" : "replay";
const onClick = rowActive ? (recordingTimeSeries ? onSensorStopTimeSeries : onSensorRecordClick.bind(null, rowIdx)) : null;
const showStopButton = recordingTimeSeries && timeSeriesRecordingRowRef.current === rowIdx;
const iconName: IconName = sensorFieldsBlank ? (isTimeSeries ? "recordDataTrial" : "record") : (isTimeSeries ? (showStopButton ? "stopDataTrial" : "reRecordDataTrial") : "replay");
const onClick = rowActive ? (recordingTimeSeries ? onSensorStopTimeSeries.bind(null, formData) : onSensorRecordClick.bind(null, rowIdx)) : null;
const refreshBtnCell = <td key="refreshBtn" className={`${css.refreshSensorReadingColumn} ${css.readOnly}`}>
{
anyNonFunctionSensorValues &&
Expand Down Expand Up @@ -421,15 +436,6 @@ export const DataTableField: React.FC<FieldProps> = props => {
);
};

const renderTimeSeries = (values: IDataTableTimeData[]) => {
if (values.length === 0) {
return null;
}

const lastValue = values[values.length - 1];
return <div>{lastValue.value} ({values.length})</div>;
};

const renderBasicCells = (fieldNames: string[], row: { [k: string]: any }, rowIdx: number) => {
let sensorFieldsBlank = true;
sensorFields.forEach((name: string) => {
Expand Down Expand Up @@ -464,11 +470,11 @@ export const DataTableField: React.FC<FieldProps> = props => {
}

let contents;
if (readOnly) {
if (name === "timeSeries") {
contents = <DataTableSparkGraph values={value || []} maxNumTimeSeriesValues={maxNumTimeSeriesValues} />;
} else if (readOnly) {
contents = <div className={css.valueCell}>{value}</div>;
} else if (name === "timeSeries") {
contents = renderTimeSeries(value);
}else if (fieldDefinition[name].type === "array") {
} else if (fieldDefinition[name].type === "array") {
contents = renderSelect({name, value, rowIdx, items: (fieldDefinition[name] as IDataTableArrayField).items});
} else {
const input = renderInput({ name, value, rowIdx, disabled: isFunction || (isSensorField && !manualEntryMode), error });
Expand Down
35 changes: 35 additions & 0 deletions src/shared/components/data-table-sparkgraph.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@import "../../shared/components/variables.scss";

.dataTableSparkgraph {
padding: 6px 6px 1.5px 6px; // bottom padding is handled in svg height margin
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;

div {
font-size: 13px;
font-weight: normal;
color: $textBlack;
}

svg {
margin-top: -6px;

polyline {
stroke: $timeSeriesStroke;
stroke-width: 1.5px;
fill: none;
}

polygon {
stroke: none;
fill: $timeSeriesFill;
}

circle {
stroke: $timeSeriesStroke;
fill: $timeSeriesLeaderFill;
}
}
}
Loading
Loading