From e4749199f871247ba1d70985c726806008ed6d4d Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Wed, 19 Jun 2024 05:34:13 -0400 Subject: [PATCH] feat: Update styling [PT-187736835] - Moved Data Trial experiment to the first experiment in the list - Changed experiment colors so that the icon colors are specified in the experiment and not in the code - Added permanent border around experiment name in header - Update time series sensor header to use svg sparkgraph - Added input borders in tables - Added (optional) placeholders with simple variable substitution for inputs --- cypress/supports/elements/ExperimentSetup.js | 6 +- .../utils/experiment-authoring-schema.json | 20 ++ src/data/experiments.json | 257 +++++++++--------- .../experiment-picker-item.module.scss | 19 +- .../components/experiment-picker-item.tsx | 16 +- .../components/experiment-wrapper.module.scss | 43 +-- .../components/experiment-wrapper.tsx | 17 +- src/mobile-app/components/run-picker.tsx | 2 +- src/mobile-app/components/sensor.module.scss | 45 +-- src/mobile-app/components/sensor.tsx | 41 ++- src/mobile-app/components/uploader.tsx | 2 +- src/mobile-app/hooks/use-experiments.test.ts | 4 + src/mobile-app/hooks/use-icon-style.ts | 25 ++ src/shared/components/data-table-barchart.tsx | 50 ---- .../components/data-table-field.module.scss | 32 ++- src/shared/components/data-table-field.tsx | 28 +- .../data-table-sparkgraph.module.scss | 40 +-- .../components/data-table-sparkgraph.tsx | 60 ++-- src/shared/components/experiment.test.tsx | 8 + src/shared/components/initials.test.tsx | 10 +- src/shared/components/initials.tsx | 9 +- src/shared/components/metadata.test.tsx | 2 + src/shared/components/metadata.tsx | 2 +- src/shared/components/variables.scss | 3 +- src/shared/experiment-types.ts | 3 + src/shared/index.tsx | 16 +- 26 files changed, 421 insertions(+), 339 deletions(-) create mode 100644 src/mobile-app/hooks/use-icon-style.ts delete mode 100644 src/shared/components/data-table-barchart.tsx diff --git a/cypress/supports/elements/ExperimentSetup.js b/cypress/supports/elements/ExperimentSetup.js index fba0891..3ad61a9 100644 --- a/cypress/supports/elements/ExperimentSetup.js +++ b/cypress/supports/elements/ExperimentSetup.js @@ -46,15 +46,15 @@ class ExperimentSetup { } getNameInput() { - return cy.get('.experiment-wrapper-module-editing-vortex input').focus() + return cy.get('.experiment-wrapper-module-name-vortex input').focus() } getExperimentNameSpan() { - return cy.get('.experiment-wrapper-module-nameDisplay-vortex') + return cy.get('.experiment-wrapper-module-name-vortex') } getExperimentName(name) { - return cy.get('.experiment-wrapper-module-nameDisplay-vortex').contains(name) + return cy.get('.experiment-wrapper-module-name-vortex').contains(name) } getStudySiteDropDown() { diff --git a/src/authoring-app/utils/experiment-authoring-schema.json b/src/authoring-app/utils/experiment-authoring-schema.json index 7d98d72..da10e72 100644 --- a/src/authoring-app/utils/experiment-authoring-schema.json +++ b/src/authoring-app/utils/experiment-authoring-schema.json @@ -67,6 +67,26 @@ "examples": [ "SI" ] + }, + "iconColor": { + "$id": "#/properties/metadata/properties/iconColor", + "type": "string", + "title": "The IconColor Schema", + "description": "Icon color for this template.", + "default": "", + "examples": [ + "#0f73b8" + ] + }, + "iconHoverColor": { + "$id": "#/properties/metadata/properties/iconHoverColor", + "type": "string", + "title": "The IconHoverColor Schema", + "description": "Icon hover color for this template.", + "default": "", + "examples": [ + "#4b96ca" + ] } } }, diff --git a/src/data/experiments.json b/src/data/experiments.json index 6ae7d99..0bbe2e3 100644 --- a/src/data/experiments.json +++ b/src/data/experiments.json @@ -1,10 +1,136 @@ [ + { + "version": "1.0.0", + "metadata": { + "uuid": "aefb5299-c127-4d84-b7f0-78da389ebecd", + "name": "Data Trial", + "initials": "DT", + "iconColor": "#008a09", + "iconHoverColor": "#40a847" + }, + "schema": { + "sections": [ + { + "title": "Collect", + "icon": "collect", + "formFields": [ + "experimentData" + ] + }, + { + "title": "Note & Photo", + "icon": "note_and_photo", + "formFields": [ + "photo" + ] + } + ], + "dataSchema": { + "type": "object", + "required": [], + "properties": { + "experimentData": { + "type": "array", + "items": { + "type": "object", + "required": [ + "timeSeries" + ], + "properties": { + "timeSeries": { + "title": "Readout", + "type": "array" + }, + "label": { + "title": "Label", + "type": "string", + "placeholder": "Label #$N" + } + } + } + }, + "photo": { + "title": "Photos", + "type": "array", + "items": { + "type": "object", + "properties": { + "isPhoto": { + "type": "boolean" + }, + "localPhotoUrl": { + "type": "string" + }, + "remotePhotoUrl": { + "type": "string" + }, + "note": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + } + } + } + }, + "formUiSchema": { + "customName": { + "ui:icon": "label", + "ui:placeholder": "Time Series Investigation" + }, + "note": { + "items": { + "ui:widget": "textarea" + } + }, + "experimentData": { + "ui:field": "dataTable", + "ui:dataTableOptions": { + "sensorFields": [ + "timeSeries" + ], + "filters": [{ + "namePrefix": "GDX-" + }] + } + }, + "photo": { + "ui:field": "photo" + } + } + }, + "data": { + "experimentData": [ + { + "label": "" + }, + { + "label": "" + }, + { + "label": "" + }, + { + "label": "" + }, + { + "label": "" + } + ], + "photo": [] + } + }, { "version": "1.0.0", "metadata": { "uuid": "e431af00-5ef9-44f8-a887-c76caa6ddde1", "name": "Schoolyard Investigation", - "initials": "SI" + "initials": "SI", + "iconColor": "#e0007f", + "iconHoverColor": "#e8409f" }, "schema": { "titleField": "studySite", @@ -184,7 +310,9 @@ "metadata": { "uuid": "d27df06a-0997-4c53-8afd-d5dcc627d44f", "name": "Stream Study", - "initials": "SS" + "initials": "SS", + "iconColor": "#0f73b8", + "iconHoverColor": "#4b96ca" }, "schema": { "titleField": "studySite", @@ -347,7 +475,9 @@ "metadata": { "uuid": "df02396f-a3d6-4dc5-bc10-fac4007fb6de", "name": "Pond Study", - "initials": "PS" + "initials": "PS", + "iconColor": "#d04a06", + "iconHoverColor": "#dc7744" }, "schema": { "titleField": "Collect", @@ -509,126 +639,5 @@ ], "photo": [] } - }, - { - "version": "1.0.0", - "metadata": { - "uuid": "aefb5299-c127-4d84-b7f0-78da389ebecd", - "name": "Record Data Trials", - "initials": "RD" - }, - "schema": { - "sections": [ - { - "title": "Collect", - "icon": "collect", - "formFields": [ - "experimentData" - ] - }, - { - "title": "Note & Photo", - "icon": "note_and_photo", - "formFields": [ - "photo" - ] - } - ], - "dataSchema": { - "type": "object", - "required": [], - "properties": { - "experimentData": { - "type": "array", - "items": { - "type": "object", - "required": [ - "timeSeries" - ], - "properties": { - "timeSeries": { - "title": "Readout", - "type": "array" - }, - "label": { - "title": "Label", - "type": "string" - } - } - } - }, - "photo": { - "title": "Photos", - "type": "array", - "items": { - "type": "object", - "properties": { - "isPhoto": { - "type": "boolean" - }, - "localPhotoUrl": { - "type": "string" - }, - "remotePhotoUrl": { - "type": "string" - }, - "note": { - "type": "string" - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - } - } - } - } - }, - "formUiSchema": { - "customName": { - "ui:icon": "label", - "ui:placeholder": "Time Series Investigation" - }, - "note": { - "items": { - "ui:widget": "textarea" - } - }, - "experimentData": { - "ui:field": "dataTable", - "ui:dataTableOptions": { - "sensorFields": [ - "timeSeries" - ], - "filters": [{ - "namePrefix": "GDX-" - }] - } - }, - "photo": { - "ui:field": "photo" - } - } - }, - "data": { - "experimentData": [ - { - "label": "" - }, - { - "label": "" - }, - { - "label": "" - }, - { - "label": "" - }, - { - "label": "" - } - ], - "photo": [] - } } ] \ No newline at end of file diff --git a/src/mobile-app/components/experiment-picker-item.module.scss b/src/mobile-app/components/experiment-picker-item.module.scss index 53499f3..2b18834 100644 --- a/src/mobile-app/components/experiment-picker-item.module.scss +++ b/src/mobile-app/components/experiment-picker-item.module.scss @@ -6,7 +6,7 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; } .icon { @@ -26,23 +26,6 @@ background-color: $sensorGreenLight1; cursor: pointer; } - - &.SS{ - background-color: $streamBlue; - &:hover { - background-color: $streamBlueLight1; - cursor: pointer; - } - - } - &.SI{ - background-color: $sensorGreen; - &:hover { - background-color: $sensorGreenLight1; - cursor: pointer; - } - } - } .title { diff --git a/src/mobile-app/components/experiment-picker-item.tsx b/src/mobile-app/components/experiment-picker-item.tsx index 1b93b0d..dd4f38a 100644 --- a/src/mobile-app/components/experiment-picker-item.tsx +++ b/src/mobile-app/components/experiment-picker-item.tsx @@ -1,5 +1,6 @@ import React from "react"; import { IExperiment } from "../../shared/experiment-types"; +import { useIconStyle } from "../hooks/use-icon-style"; import css from "./experiment-picker-item.module.scss"; @@ -10,20 +11,11 @@ interface IProps { export const ExperimentPickerItem: React.FC = ({experiment, setExperiment}) => { const {name} = experiment.metadata; - const buttonColor = (initials: string) => { - switch (initials) { - case "SS": - return css.SS; - case "SI": - return css.SI; - default: - return; - } - }; + const {style, handleMouseOut, handleMouseOver} = useIconStyle(experiment.metadata); return ( -
-
+
+
+
+
{name}
diff --git a/src/mobile-app/components/experiment-wrapper.module.scss b/src/mobile-app/components/experiment-wrapper.module.scss index 9481fde..382fd3f 100644 --- a/src/mobile-app/components/experiment-wrapper.module.scss +++ b/src/mobile-app/components/experiment-wrapper.module.scss @@ -8,22 +8,33 @@ flex-wrap: nowrap; align-items: center; background-color: $ccTealDark2; - .nameDisplay{ - display: flex; - } - .editing{ - display: flex; - input{ - display: flex; - min-width: 400px; - background-color: $ccTealDark2; + + + .headerTitle { + margin: 0 10px 0 10px; + font-size: 18px; + color: #fff; + flex-grow: 1; + + .name { + display: block; color: white; - border: 1px solid #fff; + border: 1px solid #D1D1D1; border-radius: 4px; - padding-left: 4px; - outline: none; - &:focus{ + padding: 2px 4px; + width: 100%; + + input{ + padding: 0; + width: 100%; + font-size: 18px; + background-color: $ccTealDark2; + color: white; + border: none; outline: none; + &:focus{ + outline: none; + } } } } @@ -40,12 +51,6 @@ } } -.headerTitle { - margin: 0 10px 0 10px; - font-size: 18px; - color: #fff; - flex-grow: 1; -} .headerMenu { margin-right: 15px; diff --git a/src/mobile-app/components/experiment-wrapper.tsx b/src/mobile-app/components/experiment-wrapper.tsx index 4273b0e..dc65cd9 100644 --- a/src/mobile-app/components/experiment-wrapper.tsx +++ b/src/mobile-app/components/experiment-wrapper.tsx @@ -30,7 +30,6 @@ const experimentConfig: IExperimentConfig = { export const ExperimentWrapper: React.FC = ({ experiment, experimentIdx, data, onDataChange, onBackBtnClick, onUpload, embeddedPreview }) => { const [editing, isEditing ] = useState(false); const { metadata } = experiment; - const { initials } = metadata; const workSpaceClass = embeddedPreview ? `${css.workspace} ${css.embeddedPreview}`: css.workspace; const handleSave = () => { @@ -70,16 +69,14 @@ export const ExperimentWrapper: React.FC = ({ experiment, experimentIdx,
- +
- {editing && -
- -
- } - {!editing && -
{name}
- } +
+ {editing + ? + :
{name}
+ } +
{title}
diff --git a/src/mobile-app/components/run-picker.tsx b/src/mobile-app/components/run-picker.tsx index 0ce4117..8e69144 100644 --- a/src/mobile-app/components/run-picker.tsx +++ b/src/mobile-app/components/run-picker.tsx @@ -53,7 +53,7 @@ export const RunPicker: React.FC = ({ runs, onRunSelect, onRunUpload, on runs.map(run =>
- +
diff --git a/src/mobile-app/components/sensor.module.scss b/src/mobile-app/components/sensor.module.scss index e6389ca..57d59be 100644 --- a/src/mobile-app/components/sensor.module.scss +++ b/src/mobile-app/components/sensor.module.scss @@ -80,35 +80,48 @@ border-left: none; } -.connectedValues { -} -.disconnectedValues { -} - .timeSeriesValue { height: 100px; padding: 10px; + display: flex; + justify-content: space-between; + font-size: 17px; - .tsvInfo { + .tsvLeft { display: flex; - justify-content: space-between; + flex-direction: column; + align-items: left; + gap: 5px; - .tsvInfoLeft { + .tsvGraph { display: flex; - gap: 5px; + gap: 7px; + align-items: center; - .tsvBar { - display: flex; + svg { + width: 25px; } + .tsvValue { - color: $sensorGreenLight1; - font-size: 32px; + color: #008a09; + font-size: 22px; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + gap: 5px; } } + + .tsvMeasurement { + padding-left: 2px; + } } - .tsvMeasurement { - color: $sensorGreen; - font-size: 18px; + + .tsvSeparator { + width: 2px; + border-right: 2px solid #bfe9c2; + margin: 0 5px; } } diff --git a/src/mobile-app/components/sensor.tsx b/src/mobile-app/components/sensor.tsx index 641ddbb..063b36d 100644 --- a/src/mobile-app/components/sensor.tsx +++ b/src/mobile-app/components/sensor.tsx @@ -6,7 +6,8 @@ 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 BarChart from "../../shared/components/data-table-barchart"; +import { IDataTableTimeData } from "../../shared/components/data-table-field"; +import DataTableSparkGraph from "../../shared/components/data-table-sparkgraph"; import css from "./sensor.module.scss"; @@ -16,6 +17,8 @@ interface ISensorSelectorProps { cancel: () => void; } +const maxTimeSeriesValues = 5; + export const SensorSelectorComponent: React.FC = ({devices, selectDevice, cancel}) => { const sortedDevices = devices.sort((a, b) => a.id.localeCompare(b.id)); const handleCancel = () => cancel(); @@ -69,6 +72,7 @@ export const SensorComponent: React.FC = ({sensor, manual const [showDeviceSelect, setShowDeviceSelect] = useState(false); const selectDevice = useRef(); const cancelSelectDevice = useRef(); + const latestValuesRef = useRef([]); const clearSelectDevice = () => { selectDevice.current = undefined; @@ -162,23 +166,38 @@ export const SensorComponent: React.FC = ({sensor, manual const maxSamples = sampleRate * MaxNumberOfTimeSeriesValues; const value = values[valueKey]; const displayValue = value !== undefined ? value.toFixed(1) : "--"; + while (latestValuesRef.current.length > maxTimeSeriesValues) { + latestValuesRef.current.shift(); + } + latestValuesRef.current.push({value: value ?? 0, time: 0}); + latestValuesRef.current[0].capabilities = timeSeriesCapabilities; return (
-
-
-
- +
+
+ +
+
{displayValue}
+
{units}
-
{displayValue}
-
-
Samples: {sampleRate}/sec
-
Max Time: {maxSamples} secs
+
+ {measurement}
-
- {measurement} ({units}) +
+
+
Samples: {sampleRate}/sec
+
Max Time: {maxSamples} secs
); diff --git a/src/mobile-app/components/uploader.tsx b/src/mobile-app/components/uploader.tsx index e31cc01..50afef6 100644 --- a/src/mobile-app/components/uploader.tsx +++ b/src/mobile-app/components/uploader.tsx @@ -276,7 +276,7 @@ export const Uploader = (props: IProps) => {
-
+
diff --git a/src/mobile-app/hooks/use-experiments.test.ts b/src/mobile-app/hooks/use-experiments.test.ts index fa2ed31..a51b51e 100644 --- a/src/mobile-app/hooks/use-experiments.test.ts +++ b/src/mobile-app/hooks/use-experiments.test.ts @@ -20,6 +20,8 @@ describe("use-experiments hook", () => { uuid: "first", name: "First Experiment", initials: "FE", + iconColor: "#000", + iconHoverColor: "#777" }, schema: emptySchema }, @@ -29,6 +31,8 @@ describe("use-experiments hook", () => { uuid: "second", name: "Second Experiment", initials: "SE", + iconColor: "#000", + iconHoverColor: "#777" }, schema: emptySchema }, diff --git a/src/mobile-app/hooks/use-icon-style.ts b/src/mobile-app/hooks/use-icon-style.ts new file mode 100644 index 0000000..241cad8 --- /dev/null +++ b/src/mobile-app/hooks/use-icon-style.ts @@ -0,0 +1,25 @@ +import { useState, useMemo } from "react"; +import { IExperimentMetadata } from "../../shared/experiment-types"; + +// to support older saved experiments before the icon colors were added to the json +const defaults: Record = { + "SI": {color: "#e0007f", hoverColor: "#e8409f"}, + "PS": {color: "#d04a06", hoverColor: "#dc7744"}, + "SS": {color: "#0f73b8", hoverColor: "#4b96ca"}, + "DT": {color: "#008a09", hoverColor: "#40a847"}, +}; + +export const useIconStyle = ({iconColor, iconHoverColor, initials}: IExperimentMetadata) => { + const color = iconColor ?? defaults[initials]?.color ?? "#00a80a"; + const hoverColor = iconHoverColor ?? defaults[initials]?.hoverColor ?? "#40be48"; + + const [hovering, setHovering] = useState(false); + const style: React.CSSProperties = useMemo(() => { + return {backgroundColor: hovering ? hoverColor : color}; + }, [hovering]); + + const handleMouseOver = () => setHovering(true); + const handleMouseOut = () => setHovering(false); + + return {style, handleMouseOver, handleMouseOut}; +}; diff --git a/src/shared/components/data-table-barchart.tsx b/src/shared/components/data-table-barchart.tsx deleted file mode 100644 index 1318199..0000000 --- a/src/shared/components/data-table-barchart.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useMemo } from 'react'; - -const width = 40; -const height = 40; -const barWidth = width * 0.5; - -interface IBarChartProps { - min: number; - max: number; - value: number; -} - -const BarChart: React.FC = ({ min, max, value }) => { - - // Calculate the bar height based on the value - const barHeight = useMemo(() => { - value = Math.max(min, Math.min(value, max)); - return ((value - min) / (max - min)) * height; - }, [min, max, value]); - - const x = (width - barWidth) / 2; - - return ( - - - - - - - - - - ); -}; - -export default BarChart; diff --git a/src/shared/components/data-table-field.module.scss b/src/shared/components/data-table-field.module.scss index 6933513..b806f00 100644 --- a/src/shared/components/data-table-field.module.scss +++ b/src/shared/components/data-table-field.module.scss @@ -130,14 +130,22 @@ $headerBorder: #fff; } } input, select { - border: none; - width: 100%; + // border: none; + border: solid 2px $ccGrayLight2; + width: calc(100% - 15px); height: 100%; - text-align: center; + text-align: left; + font-weight: normal; + font-size: 15px; + padding: 5px; + border-radius: 5px; &:focus { outline: none; border: solid 2px $ccTeal; } + &:disabled { + border: none; + } } input[disabled], select[disabled] { opacity: 1; @@ -223,4 +231,22 @@ $headerBorder: #fff; display: flex; flex-direction: row; } + + .sparkgraphContainer { + 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; + } + } } diff --git a/src/shared/components/data-table-field.tsx b/src/shared/components/data-table-field.tsx index f2ad8b7..d9b0532 100644 --- a/src/shared/components/data-table-field.tsx +++ b/src/shared/components/data-table-field.tsx @@ -24,6 +24,7 @@ interface IDataTableStringInputField { type: "string"; title?: string; readOnly?: boolean; + placeholder?: string; } interface IDataTableNumberInputField { type: "number" | "integer"; @@ -31,6 +32,7 @@ interface IDataTableNumberInputField { readOnly?: boolean; minimum?: number; maximum?: number; + placeholder?: string; } interface IDataTableArrayFieldItems { type: "string" | "number"; @@ -41,6 +43,7 @@ interface IDataTableArrayField { items: IDataTableArrayFieldItems; title?: string; readOnly?: boolean; + placeholder?: string; } interface IDataTableDataSchema { @@ -429,8 +432,9 @@ export const DataTableField: React.FC = props => { ); }; - const renderInput = (options: {name: string, value: any, rowIdx: number, disabled: boolean, error?: string}) => { + const renderInput = (options: {name: string, value: any, placeholder?: string, rowIdx: number, disabled: boolean, error?: string}) => { const {name, value, rowIdx, disabled, error} = options; + const placeholder = (options.placeholder ?? "").replace("$N", String(rowIdx + 1)); return ( <> = props => { disabled={disabled} onChange={handleInputChange.bind(null, rowIdx, name)} onBlur={handleInputBlur.bind(null, rowIdx, name)} + placeholder={placeholder} /> {error ?
: undefined} {error ?
{error}
: undefined} @@ -456,6 +461,7 @@ export const DataTableField: React.FC = props => { return fieldNames.map(name => { let value = row[name] || ""; const readOnly = fieldDefinition[name].readOnly; + const placeholder = fieldDefinition[name].placeholder; let isFunction = false; if (isFunctionSymbol(value)) { value = handleSpecialValue(value, name, formData); @@ -481,13 +487,29 @@ export const DataTableField: React.FC = props => { let contents; if (name === "timeSeries") { - contents = ; + let graphTitle = ""; + const values= value || []; + if (timeSeriesCapabilities) { + const duration = Math.round((timeSeriesCapabilities.measurementPeriod / 1000) * values.length); + graphTitle = `${duration} sec`; + } + + contents = +
+
{graphTitle}
+ +
; } else if (readOnly) { contents =
{value}
; } 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 }); + const input = renderInput({ name, value, placeholder, rowIdx, disabled: isFunction || (isSensorField && !manualEntryMode), error }); contents =
{sensorFieldsBlank && sensorCanRecord && renderPromptForData(sensorFieldIdx === 0)} diff --git a/src/shared/components/data-table-sparkgraph.module.scss b/src/shared/components/data-table-sparkgraph.module.scss index 6ac3c2d..5b1e3ee 100644 --- a/src/shared/components/data-table-sparkgraph.module.scss +++ b/src/shared/components/data-table-sparkgraph.module.scss @@ -1,35 +1,19 @@ @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; + polyline { + stroke: $timeSeriesStroke; + stroke-width: 1.5px; + fill: none; } - svg { - margin-top: -6px; - - polyline { - stroke: $timeSeriesStroke; - stroke-width: 1.5px; - fill: none; - } - - polygon { - stroke: none; - fill: $timeSeriesFill; - } + polygon { + stroke: none; + fill: $timeSeriesFill; + } - circle { - stroke: $timeSeriesStroke; - fill: $timeSeriesLeaderFill; - } + circle { + stroke: $timeSeriesStroke; + fill: $timeSeriesLeaderFill; } -} \ No newline at end of file +} diff --git a/src/shared/components/data-table-sparkgraph.tsx b/src/shared/components/data-table-sparkgraph.tsx index 3dcb302..90e7429 100644 --- a/src/shared/components/data-table-sparkgraph.tsx +++ b/src/shared/components/data-table-sparkgraph.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from "react"; +import React, { useRef } from "react"; import { IDataTableTimeData } from "./data-table-field"; import css from "./data-table-sparkgraph.module.scss"; @@ -7,55 +7,57 @@ const leaderRadius = 3; const leaderStokeWidth = 1.5; const leaderMargin = leaderRadius + leaderStokeWidth; -const outerWidth = 200; -const outerHeight = 30; -const innerWidth = outerWidth - (leaderMargin * 2); -const innerHeight = outerHeight - (leaderMargin * 2); - interface ISparkGraphPoint { x: number; y: number; } interface IProps { + width: number; + height: number; values: IDataTableTimeData[]; maxNumTimeSeriesValues: number; + minNumTimeSeriesValues?: number; + showAxes?: boolean; + redrawSignal?: number; } -export default function DataTableSparkGraph({values, maxNumTimeSeriesValues}: IProps) { +export default function DataTableSparkGraph({width, height, values, maxNumTimeSeriesValues, minNumTimeSeriesValues, showAxes, redrawSignal}: IProps) { + const innerWidth = width - (leaderMargin * 2); + const innerHeight = height - (leaderMargin * 2); + const innerLeft = leaderMargin; + const innerRight = innerLeft + innerWidth; + const innerTop = leaderMargin; + const innerBottom = innerTop + innerHeight; + const polyLinePointsRef = useRef([]); const polygonPointsRef = useRef([]); const leaderPointRef = useRef(undefined); const lastMaxNumTimeSeriesValuesRef = useRef(maxNumTimeSeriesValues); - const titleRef = useRef(""); + const lastRedrawSignalRef = useRef(redrawSignal); let polyLinePoints = polyLinePointsRef.current; let polygonPoints = polygonPointsRef.current; let leaderPoint = leaderPointRef.current; - // instead of relying on the values changing just compare the last number of points displayed (the +2 is for the closing polygon points) - const graphChanged = (polyLinePoints.length !== values.length) || (maxNumTimeSeriesValues !== lastMaxNumTimeSeriesValuesRef.current); + // instead of relying on the values changing just compare the last number of points displayed or if we are signaled to redraw + const graphChanged = (polyLinePoints.length !== values.length) + || (maxNumTimeSeriesValues !== lastMaxNumTimeSeriesValuesRef.current) + || (redrawSignal !== lastRedrawSignalRef.current); if (graphChanged) { const capabilities = values[0]?.capabilities; - if (capabilities) { - const duration = Math.round((capabilities.measurementPeriod / 1000) * values.length); - titleRef.current = `${duration} sec`; - } else { - titleRef.current = ""; - } - 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 maxNumValues = Math.max(maxNumTimeSeriesValues, 100); + const maxNumValues = Math.max(maxNumTimeSeriesValues, (minNumTimeSeriesValues ?? 100)); let x: number = 0; let y: number = 0; polyLinePoints = values.map(({value}, index) => { value = Math.max(minValue, Math.min(value, maxValue)); - x = leaderMargin + (innerWidth * (index / maxNumValues)); - y = leaderMargin + (innerHeight - (((value - minValue) / (maxValue - minValue)) * innerHeight)); + x = innerLeft + (innerWidth * (index / maxNumValues)); + y = innerLeft + (innerHeight - (((value - minValue) / (maxValue - minValue)) * innerHeight)); return `${x},${y}`; }); @@ -63,21 +65,21 @@ export default function DataTableSparkGraph({values, maxNumTimeSeriesValues}: IP leaderPointRef.current = leaderPoint; // add closing polygon points - polygonPoints = values.length > 0 ? [...polyLinePoints, `${x}, ${innerHeight + leaderMargin}`, `${leaderMargin}, ${innerHeight + leaderMargin}`] : []; + polygonPoints = values.length > 0 ? [...polyLinePoints, `${x}, ${innerBottom}`, `${innerLeft}, ${innerBottom}`] : []; polyLinePointsRef.current = polyLinePoints; polygonPointsRef.current = polygonPoints; lastMaxNumTimeSeriesValuesRef.current = maxNumTimeSeriesValues; + lastRedrawSignalRef.current = redrawSignal; } return ( -
-
{titleRef.current}
- - {polygonPoints.length > 0 && } - {polyLinePoints.length > 0 && } - {leaderPoint && } - -
+ + {polygonPoints.length > 0 && } + {polyLinePoints.length > 0 && } + {showAxes && } + {showAxes && } + {leaderPoint && } + ); } \ No newline at end of file diff --git a/src/shared/components/experiment.test.tsx b/src/shared/components/experiment.test.tsx index ac2a5ba..715b1d0 100644 --- a/src/shared/components/experiment.test.tsx +++ b/src/shared/components/experiment.test.tsx @@ -17,6 +17,8 @@ describe("Experiment component", () => { uuid: "123", name: "test", initials: "tt", + iconColor: "#000", + iconHoverColor: "#777" }, schema: { sections: [{ @@ -61,6 +63,8 @@ describe("Experiment component", () => { uuid: "123", name: "test", initials: "tt", + iconColor: "#000", + iconHoverColor: "#777" }, schema: { sections: [{ @@ -99,6 +103,8 @@ describe("Experiment component", () => { uuid: "123", name: "test", initials: "tt", + iconColor: "#000", + iconHoverColor: "#777" }, schema: { sections: [{ @@ -143,6 +149,8 @@ describe("Experiment component", () => { uuid: "123", name: "test", initials: "tt", + iconColor: "#000", + iconHoverColor: "#777" }, schema: { sections: [{ diff --git a/src/shared/components/initials.test.tsx b/src/shared/components/initials.test.tsx index a6c9601..16865b7 100644 --- a/src/shared/components/initials.test.tsx +++ b/src/shared/components/initials.test.tsx @@ -1,10 +1,18 @@ import React from "react"; import { shallow } from "enzyme"; import { Initials } from "./initials"; +import { IExperimentMetadata } from "../experiment-types"; describe("Initials component", () => { it("renders provided text", () => { - const wrapper = shallow(); + const metadata: IExperimentMetadata = { + uuid: "test", + name: "Test", + initials: "TT", + iconColor: "#000", + iconHoverColor: "#777", + }; + const wrapper = shallow(); expect(wrapper.text()).toEqual("TT"); }); }); diff --git a/src/shared/components/initials.tsx b/src/shared/components/initials.tsx index f5ca27a..dfdcd2c 100644 --- a/src/shared/components/initials.tsx +++ b/src/shared/components/initials.tsx @@ -1,11 +1,14 @@ import React from "react"; import css from "./initials.module.scss"; +import { IExperimentMetadata } from "../experiment-types"; +import { useIconStyle } from "../../mobile-app/hooks/use-icon-style"; interface IProps { - text: string; + metadata: IExperimentMetadata; active?: boolean; } -export const Initials: React.FC = ({ text, active }) => { - return
{text}
; +export const Initials: React.FC = ({ metadata, active }) => { + const {style, handleMouseOut, handleMouseOver} = useIconStyle(metadata); + return
{metadata.initials}
; }; diff --git a/src/shared/components/metadata.test.tsx b/src/shared/components/metadata.test.tsx index 82aa6f2..ccec43f 100644 --- a/src/shared/components/metadata.test.tsx +++ b/src/shared/components/metadata.test.tsx @@ -12,6 +12,8 @@ describe("Metadata component", () => { uuid: "123", name: "test experiment", initials: "tt", + iconColor: "#000", + iconHoverColor: "#777" }, schema: { sections: [], diff --git a/src/shared/components/metadata.tsx b/src/shared/components/metadata.tsx index c976f7c..00150ce 100644 --- a/src/shared/components/metadata.tsx +++ b/src/shared/components/metadata.tsx @@ -8,7 +8,7 @@ export const Metadata: SectionComponent = ({ experiment, data }) => { return (
- +
{experiment.metadata.name}
{formatTime(data.timestamp)}
diff --git a/src/shared/components/variables.scss b/src/shared/components/variables.scss index abdb3e2..82acabd 100644 --- a/src/shared/components/variables.scss +++ b/src/shared/components/variables.scss @@ -20,14 +20,13 @@ $ccOrangeLight3: #fadacb; $ccOrangeLight4: #fdf5f0; $ccGray: #f1f1f1; $ccGrayLight1: #a1a1a1; +$ccGrayLight2: #737373; $darkGray: #646464; $textBlack: #444444; $sensorGreen: #00a80a; $sensorGreenLight1: #40be48; $sensorGreenLight2: #7fd384; -$streamBlue: #0f73b8; -$streamBlueLight1: #4b96ca; $timeSeriesStroke: #444444; $timeSeriesFill: #99FF8A; diff --git a/src/shared/experiment-types.ts b/src/shared/experiment-types.ts index 02daf41..5757787 100644 --- a/src/shared/experiment-types.ts +++ b/src/shared/experiment-types.ts @@ -64,12 +64,15 @@ export interface IExperimentV1 { uuid: string; name: string; initials: string; + iconColor: string; + iconHoverColor: string; }; schema: IExperimentSchema; data?: IExperimentData; } export type IExperiment = IExperimentV1; +export type IExperimentMetadata = IExperimentV1["metadata"]; export interface IExperimentData { // This will be injected by ExperimentWrapper automatically on initial load. diff --git a/src/shared/index.tsx b/src/shared/index.tsx index c146aec..a2a7b5f 100644 --- a/src/shared/index.tsx +++ b/src/shared/index.tsx @@ -47,7 +47,9 @@ const experiment2 = { "metadata": { "uuid": "e431af00-5ef9-44f8-a887-c76caa6ddde1", "name": "Data Table Example", - "initials": "DT" + "initials": "DT", + "iconColor": "#000", + "iconHoverColor": "#777" }, "schema": { "sections": [ @@ -144,7 +146,9 @@ const experiment3 = { "metadata": { "uuid": "e431af00-5ef9-44f8-a887-c76caa6ddde1", "name": "Data Table Example", - "initials": "DT" + "initials": "DT", + "iconColor": "#000", + "iconHoverColor": "#777" }, "schema": { "sections": [ @@ -217,7 +221,9 @@ const experiment4 = { "metadata": { "uuid": "e431af00-5ef9-44f8-a887-c76caa6ddde1", "name": "Data Table Example", - "initials": "DT" + "initials": "DT", + "iconColor": "#000", + "iconHoverColor": "#777" }, "schema": { "sections": [ @@ -293,7 +299,9 @@ const experiment5 = { "metadata": { "uuid": "e431af00-5ef9-44f8-a887-c76caa6ddde1", "name": "Data Table Example", - "initials": "DT" + "initials": "DT", + "iconColor": "#000", + "iconHoverColor": "#777" }, "schema": { "sections": [