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

Dev #69

Merged
merged 14 commits into from
Aug 23, 2024
67 changes: 26 additions & 41 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
}
2 changes: 1 addition & 1 deletion dashboard-react/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions dashboard-react/app/operating-conditions/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"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 (
<main className="flex flex-col items-start">
<div className="block lg:hidden p-2 h-[100vh] flex items-center align-center justify-center">
Please use a device that is at least 1280 pixels wide to view the
disintegration dashboard.
</div>
<div className="hidden lg:block h-[1300px] w-[1280px] overflow-hidden">
<div className="h-[600px] mx-auto mb-3">
<OperatingConditionsDashboard />
</div>
{/*
<div className="h-[700px] mx-auto w-full">
<DashboardControls />
</div>
*/}
</div>
</main>
);
}
2 changes: 1 addition & 1 deletion dashboard-react/components/DashboardDisplayControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function DashboardDisplayControls() {

return (
<>
<div className="flex justify-center">
<div className="flex justify-center mb-4">
<h2>Display Options</h2>
</div>
<RadioSingleSelect
Expand Down
175 changes: 175 additions & 0 deletions dashboard-react/components/OperatingConditionsDashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";
import React, { useState, useEffect } from "react";
import Plot from "react-plotly.js";
import { csv } from "d3-fetch";

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("");

useEffect(() => {
csv("/data/temperature_data.csv")
.then((data) => {
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: trialName, // Use the mapped trial name
});
}
});

setPlotData(formattedData);
setDataLoaded(true);
})
.catch((error) => {
console.error("Error loading CSV data:", error);
setErrorMessage("Failed to load data.");
});
}, [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;

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;

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 {
lastValidIndex = i;
}
}

return yData;
}

// Moving average function
function movingAverage(data, windowSize) {
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);

if (validNumbers.length === 0) return null;

const sum = validNumbers.reduce((acc, num) => acc + num, 0);
return sum / validNumbers.length;
});
}

const yAxisTitle = "Temperature";

const title = "Temperature Over Time";

const yMax =
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 (
<>
{errorMessage ? (
<div className="flex items-center justify-center h-full mx-[200px]">
<p>{errorMessage}</p>
</div>
) : (
<Plot
data={plotData}
layout={{
width: 1280,
height: 600,
title: {
text: `<b>${title}</b>`,
x: 0.5,
xanchor: "center",
yanchor: "top",
},
showlegend: true,
yaxis: {
title: {
text: `<b>${yAxisTitle}</b>`,
},
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",
}}
config={{
displayModeBar: false,
}}
/>
)}
</>
);
}
Loading
Loading