From 294bc3580fdde16dfca896e3331c59b28ed455b1 Mon Sep 17 00:00:00 2001 From: toddnief Date: Fri, 16 Aug 2024 15:10:58 -0500 Subject: [PATCH 01/13] add margin for display controls --- dashboard-react/components/DashboardDisplayControls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard-react/components/DashboardDisplayControls.js b/dashboard-react/components/DashboardDisplayControls.js index bcaa6ce..ad9693f 100644 --- a/dashboard-react/components/DashboardDisplayControls.js +++ b/dashboard-react/components/DashboardDisplayControls.js @@ -11,7 +11,7 @@ export default function DashboardDisplayControls() { return ( <> -
+

Display Options

Date: Sat, 17 Aug 2024 15:39:04 +0000 Subject: [PATCH 02/13] update devcontainer to use ruff --- .devcontainer/devcontainer.json | 67 ++++++++++-------------- scripts/pipeline-template.py | 90 ++++++++------------------------- 2 files changed, 48 insertions(+), 109 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c201636..3ef2ffd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,48 +3,33 @@ "build": { "dockerfile": "../Dockerfile", "context": "..", - "args": {}, + "args": {} }, // Set *default* container specific settings.json values on container create. - "customizations": { - "vscode": { - "settings": { - "terminal.integrated.profiles.linux": { - "bash": { - "path": "/bin/bash" - } - }, - "python.defaultInterpreterPath": "/usr/local/bin/python3", - "python.languageServer": "Pylance", - "python.formatting.provider": "black", - "[python]": { - "diffEditor.ignoreTrimWhitespace": false, - "editor.formatOnSave": true, - "editor.wordBasedSuggestions": "matchingDocuments", - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.codeActionsOnSave": { - "source.organizeImports": true - } - }, - "isort.args": [ - "--settings-path", "${workspaceFolder}/setup.cfg" - ], - "flake8.args": [ - "--config", "${workspaceFolder}/setup.cfg" - ] - }, - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.flake8", - "ms-python.black-formatter", - "ms-python.isort", - "ms-vscode-remote.remote-containers", - "ms-toolsai.jupyter", - "ms-toolsai.jupyter-renderers" - ] - } - }, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash" + } + }, + "python.defaultInterpreterPath": "/usr/local/bin/python3", + "python.languageServer": "None", // Prevent Pylance warnings - using Ruff + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "extensions": [ + "ms-python.debugpy", + "charliermarsh.ruff", + "ms-vscode-remote.remote-containers", + "ms-toolsai.jupyter", + "ms-toolsai.jupyter-renderers" + ] + } + }, // Add the IDs of extensions you want installed when the container is created. "features": { "github-cli": "latest" @@ -70,4 +55,4 @@ // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "workspaceMount": "source=${localWorkspaceFolder},target=/project,type=bind", "workspaceFolder": "/project" -} +} \ No newline at end of file diff --git a/scripts/pipeline-template.py b/scripts/pipeline-template.py index e16bdde..8e3a96a 100644 --- a/scripts/pipeline-template.py +++ b/scripts/pipeline-template.py @@ -41,10 +41,7 @@ OUTLIER_THRESHOLD = 10 item2id = { - key.strip(): value - for key, value in df_items.set_index("Item Description Refined")["Item ID"] - .to_dict() - .items() + key.strip(): value for key, value in df_items.set_index("Item Description Refined")["Item ID"].to_dict().items() } extra_items = pd.read_excel(EXTRA_ITEMS_PATH) @@ -102,14 +99,10 @@ def map_technology(trial_id: str) -> str: "Facility 10": "WR005-01", } -OPERATING_CONDITIONS_PATH = ( - DATA_DIR / "Donated Data 2023 - Compiled Facility Conditions for DSI.xlsx" -) +OPERATING_CONDITIONS_PATH = DATA_DIR / "Donated Data 2023 - Compiled Facility Conditions for DSI.xlsx" # TODO: Set this up so we can actually plot the full temperature data -df_temps = pd.read_excel( - OPERATING_CONDITIONS_PATH, sheet_name=3, skiprows=1, index_col="Day #" -) +df_temps = pd.read_excel(OPERATING_CONDITIONS_PATH, sheet_name=3, skiprows=1, index_col="Day #") df_temps.columns = [trial2id[col.replace("*", "")] for col in df_temps.columns] df_avg_temps = df_temps.mean().to_frame("Average Temperature (F)") @@ -118,34 +111,23 @@ def map_technology(trial_id: str) -> str: sheet_name=2, skiprows=3, ) -df_trial_duration.columns = [ - col.replace("\n", "").strip() for col in df_trial_duration.columns -] -df_trial_duration = df_trial_duration[ - ["Facility Designation", "Endpoint Analysis (trial length)"] -].rename( +df_trial_duration.columns = [col.replace("\n", "").strip() for col in df_trial_duration.columns] +df_trial_duration = df_trial_duration[["Facility Designation", "Endpoint Analysis (trial length)"]].rename( columns={ "Facility Designation": "Trial ID", "Endpoint Analysis (trial length)": "Trial Duration", } ) df_trial_duration["Trial ID"] = ( - df_trial_duration["Trial ID"] - .str.replace("( ", "(", regex=False) - .str.replace(" )", ")", regex=False) - .map(trial2id) + df_trial_duration["Trial ID"].str.replace("( ", "(", regex=False).str.replace(" )", ")", regex=False).map(trial2id) ) df_trial_duration = df_trial_duration.set_index("Trial ID") -df_moisture = pd.read_excel( - OPERATING_CONDITIONS_PATH, sheet_name=4, skiprows=1, index_col="Week" -) +df_moisture = pd.read_excel(OPERATING_CONDITIONS_PATH, sheet_name=4, skiprows=1, index_col="Week") df_moisture.columns = [trial2id[col.replace("*", "")] for col in df_moisture.columns] df_moisture = df_moisture.mean().to_frame("Average % Moisture (In Field)") -df_operating_conditions = pd.concat( - [df_trial_duration, df_avg_temps, df_moisture], axis=1 -) +df_operating_conditions = pd.concat([df_trial_duration, df_avg_temps, df_moisture], axis=1) processed_data = [] @@ -195,16 +177,12 @@ def __init__( self.output_filepath = self.data_filepath.with_name(filename + file_suffix) # TODO: This is kind of messy and could probably be better - self.raw_data = self.load_data( - data_filepath, sheet_name=sheet_name, skiprows=skiprows - ) + self.raw_data = self.load_data(data_filepath, sheet_name=sheet_name, skiprows=skiprows) self.items = items self.item2id = item2id @abstractmethod - def load_data( - self, data_filepath: Path, sheet_name: int = 0, skip_rows: int = 0 - ) -> pd.DataFrame: + def load_data(self, data_filepath: Path, sheet_name: int = 0, skip_rows: int = 0) -> pd.DataFrame: """Loads data from the specified file. This method should be implemented by subclasses to load data from the @@ -301,9 +279,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # Start weight is set in preprocess_data self.items = self.items.drop("Start Weight", axis=1) - def load_data( - self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0 - ) -> pd.DataFrame: + def load_data(self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0) -> pd.DataFrame: """Loads data from the specified Excel file. Args: @@ -347,12 +323,8 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: data["End Weight"] = data["End Weight"].fillna(0) # Ok...we need to do some weird items work arounds here...this might work? - casp004_items = pd.read_excel(self.data_filepath, sheet_name=2).drop_duplicates( - subset=["Item Name"] - ) - casp004_weights = casp004_items.set_index("Item Name")[ - "Weight (average)" - ].to_dict() + casp004_items = pd.read_excel(self.data_filepath, sheet_name=2).drop_duplicates(subset=["Item Name"]) + casp004_weights = casp004_items.set_index("Item Name")["Weight (average)"].to_dict() data["Start Weight"] = data["Product Name"].map(casp004_weights) # rename so this matches the other trials data["Item Description Refined"] = data["Product Name"] @@ -360,9 +332,7 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: # TODO: Some of this should be in the abstract method... data["Item ID"] = data["Item Description Refined"].str.strip().map(self.item2id) # Prevent duplicate columns when merging with items - data = data.rename( - columns={"Item Description Refined": "Item Description Refined (Trial)"} - ) + data = data.rename(columns={"Item Description Refined": "Item Description Refined (Trial)"}) data["Trial ID"] = "CASP004-01" if data["Item ID"].isna().sum() > 0: raise ValueError("There are null items after mapping") @@ -389,9 +359,7 @@ def calculate_results(self, data: pd.DataFrame) -> pd.DataFrame: return data -CASP004_PATH = ( - DATA_DIR / "CASP004-01 - Results Pre-Processed for Analysis from PDF Tables.xlsx" -) +CASP004_PATH = DATA_DIR / "CASP004-01 - Results Pre-Processed for Analysis from PDF Tables.xlsx" casp004_pipeline = CASP004Pipeline(CASP004_PATH, sheet_name=1, trial_name="casp004") processed_data.append(casp004_pipeline.run()) @@ -441,9 +409,7 @@ def melt_trial(self, data: pd.DataFrame, value_name: str) -> pd.DataFrame: .reset_index(drop=True) ) - def load_data( - self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0 - ) -> pd.DataFrame: + def load_data(self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0) -> pd.DataFrame: """Loads data from the specified Excel file. Args: @@ -492,9 +458,7 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: class PDFPipeline(AbstractDataPipeline): """Pipeline for processing PDF trial data.""" - def __init__( - self, *args: Any, weight_col: str = "Residual Weight - Oven-dry", **kwargs: Any - ) -> None: + def __init__(self, *args: Any, weight_col: str = "Residual Weight - Oven-dry", **kwargs: Any) -> None: """Initializes the PDFPipeline with the given parameters. Args: @@ -505,9 +469,7 @@ def __init__( super().__init__(*args, **kwargs) self.weight_col = weight_col - def load_data( - self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0 - ) -> pd.DataFrame: + def load_data(self, data_filepath: Path, sheet_name: int = 0, skiprows: int = 0) -> pd.DataFrame: """Loads data from the specified Excel file. Args: @@ -535,9 +497,7 @@ def join_with_items(self, data: pd.DataFrame) -> pd.DataFrame: # TODO: Do we want to merge on ID or should we just merge on description if we have it? data["Item ID"] = data["Item Description Refined"].str.strip().map(self.item2id) # Prevent duplicate columns when merging with items - data = data.rename( - columns={"Item Description Refined": "Item Description Refined (Trial)"} - ) + data = data.rename(columns={"Item Description Refined": "Item Description Refined (Trial)"}) drop_cols = ["Item Description From Trial"] data = data.drop(drop_cols, axis=1) if data["Item ID"].isna().sum() > 0: @@ -556,9 +516,7 @@ def calculate_results(self, data: pd.DataFrame) -> pd.DataFrame: Returns: Data with calculated results. """ - data["% Residuals (Mass)"] = data[self.weight_col] / ( - data["Start Weight"] * data["Number of Items per bag"] - ) + data["% Residuals (Mass)"] = data[self.weight_col] / (data["Start Weight"] * data["Number of Items per bag"]) data["% Residuals (Area)"] = None data["Trial"] = data["Trial ID"] return data @@ -616,9 +574,7 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: # Exclude mixed materials and multi-laminate pouches all_trials = all_trials[~(all_trials["Material Class II"] == "Mixed Materials")] -all_trials = all_trials[ - ~(all_trials["Item Name"] == "Multi-laminate stand-up pounch with zipper") -] +all_trials = all_trials[~(all_trials["Item Name"] == "Multi-laminate stand-up pounch with zipper")] # Exclude anything over 1000% as outlier all_trials = all_trials[all_trials["% Residuals (Mass)"] < OUTLIER_THRESHOLD] @@ -628,9 +584,7 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: all_trials.to_csv(output_filepath, index=False) # Make sure all trial IDs are represented in operating conditions -unique_trial_ids = pd.DataFrame( - all_trials["Trial ID"].unique(), columns=["Trial ID"] -).set_index("Trial ID") +unique_trial_ids = pd.DataFrame(all_trials["Trial ID"].unique(), columns=["Trial ID"]).set_index("Trial ID") df_operating_conditions = unique_trial_ids.merge( df_operating_conditions, left_index=True, right_index=True, how="left" ) From a10315110eff60d0815d22120e26582c86cdd020 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 14:22:44 -0500 Subject: [PATCH 03/13] start on operating conditions dash --- .../app/operating-conditions/page.js | 44 ++++++ .../OperatingConditionsDashboard.js | 142 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 dashboard-react/app/operating-conditions/page.js create mode 100644 dashboard-react/components/OperatingConditionsDashboard.js diff --git a/dashboard-react/app/operating-conditions/page.js b/dashboard-react/app/operating-conditions/page.js new file mode 100644 index 0000000..88a01dd --- /dev/null +++ b/dashboard-react/app/operating-conditions/page.js @@ -0,0 +1,44 @@ +"use client"; +import React, { useEffect } from "react"; +import state from "@/lib/state"; +import dynamic from "next/dynamic"; +import DashboardControls from "@/components/DashboardControls"; + +const OperatingConditionsDashboard = dynamic( + () => import("@/components/OperatingConditionsDashboard"), + { + ssr: false, + } +); + +export default function OperatingConditions() { + // Document this better — kind of confusing cuz this is what gets the options for the menus + // TODO: Is there a way to do this only on first load in state.js with Valtio? + useEffect(() => { + const fetchOptions = async () => { + const response = await fetch("/api/options"); + const result = await response.json(); + Object.keys(result).forEach((key) => { + state.setOptions(key, result[key]); + }); + }; + fetchOptions(); + }, []); + + return ( +
+
+ Please use a device that is at least 1280 pixels wide to view the + disintegration dashboard. +
+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js new file mode 100644 index 0000000..8c76986 --- /dev/null +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -0,0 +1,142 @@ +"use client"; +import React from "react"; +import Plot from "react-plotly.js"; +import { useSnapshot } from "valtio"; +import state from "@/lib/state"; +import { col2material } from "@/lib/constants"; +import Alert from "@/components/Alert"; + +export default function OperatingConditionsDashboard() { + const snap = useSnapshot(state); + + if (!snap.dataLoaded) { + return

Loading data...

; + } + + const class2color = { + "Positive Control": "#70AD47", + "Mixed Materials": "#48646A", + Fiber: "#298FC2", + Biopolymer: "#FFB600", + }; + + const maxLabelLength = 30; + + function wrapLabel(label) { + const words = label.split(" "); + let wrappedLabel = ""; + let line = ""; + + for (const word of words) { + if ((line + word).length > maxLabelLength) { + wrappedLabel += line + "
"; + line = word + " "; + } else { + line += word + " "; + } + } + wrappedLabel += line.trim(); // Add the last line + + return wrappedLabel.trim(); + } + + const plotData = + Object.keys(snap.data).length > 0 + ? snap.data.data.map((d) => { + console.log(d); + const materialClass = d["Material Class I"]; + const color = class2color[materialClass] || "#000"; + const countDisplay = + snap.filters["testMethod"] === "Mesh Bag" + ? ` (n=${d["count"]})` + : ""; + // Replace "Positive" with "Pos." in labels and append count + const name = `${d["aggCol"]}${countDisplay}`.replace( + "Positive", + "Pos." + ); + const wrappedName = wrapLabel(name); + + return { + type: "box", + name: wrappedName, + y: [d.min, d.q1, d.median, d.q3, d.max], + marker: { color }, + boxmean: true, + line: { width: 3.25 }, + }; + }) + : []; + + function generateYAxisTitle(displayCol, cap) { + let yAxisTitle = `${displayCol}`; + if (cap) { + yAxisTitle += " Capped"; + } + return yAxisTitle; + } + const yAxisTitle = generateYAxisTitle( + snap.filters.displayCol, + !snap.filters.uncapResults + ); + + function generateTitle(displayCol, aggCol, num_trials) { + return `${displayCol} by ${col2material[aggCol]} - ${num_trials} Trial(s)`; + } + + const title = generateTitle( + snap.filters.displayCol, + snap.filters.aggCol, + snap.data.numTrials + ); + + const yMax = + snap.data.data && snap.data.data.length > 0 + ? Math.max(...snap.data.data.map((d) => d.max + 0.05), 1.05) + : 1.05; + + const xTickAngle = plotData.length > 6 ? 90 : 0; + + return ( + <> + {snap.errorMessage ? ( +
+

+ +

+
+ ) : ( + ${title}`, + x: 0.5, + xanchor: "center", + yanchor: "top", + }, + showlegend: false, + yaxis: { + title: { + text: `${yAxisTitle}`, + }, + tickformat: ".0%", + range: [0, yMax], + }, + xaxis: { + tickangle: xTickAngle, + ticklen: 10, + automargin: true, + }, + hovermode: "x", + }} + config={{ + displayModeBar: false, + }} + /> + )} + + ); +} From bf06019a11352921ce7bf3743d023be27fc796cd Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 19:33:05 +0000 Subject: [PATCH 04/13] save average operating conditions separately --- scripts/pipeline-template.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/pipeline-template.py b/scripts/pipeline-template.py index 8e3a96a..72d9379 100644 --- a/scripts/pipeline-template.py +++ b/scripts/pipeline-template.py @@ -127,7 +127,7 @@ def map_technology(trial_id: str) -> str: df_moisture.columns = [trial2id[col.replace("*", "")] for col in df_moisture.columns] df_moisture = df_moisture.mean().to_frame("Average % Moisture (In Field)") -df_operating_conditions = pd.concat([df_trial_duration, df_avg_temps, df_moisture], axis=1) +df_operating_conditions_avg = pd.concat([df_trial_duration, df_avg_temps, df_moisture], axis=1) processed_data = [] @@ -585,11 +585,11 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: # Make sure all trial IDs are represented in operating conditions unique_trial_ids = pd.DataFrame(all_trials["Trial ID"].unique(), columns=["Trial ID"]).set_index("Trial ID") -df_operating_conditions = unique_trial_ids.merge( - df_operating_conditions, left_index=True, right_index=True, how="left" +df_operating_conditions_avg = unique_trial_ids.merge( + df_operating_conditions_avg, left_index=True, right_index=True, how="left" ) -operating_conditions_output_path = DATA_DIR / "operating_conditions.csv" -df_operating_conditions.to_csv(operating_conditions_output_path, index_label="Trial ID") +operating_conditions_output_path = DATA_DIR / "operating_conditions_avg.csv" +df_operating_conditions_avg.to_csv(operating_conditions_output_path, index_label="Trial ID") print("Complete!") From 21d5aedbaecfde2bdfbfd8bdec4802251f9fa8a7 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 19:41:42 +0000 Subject: [PATCH 05/13] save full temperature data in pipeline --- scripts/pipeline-template.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/pipeline-template.py b/scripts/pipeline-template.py index 72d9379..cb4231a 100644 --- a/scripts/pipeline-template.py +++ b/scripts/pipeline-template.py @@ -101,10 +101,9 @@ def map_technology(trial_id: str) -> str: OPERATING_CONDITIONS_PATH = DATA_DIR / "Donated Data 2023 - Compiled Facility Conditions for DSI.xlsx" -# TODO: Set this up so we can actually plot the full temperature data df_temps = pd.read_excel(OPERATING_CONDITIONS_PATH, sheet_name=3, skiprows=1, index_col="Day #") df_temps.columns = [trial2id[col.replace("*", "")] for col in df_temps.columns] -df_avg_temps = df_temps.mean().to_frame("Average Temperature (F)") +df_temps_avg = df_temps.mean().to_frame("Average Temperature (F)") df_trial_duration = pd.read_excel( OPERATING_CONDITIONS_PATH, @@ -125,9 +124,9 @@ def map_technology(trial_id: str) -> str: df_moisture = pd.read_excel(OPERATING_CONDITIONS_PATH, sheet_name=4, skiprows=1, index_col="Week") df_moisture.columns = [trial2id[col.replace("*", "")] for col in df_moisture.columns] -df_moisture = df_moisture.mean().to_frame("Average % Moisture (In Field)") +df_moisture_avg = df_moisture.mean().to_frame("Average % Moisture (In Field)") -df_operating_conditions_avg = pd.concat([df_trial_duration, df_avg_temps, df_moisture], axis=1) +df_operating_conditions_avg = pd.concat([df_trial_duration, df_temps_avg, df_moisture_avg], axis=1) processed_data = [] @@ -592,4 +591,8 @@ def preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: operating_conditions_output_path = DATA_DIR / "operating_conditions_avg.csv" df_operating_conditions_avg.to_csv(operating_conditions_output_path, index_label="Trial ID") +# Save full temperature data (TODO: Currently testing this...) +temperature_output_path = DATA_DIR / "temperature_data.csv" +df_temps.to_csv(temperature_output_path, index=True) + print("Complete!") From d256ba94dce908d7b15460ddf25c229219a12735 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 14:47:52 -0500 Subject: [PATCH 06/13] add rough draft of operating conditions dash --- .../app/operating-conditions/page.js | 4 +- .../OperatingConditionsDashboard.js | 139 +++++++----------- 2 files changed, 59 insertions(+), 84 deletions(-) diff --git a/dashboard-react/app/operating-conditions/page.js b/dashboard-react/app/operating-conditions/page.js index 88a01dd..6c0c05b 100644 --- a/dashboard-react/app/operating-conditions/page.js +++ b/dashboard-react/app/operating-conditions/page.js @@ -35,9 +35,11 @@ export default function OperatingConditions() {
+ {/*
-
+
+ */} ); diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index 8c76986..ae7b3e8 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -1,109 +1,83 @@ "use client"; -import React from "react"; +import React, { useState, useEffect } from "react"; import Plot from "react-plotly.js"; -import { useSnapshot } from "valtio"; -import state from "@/lib/state"; -import { col2material } from "@/lib/constants"; -import Alert from "@/components/Alert"; +import { csv } from "d3-fetch"; export default function OperatingConditionsDashboard() { - const snap = useSnapshot(state); + const [dataLoaded, setDataLoaded] = useState(false); + const [plotData, setPlotData] = useState([]); + const [errorMessage, setErrorMessage] = useState(""); - if (!snap.dataLoaded) { - return

Loading data...

; - } + useEffect(() => { + // Load the CSV data + csv("/data/temperature_data.csv") + .then((data) => { + const formattedData = []; + const days = data.map((d) => d["Day #"]); + + Object.keys(data[0]).forEach((column) => { + if (column !== "Day #") { + const yData = data.map((d) => parseFloat(d[column]) || null); + const interpolatedYData = interpolateData(yData); // Perform interpolation + + formattedData.push({ + x: days, + y: interpolatedYData, + mode: "lines", + name: column, + }); + } + }); - const class2color = { - "Positive Control": "#70AD47", - "Mixed Materials": "#48646A", - Fiber: "#298FC2", - Biopolymer: "#FFB600", - }; + setPlotData(formattedData); + setDataLoaded(true); + }) + .catch((error) => { + console.error("Error loading CSV data:", error); + setErrorMessage("Failed to load data."); + }); + }, []); - const maxLabelLength = 30; + // Linear interpolation function + function interpolateData(yData) { + let lastValidIndex = null; - function wrapLabel(label) { - const words = label.split(" "); - let wrappedLabel = ""; - let line = ""; + for (let i = 0; i < yData.length; i++) { + if (yData[i] === null) { + // Find the next valid index + const nextValidIndex = yData.slice(i).findIndex((v) => v !== null) + i; - for (const word of words) { - if ((line + word).length > maxLabelLength) { - wrappedLabel += line + "
"; - line = word + " "; + if (lastValidIndex !== null && nextValidIndex < yData.length) { + // Interpolate between the last valid and next valid index + const slope = + (yData[nextValidIndex] - yData[lastValidIndex]) / + (nextValidIndex - lastValidIndex); + yData[i] = yData[lastValidIndex] + slope * (i - lastValidIndex); + } } else { - line += word + " "; + lastValidIndex = i; } } - wrappedLabel += line.trim(); // Add the last line - return wrappedLabel.trim(); + return yData; } - const plotData = - Object.keys(snap.data).length > 0 - ? snap.data.data.map((d) => { - console.log(d); - const materialClass = d["Material Class I"]; - const color = class2color[materialClass] || "#000"; - const countDisplay = - snap.filters["testMethod"] === "Mesh Bag" - ? ` (n=${d["count"]})` - : ""; - // Replace "Positive" with "Pos." in labels and append count - const name = `${d["aggCol"]}${countDisplay}`.replace( - "Positive", - "Pos." - ); - const wrappedName = wrapLabel(name); + const yAxisTitle = "Temperature"; - return { - type: "box", - name: wrappedName, - y: [d.min, d.q1, d.median, d.q3, d.max], - marker: { color }, - boxmean: true, - line: { width: 3.25 }, - }; - }) - : []; - - function generateYAxisTitle(displayCol, cap) { - let yAxisTitle = `${displayCol}`; - if (cap) { - yAxisTitle += " Capped"; - } - return yAxisTitle; - } - const yAxisTitle = generateYAxisTitle( - snap.filters.displayCol, - !snap.filters.uncapResults - ); - - function generateTitle(displayCol, aggCol, num_trials) { - return `${displayCol} by ${col2material[aggCol]} - ${num_trials} Trial(s)`; - } - - const title = generateTitle( - snap.filters.displayCol, - snap.filters.aggCol, - snap.data.numTrials - ); + const title = "Temperature Over Time"; const yMax = - snap.data.data && snap.data.data.length > 0 - ? Math.max(...snap.data.data.map((d) => d.max + 0.05), 1.05) + plotData.length > 0 + ? Math.max(...plotData.flatMap((d) => d.y.map((y) => y + 0.05)), 1.05) : 1.05; const xTickAngle = plotData.length > 6 ? 90 : 0; return ( <> - {snap.errorMessage ? ( + {errorMessage ? (
-

- -

+

{errorMessage}

) : ( ${yAxisTitle}`, }, - tickformat: ".0%", range: [0, yMax], }, xaxis: { From 987e00762d6c5f7f430d80f0ca37a80037b820d0 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 14:48:21 -0500 Subject: [PATCH 07/13] commit the temp data --- .../public/data/temperature_data.csv | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 dashboard-react/public/data/temperature_data.csv diff --git a/dashboard-react/public/data/temperature_data.csv b/dashboard-react/public/data/temperature_data.csv new file mode 100644 index 0000000..4d406ef --- /dev/null +++ b/dashboard-react/public/data/temperature_data.csv @@ -0,0 +1,98 @@ +Day #,WR004-01,CASP005-01,EASP001-01,IV002-01,EASP002-01,CASP006-01,CASP004-02,ASP001-01,EASP003-01,WR005-01 +1,112.8,128.8,92.16333333333334,103.625,89.16666666666667,103.0,172.352,127.8,120.4,146.5 +2,117.0,137.6,133.09,118.25,114.66666666666667,,169.172,133.8,116.2,136.0 +3,125.2,140.8,116.84666666666668,117.875,129.66666666666666,109.0,164.956,136.4,124.2,141.0 +4,128.0,,156.8,118.42857142857143,,,161.138,,,142.0 +5,,,161.97,118.0,,,144.674,,,142.33333333333334 +6,,142.6,140.29999999999998,117.66666666666667,143.66666666666666,137.33333333333334,137.366,142.0,165.4, +7,134.0,143.4,162.1,118.85714285714286,148.66666666666666,138.0,132.35000000000002,144.4,162.0, +8,135.8,144.6,166.95,119.28571428571429,149.66666666666666,141.33333333333334,157.474,146.4,167.2,108.0 +9,136.4,131.2,170.01,121.88888888888889,158.66666666666666,146.66666666666666,155.116,148.0,170.0,107.33333333333333 +10,139.2,156.6,143.47375,120.14285714285714,162.0,151.33333333333334,156.818,148.2,170.4,109.33333333333333 +11,135.8,,169.03,119.57142857142857,,,128.574,,,110.33333333333333 +12,,,169.77,119.14285714285714,,,115.68199999999999,,,108.0 +13,,163.0,140.04375,118.57142857142857,160.0,,122.04,140.2,165.4,109.0 +14,138.2,164.6,171.12,117.85714285714286,161.66666666666666,133.0,127.976,141.0,164.8, +15,135.8,166.0,134.94375,117.57142857142857,162.66666666666666,132.33333333333334,118.00199999999998,142.6,166.6,126.0 +16,136.6,166.2,172.48,117.57142857142857,161.66666666666666,136.66666666666666,127.91400000000002,,165.2,129.0 +17,135.8,168.2,138.24625,119.0,161.66666666666666,,117.50600000000001,142.2,160.2,144.83333333333334 +18,136.6,,167.4,117.28571428571429,,,126.46400000000001,,,133.66666666666666 +19,,,166.56,118.14285714285714,,,117.556,,,148.16666666666666 +20,,,127.5975,117.71428571428571,160.83333333333334,133.33333333333334,122.648,142.6,153.8, +21,136.4,150.6,170.6,118.57142857142857,159.0,137.5,131.92999999999998,155.2,, +22,137.2,150.8,127.2025,141.85714285714286,157.33333333333334,129.4,117.75,155.4,152.8, +23,139.6,153.6,168.09,141.14285714285714,135.0,131.8,122.6,156.8,, +24,138.8,155.6,123.42857142857143,143.57142857142858,140.33333333333334,,136.218,155.8,148.2,140.33333333333334 +25,136.0,,165.61,144.14285714285714,,,117.458,,, +26,,,164.11,145.28571428571428,,,119.94200000000001,,, +27,,160.8,162.11,146.85714285714286,157.33333333333334,156.33333333333334,124.796,156.2,159.2, +28,136.6,163.4,118.28571428571429,148.57142857142858,158.66666666666666,154.0,134.12199999999999,157.0,156.6, +29,136.0,165.6,118.57142857142857,148.71428571428572,158.33333333333334,154.66666666666666,123.418,,156.0,140.0 +30,135.6,167.8,130.28571428571428,151.0,156.66666666666666,153.33333333333334,116.53600000000002,162.0,,147.0 +31,133.8,169.0,130.28571428571428,149.85714285714286,157.66666666666666,157.0,117.93800000000002,,155.8,153.16666666666666 +32,135.0,,158.16,135.14285714285714,,,119.39000000000001,,, +33,,,157.88,137.85714285714286,,,121.502,,,148.83333333333334 +34,,169.6,129.42857142857142,142.42857142857142,157.0,,124.15599999999999,154.8,, +35,,169.8,130.57142857142858,151.85714285714286,158.0,153.0,127.25399999999999,151.6,, +36,135.3,169.8,131.14285714285714,148.28571428571428,157.0,145.5,141.254,151.6,149.2, +37,135.1,168.0,154.04,150.0,160.33333333333334,151.66666666666666,127.704,151.6,, +38,135.2,169.2,126.85714285714286,157.14285714285714,158.33333333333334,,117.78000000000002,,,151.83333333333334 +39,135.2,,153.07,151.14285714285714,,,117.09400000000001,,, +40,,,152.23,150.0,,,116.36800000000001,,, +41,,,126.85714285714286,150.71428571428572,157.33333333333334,148.66666666666666,116.23799999999999,149.0,141.2, +42,135.0,168.4,149.89,158.42857142857142,159.0,144.0,116.96,145.2,, +43,134.9,169.0,125.42857142857143,158.28571428571428,153.66666666666666,152.0,110.06199999999998,,135.8,142.66666666666666 +44,136.2,170.2,150.0,157.85714285714286,150.33333333333334,150.66666666666666,110.828,147.2,167.4, +45,136.8,169.6,125.14285714285714,159.0,153.0,,113.33599999999998,151.0,166.6,148.16666666666666 +46,128.4,,,158.42857142857142,,,117.98400000000001,,,153.33333333333334 +47,,,,159.28571428571428,,166.0,119.974,,, +48,,,,160.85714285714286,148.66666666666666,164.0,121.99600000000001,,167.6, +49,124.4,,,159.28571428571428,154.33333333333334,160.66666666666666,130.964,,167.6, +50,124.9,,,159.71428571428572,118.33333333333333,,148.8,,171.8, +51,123.7,,,,128.66666666666666,,140.8,,167.6,143.16666666666666 +52,122.2,,,,131.0,,149.0,,168.0, +53,117.8,,,,,,145.6,,, +54,,,,,,,148.0,,, +55,,,,,150.66666666666666,160.33333333333334,149.2,,168.0, +56,114.3,,,,154.0,151.66666666666666,144.0,,, +57,110.7,,,,156.0,157.0,155.8,,168.2,151.33333333333334 +58,114.4,,,,158.33333333333334,157.33333333333334,156.8,,169.0,148.66666666666666 +59,112.4,,,,160.33333333333334,,160.2,,168.2,146.83333333333334 +60,109.4,,,,,,159.8,,,146.5 +61,,,,,,157.33333333333334,159.8,,,144.83333333333334 +62,,,,,161.0,158.0,161.0,,165.6, +63,105.1,,,,160.33333333333334,163.0,152.8,,, +64,106.9,,,,156.33333333333334,,148.4,,167.8,146.66666666666666 +65,102.3,,,,157.0,,148.0,,168.4, +66,101.7,,,,156.33333333333334,,152.8,,166.4,154.0 +67,96.3,,,,,,145.6,,,153.33333333333334 +68,,,,,,154.66666666666666,,,, +69,,,,,156.66666666666666,154.0,,,166.6, +70,,,,,130.33333333333334,153.0,,,, +71,,,,,133.0,153.0,,,,148.33333333333334 +72,,,,,137.0,,,,, +73,,,,,142.0,,,,,151.16666666666666 +74,,,,,,,,,, +75,,,,,,150.0,,,,144.66666666666666 +76,,,,,,149.33333333333334,,,, +77,,,,,,150.5,,,, +78,,,,,140.83333333333334,149.52666666666667,,,, +79,,,,,141.33333333333334,151.21,,,, +80,,,,,141.33333333333334,149.6,,,,150.66666666666666 +81,,,,,,150.24,,,, +82,,,,,,148.51333333333332,,,, +83,,,,,140.33333333333334,147.95000000000002,,,, +84,,,,,140.0,149.41333333333333,,,, +85,,,,,139.66666666666666,147.42333333333332,,,,143.33333333333334 +86,,,,,140.33333333333334,147.53666666666666,,,, +87,,,,,139.66666666666666,,,,,140.66666666666666 +88,,,,,,,,,, +89,,,,,,,,,,141.66666666666666 +90,,,,,140.0,,,,, +91,,,,,,,,,, +92,,,,,,,,,, +93,,,,,,,,,,143.66666666666666 +94,,,,,,,,,,150.33333333333334 +95,,,,,,,,,,150.0 +96,,,,,,,,,, +97,,,,,,,,,, From 23ff1be24dc148dd4c9d8e9eab7f3323703fcb0f Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 16:20:08 -0500 Subject: [PATCH 08/13] cap x-axis --- dashboard-react/components/OperatingConditionsDashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index ae7b3e8..27437aa 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -3,13 +3,12 @@ import React, { useState, useEffect } from "react"; import Plot from "react-plotly.js"; import { csv } from "d3-fetch"; -export default function OperatingConditionsDashboard() { +export default function OperatingConditionsDashboard({ maxDays = 45 }) { const [dataLoaded, setDataLoaded] = useState(false); const [plotData, setPlotData] = useState([]); const [errorMessage, setErrorMessage] = useState(""); useEffect(() => { - // Load the CSV data csv("/data/temperature_data.csv") .then((data) => { const formattedData = []; @@ -102,6 +101,7 @@ export default function OperatingConditionsDashboard() { tickangle: xTickAngle, ticklen: 10, automargin: true, + range: [0, maxDays], }, hovermode: "x", }} From 1dce733220d27409d745730e1d2a432da2a8c8b5 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 16:21:56 -0500 Subject: [PATCH 09/13] add a 10 day moving average --- .../OperatingConditionsDashboard.js | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index 27437aa..0ae9ddd 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -3,7 +3,11 @@ import React, { useState, useEffect } from "react"; import Plot from "react-plotly.js"; import { csv } from "d3-fetch"; -export default function OperatingConditionsDashboard({ maxDays = 45 }) { +export default function OperatingConditionsDashboard({ + maxDays = 45, + windowSize = 10, +}) { + // Add windowSize as a prop const [dataLoaded, setDataLoaded] = useState(false); const [plotData, setPlotData] = useState([]); const [errorMessage, setErrorMessage] = useState(""); @@ -16,12 +20,13 @@ export default function OperatingConditionsDashboard({ maxDays = 45 }) { Object.keys(data[0]).forEach((column) => { if (column !== "Day #") { - const yData = data.map((d) => parseFloat(d[column]) || null); - const interpolatedYData = interpolateData(yData); // Perform interpolation + let yData = data.map((d) => parseFloat(d[column]) || null); + yData = interpolateData(yData); // Perform interpolation + yData = movingAverage(yData, windowSize); // Smooth using moving average formattedData.push({ x: days, - y: interpolatedYData, + y: yData, mode: "lines", name: column, }); @@ -35,7 +40,7 @@ export default function OperatingConditionsDashboard({ maxDays = 45 }) { console.error("Error loading CSV data:", error); setErrorMessage("Failed to load data."); }); - }, []); + }, [windowSize]); // Linear interpolation function function interpolateData(yData) { @@ -61,6 +66,18 @@ export default function OperatingConditionsDashboard({ maxDays = 45 }) { return yData; } + // Moving average function + function movingAverage(data, windowSize) { + return data.map((_, idx, arr) => { + const start = Math.max(0, idx - Math.floor(windowSize / 2)); + const end = Math.min(arr.length, idx + Math.ceil(windowSize / 2)); + const window = arr.slice(start, end); + const validNumbers = window.filter((n) => n !== null); // Ignore nulls + const sum = validNumbers.reduce((acc, num) => acc + num, 0); + return validNumbers.length > 0 ? sum / validNumbers.length : null; + }); + } + const yAxisTitle = "Temperature"; const title = "Temperature Over Time"; @@ -101,7 +118,7 @@ export default function OperatingConditionsDashboard({ maxDays = 45 }) { tickangle: xTickAngle, ticklen: 10, automargin: true, - range: [0, maxDays], + range: [0, maxDays], // Cap x-axis at maxDays }, hovermode: "x", }} From 9b49879f4d1091fd484175f1af2a119c25c0e3e8 Mon Sep 17 00:00:00 2001 From: toddnief Date: Tue, 20 Aug 2024 16:28:02 -0500 Subject: [PATCH 10/13] smooth curves with a moving average and fix axis display issues --- .../components/OperatingConditionsDashboard.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index 0ae9ddd..1c56519 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -68,13 +68,19 @@ export default function OperatingConditionsDashboard({ // Moving average function function movingAverage(data, windowSize) { - return data.map((_, idx, arr) => { + return data.map((value, idx, arr) => { + // Ignore null values + if (value === null) return null; + const start = Math.max(0, idx - Math.floor(windowSize / 2)); const end = Math.min(arr.length, idx + Math.ceil(windowSize / 2)); const window = arr.slice(start, end); - const validNumbers = window.filter((n) => n !== null); // Ignore nulls + const validNumbers = window.filter((n) => n !== null); + + if (validNumbers.length === 0) return null; + const sum = validNumbers.reduce((acc, num) => acc + num, 0); - return validNumbers.length > 0 ? sum / validNumbers.length : null; + return sum / validNumbers.length; }); } @@ -113,12 +119,14 @@ export default function OperatingConditionsDashboard({ text: `${yAxisTitle}`, }, range: [0, yMax], + linewidth: 2, // Set y-axis line thickness }, xaxis: { tickangle: xTickAngle, ticklen: 10, automargin: true, range: [0, maxDays], // Cap x-axis at maxDays + linewidth: 2, // Set x-axis line thickness }, hovermode: "x", }} From 74c86111f3fc52028f0cea2e2a24cad5b6cb2109 Mon Sep 17 00:00:00 2001 From: toddnief Date: Wed, 21 Aug 2024 13:18:21 -0500 Subject: [PATCH 11/13] map trial names to technology --- .../OperatingConditionsDashboard.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index 1c56519..c8cda6a 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -28,7 +28,7 @@ export default function OperatingConditionsDashboard({ x: days, y: yData, mode: "lines", - name: column, + name: mapTrialName(column), // Use mapTrialName to format the legend name }); } }); @@ -84,6 +84,24 @@ export default function OperatingConditionsDashboard({ }); } + const mapTrialName = (trialName) => { + const mappings = { + IV: "In-Vessel", + CASP: "Covered Aerated Static Pile", + WR: "Windrow", + EASP: "Extended Aerated Static Pile", + ASP: "Aerated Static Pile", + AD: "Anaerobic Digestion", + }; + + // Extract the prefix (e.g., IV, CASP, etc.) and the rest of the trial name + const prefix = trialName.match(/^[A-Z]+/)[0]; // Extracts the prefix + const rest = trialName.replace(prefix, ""); // Removes the prefix + + // Map the prefix and return the formatted name + return mappings[prefix] ? `${mappings[prefix]}: ${rest}` : trialName; + }; + const yAxisTitle = "Temperature"; const title = "Temperature Over Time"; From 00998d3973795ad98f39acf4d4c9804732cb68b4 Mon Sep 17 00:00:00 2001 From: toddnief Date: Thu, 22 Aug 2024 20:54:32 -0500 Subject: [PATCH 12/13] update h2 for filter headers --- dashboard-react/app/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard-react/app/globals.css b/dashboard-react/app/globals.css index 3a4b2e8..298654c 100644 --- a/dashboard-react/app/globals.css +++ b/dashboard-react/app/globals.css @@ -27,7 +27,7 @@ body { } h2 { - @apply text-2xl font-bold uppercase text-center text-primary underline; + @apply text-3xl font-bold uppercase text-center underline; } h3 { From f89cf1e84216d622d52c5303d6bbcb9904e763c5 Mon Sep 17 00:00:00 2001 From: toddnief Date: Thu, 22 Aug 2024 21:02:13 -0500 Subject: [PATCH 13/13] number trials on operating conditions dash --- .../OperatingConditionsDashboard.js | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/dashboard-react/components/OperatingConditionsDashboard.js b/dashboard-react/components/OperatingConditionsDashboard.js index c8cda6a..e2ddf8d 100644 --- a/dashboard-react/components/OperatingConditionsDashboard.js +++ b/dashboard-react/components/OperatingConditionsDashboard.js @@ -18,17 +18,21 @@ export default function OperatingConditionsDashboard({ const formattedData = []; const days = data.map((d) => d["Day #"]); + const trialCount = {}; // Reset trial count each time data is processed + Object.keys(data[0]).forEach((column) => { if (column !== "Day #") { let yData = data.map((d) => parseFloat(d[column]) || null); yData = interpolateData(yData); // Perform interpolation yData = movingAverage(yData, windowSize); // Smooth using moving average + const trialName = mapTrialName(column, trialCount); // Pass trialCount to mapTrialName + formattedData.push({ x: days, y: yData, mode: "lines", - name: mapTrialName(column), // Use mapTrialName to format the legend name + name: trialName, // Use the mapped trial name }); } }); @@ -42,6 +46,37 @@ export default function OperatingConditionsDashboard({ }); }, [windowSize]); + const mapTrialName = (trialName, trialCount) => { + const mappings = { + IV: "In-Vessel", + CASP: "Covered Aerated Static Pile", + WR: "Windrow", + EASP: "Extended Aerated Static Pile", + ASP: "Aerated Static Pile", + AD: "Anaerobic Digestion", + }; + + // Extract the prefix (e.g., IV, CASP, etc.) + const prefix = trialName.match(/^[A-Z]+/)[0]; + + // Get the mapped name for the prefix + const mappedName = mappings[prefix]; + + if (mappedName) { + // Initialize the count for this trial type if it doesn't exist + if (!trialCount[mappedName]) { + trialCount[mappedName] = 0; + } + // Increment the count for this trial type + trialCount[mappedName] += 1; + + // Return the formatted name with the count + return `${mappedName} #${trialCount[mappedName]}`; + } + + return trialName; // Return the original trial name if the prefix is not recognized + }; + // Linear interpolation function function interpolateData(yData) { let lastValidIndex = null; @@ -84,24 +119,6 @@ export default function OperatingConditionsDashboard({ }); } - const mapTrialName = (trialName) => { - const mappings = { - IV: "In-Vessel", - CASP: "Covered Aerated Static Pile", - WR: "Windrow", - EASP: "Extended Aerated Static Pile", - ASP: "Aerated Static Pile", - AD: "Anaerobic Digestion", - }; - - // Extract the prefix (e.g., IV, CASP, etc.) and the rest of the trial name - const prefix = trialName.match(/^[A-Z]+/)[0]; // Extracts the prefix - const rest = trialName.replace(prefix, ""); // Removes the prefix - - // Map the prefix and return the formatted name - return mappings[prefix] ? `${mappings[prefix]}: ${rest}` : trialName; - }; - const yAxisTitle = "Temperature"; const title = "Temperature Over Time";