diff --git a/.changeset/great-lies-sink.md b/.changeset/great-lies-sink.md
new file mode 100644
index 00000000..d468fefd
--- /dev/null
+++ b/.changeset/great-lies-sink.md
@@ -0,0 +1,5 @@
+---
+"@usace-watermanagement/groundwork-water": minor
+---
+
+Added A2W Dam Profile Chart to component library
diff --git a/docs/package-lock.json b/docs/package-lock.json
index aa3e5095..5acd4528 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -569,9 +569,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -589,9 +586,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -609,9 +603,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -629,9 +620,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -649,9 +637,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -669,9 +654,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3740,9 +3722,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3764,9 +3743,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3788,9 +3764,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -3812,9 +3785,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
diff --git a/docs/src/bundles/route-bundle.js b/docs/src/bundles/route-bundle.js
index 4877fa6d..013f8c3f 100644
--- a/docs/src/bundles/route-bundle.js
+++ b/docs/src/bundles/route-bundle.js
@@ -49,6 +49,8 @@ import CWMSSpreadsheetDocs from "../pages/docs/forms/cwms-spreadsheet";
import InteractiveFormTest from "../pages/docs/forms/interactive-test";
import { docsConfig } from "../config/docs.config";
import GetPrecisionPage from "../pages/docs/utilities/get-precision";
+import DamProfileDocs from "../pages/docs/graphics/dam-profile";
+import GraphicsDocs from "../pages/docs/graphics";
import DataStatus from "../pages/docs/summary/data-status";
export default createRouteBundle(
@@ -79,6 +81,8 @@ export default createRouteBundle(
"/docs/hooks/use-cda-blobs": useCdaBlobs,
"/docs/plots": PlotsDocs,
"/docs/plots/cwms-plot": CWMSPlotDocs,
+ "/docs/graphics": GraphicsDocs,
+ "/docs/graphics/dam-profile": DamProfileDocs,
"/docs/plots/basin-pie": BasinPieDocs,
"/docs/maps": Maps,
"/docs/tables": Tables,
diff --git a/docs/src/nav-links.js b/docs/src/nav-links.js
index 4576d989..89e6e352 100644
--- a/docs/src/nav-links.js
+++ b/docs/src/nav-links.js
@@ -171,6 +171,18 @@ export default [
},
],
},
+ {
+ id: "graphics",
+ text: "Graphics",
+ href: `${BASE_URL}#/docs/graphics`,
+ children: [
+ {
+ id: "dam-profile",
+ text: "Dam Profile",
+ href: `${BASE_URL}#/docs/graphics/dam-profile`,
+ },
+ ],
+ },
{
id: "forms",
text: "Forms",
diff --git a/docs/src/pages/docs/graphics/dam-profile.jsx b/docs/src/pages/docs/graphics/dam-profile.jsx
new file mode 100644
index 00000000..ae12de55
--- /dev/null
+++ b/docs/src/pages/docs/graphics/dam-profile.jsx
@@ -0,0 +1,98 @@
+import { UsaceBox, Code, H3, Text, Badge } from "@usace/groundwork";
+import { DamProfile } from "@usace-watermanagement/groundwork-water";
+import { Code as CodeBlock } from "../../components/code.jsx";
+import Divider from "../../components/divider.jsx";
+import DocsPage from "../_docs-wrapper.jsx";
+import PropsTable from "../../components/props-table.jsx";
+import chartProps from "../../../props-declarations/chart.jsx";
+
+function DamProfileDocs() {
+ return (
+
+
+
+ The Dam Profile is a graphic for displaying current levels and timeseries
+ values of a project.
+
+
+ The graphic intakes an object of values and renders the chart. Chart can be
+ customized to draw/omit specific sections by providing specific props.
+
+
+
+
+
+
+
+ {`import { DamProfile } from "@usace-watermanagement/groundwork-water";
+
+
+`}
+
+
+
+
+ Component API - {` `}
+
+
+
+ );
+}
+
+export default DamProfileDocs;
diff --git a/docs/src/pages/docs/graphics/index.jsx b/docs/src/pages/docs/graphics/index.jsx
new file mode 100644
index 00000000..70682b07
--- /dev/null
+++ b/docs/src/pages/docs/graphics/index.jsx
@@ -0,0 +1,39 @@
+import { Link, Text } from "@usace/groundwork";
+import { H4 } from "@usace/groundwork";
+import DocsPage from "../_docs-wrapper";
+
+const BASE_URL = import.meta.env.BASE_URL;
+
+function GraphicsDocs() {
+ return (
+
+
+
+ The groundwork-water graphics components are intended to create
+ consistently-styled water management graphics without being opinonated on the
+ source of the data,
+
+
+ Components
+
+
+
+ Dam Profile Graphic - A graphic for displaying levels and timeseries values
+ at a project.
+
+
+
+
+ ``
+
+ );
+}
+
+export { GraphicsDocs as DataHooks };
+export default GraphicsDocs;
diff --git a/docs/src/pages/docs/summary/data-status.jsx b/docs/src/pages/docs/summary/data-status.jsx
index fadc45d1..173b20e5 100644
--- a/docs/src/pages/docs/summary/data-status.jsx
+++ b/docs/src/pages/docs/summary/data-status.jsx
@@ -138,7 +138,7 @@ function DataStatusPage() {
office="SWT"
tsids={swtTsids}
pageSize={12}
- // linkPath="/SWT/projects"
+ // linkPath="/SWT/projects"
lookBackHours={6}
dateFormat="MMM DD HH:mm z"
showBadges={true}
diff --git a/docs/src/props-declarations/chart.jsx b/docs/src/props-declarations/chart.jsx
new file mode 100644
index 00000000..cac403ef
--- /dev/null
+++ b/docs/src/props-declarations/chart.jsx
@@ -0,0 +1,88 @@
+const chartProps = [
+ {
+ name: "damBottom",
+ type: "number",
+ default: "undefined",
+ desc: "The level at which the bottom of the dam will be drawn. Usually Elev.Streambed or Stage.Streambed",
+ },
+ {
+ name: "damTop",
+ type: "number",
+ default: "undefined",
+ desc: "The level at which the top of the dam will be drawn. Usually Elev.Top of Dam or Stage.Top of Dam",
+ },
+ {
+ name: "gradientBottom",
+ type: "number",
+ default: "undefined",
+ desc: "The level at which the bottom of the dam gradient will be drawn. Usually Elev.Bottom of Flood or Elev.Bottom of Flood Control or Elev.Top of Conservation",
+ },
+ {
+ name: "gradientTop",
+ type: "number",
+ default: "undefined",
+ desc: "The level at which the top of the dam gradient will be drawn. Usually Elev.Top of Flood or Elev.Top of Flood Control",
+ },
+ {
+ name: "inflow",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the inflow timeseries.",
+ },
+ {
+ name: "outflow",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the outflow timeseries.",
+ },
+ {
+ name: "pool",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the elevation or stage timeseries.",
+ },
+ {
+ name: "powerGeneration",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the power generation timeseries. Determines if the turbine graphic is drawn.",
+ },
+ {
+ name: "surcharge",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the surcharge release timeseries.",
+ },
+ {
+ name: "tail",
+ type: "number",
+ default: "undefined",
+ desc: "The latest value of the Elevation Tailwater or Stage Tailwater timeseries.",
+ },
+ {
+ name: "infoText",
+ type: "string",
+ default: "undefined",
+ desc: "The name of project",
+ },
+ {
+ name: "latestTime",
+ type: "string",
+ default: "undefined",
+ desc: "The latest time from the Elevation or Stage timeseries",
+ },
+ {
+ name: "levels",
+ type: "array",
+ default: "undefined",
+ desc: "Array of levels to be drawn on the dam Y axis.",
+ },
+ {
+ name: "outflowTotals",
+ type: "array",
+ default: "undefined",
+ desc: "Array of outflow types to be drawn on below the Outflow Icon.",
+ },
+];
+
+export default chartProps;
diff --git a/lib/components/data/plots/dam-profile/chart.jsx b/lib/components/data/plots/dam-profile/chart.jsx
new file mode 100644
index 00000000..329f770d
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/chart.jsx
@@ -0,0 +1,24 @@
+import { createRef, useEffect } from "react";
+import { useConnect } from "redux-bundler-hook";
+import DamProfileChart from "./dam-profile-chart/dam-profile-chart";
+
+function DamProfile({ info }) {
+ const ref = createRef(); // element where DamProfileChart will be rendered
+
+ useEffect(() => {
+ DamProfileChart(info, ref.current);
+ });
+
+ return (
+
+
+
+ );
+}
+
+export default DamProfile;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/dam-profile-chart.js b/lib/components/data/plots/dam-profile/dam-profile-chart/dam-profile-chart.js
new file mode 100644
index 00000000..618567be
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/dam-profile-chart.js
@@ -0,0 +1,160 @@
+import * as d3 from "d3";
+
+import Dam from "./elements/Dam.js";
+import Gradient from "./elements/Gradient.js";
+import InflowIcon from "./elements/InflowIcon.js";
+import LeftAxis from "./elements/LeftAxis.js";
+import Legend from "./elements/Legend.js";
+import Mountain from "./elements/Mountain.js";
+import OutflowIcon from "./elements/OutflowIcon.js";
+import SurchargeIcon from "./elements/SurchargeIcon.js";
+import WaterLevel from "./elements/WaterLevel.js";
+import TailwaterLevel from "./elements/TailwaterLevel.js";
+import Levels from "./elements/Levels.js";
+import Turbine from "./elements/Turbine.js";
+import Info from "./elements/Info.js";
+import noLockTurbine from "./elements/NoLockTurbine.js";
+import Precipitation from "./elements/Precipitation.js";
+
+export default function DamProfileChart(info, dom) {
+ if (!info) return null;
+ const {
+ infoText,
+ pool,
+ tail,
+ inflow,
+ outflow,
+ surcharge,
+ powerGeneration,
+ damBottom,
+ damTop,
+ height = undefined,
+ gradientBottom,
+ gradientTop,
+ browser,
+ levels,
+ latestTime,
+ precip,
+ outflowTotals,
+ } = info;
+ ////////////
+ // D3 SCRIPT
+ ////////////
+
+ // var margin = { top: 50, right: 50, bottom: 50, left: 50 };
+ // var width = window.innerWidth - margin.left - margin.right; // Use the window's width
+ // var height = window.innerHeight - margin.top - margin.bottom; // Use the window's height
+ // var cWidth = 1240;
+ // var cHeight = 650;
+
+ // Dam Scale
+ const damScale = d3.scaleLinear().domain([damTop, damBottom]).range([130, 560]);
+ // Build SVG; Add to Chart
+ var svg = d3.select(dom);
+
+ // Clear any InnerHTML in the case that this function has already run against the provided dom
+ svg.selectChildren().remove();
+
+ // @todo; see if this is necessary
+ if (height !== undefined) {
+ svg.attr("height", height);
+ }
+
+ // Handle IE
+ if (browser && browser.toUpperCase() === "IE") {
+ svg.attr("style", "min-height: 660px");
+ }
+
+ svg.append("g").classed("svg-content-responsive", true);
+ svg.append("defs");
+
+ //////////////////////////
+ // create line on the left
+ // replaces drawTicks()
+ //////////////////////////
+ LeftAxis(svg, damBottom, damTop);
+
+ ////////////////////////////
+ // Draw Water Level
+ // replaces drawWaterLevel()
+ ////////////////////////////
+ pool && WaterLevel(svg, damScale, pool);
+ // display tailwater component regardless of value set or not
+ TailwaterLevel(svg, damScale, tail);
+
+ //////////////////////////
+ // create center dam
+ // Also includes code
+ // previously in noLock()
+ //////////////////////////
+ Dam(svg);
+
+ ////////////////////////////
+ // Create Legend
+ // replaces createLegend
+ ////////////////////////////
+ Legend(svg);
+
+ ////////////////////////////
+ // Icons
+ ////////////////////////////
+ InflowIcon(svg, inflow);
+ OutflowIcon(svg, outflow, outflowTotals, { x: 700, y: 360 });
+ surcharge && surcharge > 0 && SurchargeIcon(svg, surcharge);
+
+ ////////////////////////////
+ // Draw Mountain
+ // replaces drawMountain
+ ////////////////////////////
+ Mountain(svg);
+ precip && precip >= 0 && Precipitation(svg, precip);
+ // @todo
+ // need to abstract this concept into multiple visualizations
+ // if (options.hasLock) {
+ // createLock();
+ // drawBoat();
+ // } else {
+ // if (options.hasTurbine) {
+ // noLockTurbine();
+ // } else {
+ // noLock();
+ // }
+ // }
+
+ // @todo refactor turbine logic
+ // if (options.hasTurbine) {
+ // createTurbine();
+ // }
+
+ // Note: PowerGeneration can be zero and we still want to show the turbine
+ if (powerGeneration !== null && powerGeneration !== undefined) {
+ noLockTurbine(svg);
+ Turbine(svg);
+ }
+
+ //////////////////////////////////////
+ // Middle Gradient
+ // replaces createMiddleGradient(mode)
+ // @todo; Get this working
+ // @todo; Confirm desired behavior of
+ // drawn gradient scale
+ // (red, yellow, green)
+ //////////////////////////////////////
+ if (!isNaN(gradientBottom) || !isNaN(gradientTop)) {
+ Gradient(svg, damScale, gradientBottom, gradientTop);
+ }
+
+ Levels(svg, damScale, damBottom, damTop, levels);
+
+ const infoDate = new Date(latestTime).toLocaleDateString("en-US", {
+ hour: "numeric",
+ minute: "numeric",
+ hour12: true,
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZoneName: "longGeneric",
+ });
+
+ Info(svg, `${infoText} - ${infoDate}`);
+}
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Dam.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Dam.js
new file mode 100644
index 00000000..5f2265a7
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Dam.js
@@ -0,0 +1,47 @@
+import { straightLine } from "./Lines.js";
+
+const Dam = (svg) => {
+ svg.append("g").attr("class", "dam");
+ // Dam Embankment
+ svg
+ .select("g.dam")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [410, 561],
+ [410, 150],
+ [390, 150],
+ [390, 130],
+ [510, 130],
+ [510, 150],
+ [490, 170],
+ [490, 210],
+ [560, 440],
+ [610, 440],
+ [610, 561],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3")
+ .attr("stroke-linejoin", "bevel");
+
+ // Dam Bottom
+ svg
+ .select("g.dam")
+ .append("path")
+ .attr("id", "bottom")
+ .attr(
+ "d",
+ straightLine([
+ [239, 560],
+ [611, 560],
+ [611, 590],
+ [239, 590],
+ ]),
+ )
+ .attr("fill", "#B3B3B3");
+};
+
+export default Dam;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Gradient.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Gradient.js
new file mode 100644
index 00000000..0cf2bb31
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Gradient.js
@@ -0,0 +1,106 @@
+import * as d3 from "d3";
+import { straightLine } from "./Lines.js";
+
+const Gradient = (svg, damScale, min, max) => {
+ // short circuit
+ if (!damScale || !min || !max) {
+ return;
+ }
+ // console.log('--Gradient--');
+ // console.log(`svg: ${svg}`);
+ // console.log(`damScale: ${damScale}`);
+ // console.log(`min: ${min}`);
+ // console.log(`max: ${max}`);
+ // console.log('---');
+ const colors = [
+ { level: 0.0, color: "red" },
+ { level: 0.2, color: "yellow" },
+ { level: 0.3, color: "yellow" },
+ { level: 0.4, color: "green" },
+ ];
+ const labels = [100, 80, 60, 40, 20, 0];
+
+ if (!isNaN(max) && !isNaN(min)) {
+ const bottom = damScale(min);
+ const top = damScale(max);
+ // gradientScale:
+ // returns pixel space values, given a real dam value
+ const gradientScale = d3.scaleOrdinal(
+ labels, // domain
+ [bottom, top], // range
+ );
+ const height = bottom - top;
+
+ // Set Axis on Right Side
+ d3.axisRight(gradientScale);
+
+ //create the actual gradient with green: 60%, yellow: 60-75%, and red: 85-100%
+ var middleGradient = svg
+ .select("defs")
+ .append("linearGradient")
+ .attr("id", "MiddleGradient")
+ .attr("x1", "0%")
+ .attr("x2", "0%")
+ .attr("y1", "0%")
+ .attr("y2", "100%");
+
+ colors.forEach((c) => {
+ middleGradient
+ .append("stop")
+ .attr("offset", c.level * 100 + "%")
+ .attr("stop-color", c.color);
+ });
+
+ svg
+ .append("g")
+ .attr("class", "middleGradient")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [445, top],
+ [460, top],
+ [460, bottom],
+ [445, bottom],
+ [445, top],
+ ]),
+ )
+ .attr("fill", "url(#MiddleGradient)");
+
+ //create percentage marks
+ if (Math.abs(top - bottom) > 40) {
+ // Add All Labels
+ labels.forEach((d, i) => {
+ svg
+ .select("g.middleGradient")
+ .append("text")
+ .attr("x", 465)
+ .attr("y", top + 5 + i * (height / 5))
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "12px")
+ .text(d + "%");
+ });
+ } else {
+ // Add First And Last Label
+ svg
+ .select("g.middleGradient")
+ .append("text")
+ .attr("x", 465)
+ .attr("y", top + 5)
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "12px")
+ .text(labels[0] + "%");
+
+ svg
+ .select("g.middleGradient")
+ .append("text")
+ .attr("x", 465)
+ .attr("y", top + 5 + (labels.length - 1) * (height / 5))
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "12px")
+ .text(labels[labels.length - 1] + "%");
+ }
+ }
+};
+
+export default Gradient;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/InflowIcon.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/InflowIcon.js
new file mode 100644
index 00000000..971a2ad9
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/InflowIcon.js
@@ -0,0 +1,71 @@
+import { straightLine } from "./Lines.js";
+
+const InflowIcon = (svg, inflow) => {
+ svg
+ .append("g")
+ .attr("class", "inflowIcon")
+ .append("circle")
+ .attr("r", 25)
+ .attr("cx", 320)
+ .attr("cy", 60)
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.inflowIcon")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [315, 70],
+ [315, 100],
+ [300, 100],
+ [320, 120],
+ [340, 100],
+ [325, 100],
+ [325, 70],
+ ]),
+ )
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.inflowIcon")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [60, 58],
+ [320, 58],
+ [320, 65],
+ [60, 65],
+ ]),
+ )
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.inflowIcon")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", 320)
+ .attr("dy", 67)
+ .attr("fill", "#fff")
+ .attr("font-family", "sans-serif")
+ .text("IN");
+
+ // inflow text label
+ svg
+ .append("g")
+ .attr("class", "inflowLabel")
+ .append("text")
+ .attr("text-anchor", "start")
+ .attr("dx", 350)
+ .attr("dy", 65)
+ .attr("class", "inflowText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.3em")
+ .text(
+ inflow !== null && !isNaN(inflow)
+ ? inflow?.toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ }) + " cfs"
+ : "Not Available",
+ );
+};
+
+export default InflowIcon;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Info.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Info.js
new file mode 100644
index 00000000..a88bdb6b
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Info.js
@@ -0,0 +1,16 @@
+const Info = (svg, text) => {
+ svg.append("g").attr("class", "dam");
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "end")
+ .attr("dx", 1230)
+ .attr("dy", 20)
+ .attr("class", "infoText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.2em")
+ .text(text);
+};
+
+export default Info;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/LeftAxis.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/LeftAxis.js
new file mode 100644
index 00000000..3747b574
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/LeftAxis.js
@@ -0,0 +1,65 @@
+import * as d3 from "d3";
+
+import { curvedLine, createLine } from "./Lines.js";
+
+const LeftAxis = (svg, damBottom, damTop) => {
+ svg
+ .append("g")
+ .attr("class", "leftLine")
+ .append("path")
+ .attr(
+ "d",
+ curvedLine([
+ [0, 100],
+ [220, 100],
+ [243, 130],
+ [243, 150],
+ [243, 560],
+ [243, 560],
+ [243, 560],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 7)
+ .attr("fill", "none")
+ .attr("stroke-linejoin", "miter");
+
+ // Number Format
+ var numFormat = d3.format(".2f");
+ // Tick Scale
+ var tickScale = d3.scaleLinear().domain([0, 17]).range([damTop, damBottom]);
+ // @todo; consider more consistent scale, not based
+ // on the top/bottom of dam divided into 18 equal
+ // increments, as this creates unexpected tick
+ // values (e.g. 1.41, 1.52)
+ svg.append("g").attr("class", "ticks");
+ var length = 5;
+ var strokeWidth = 4;
+ for (var i = 0; i < 18; i++) {
+ if (i % 2 === 0) {
+ strokeWidth = 4;
+ length = 15;
+ } else {
+ strokeWidth = 2;
+ length = 10;
+ }
+ svg
+ .select("g.ticks")
+ .append("path")
+ .attr("d", createLine(length))
+ .attr("stroke-width", strokeWidth)
+ .attr("stroke", "#B3B3B3")
+ .attr("transform", "translate(" + (240 - length) + "," + (130 + i * 25.4) + ")");
+
+ svg
+ .select("g.ticks")
+ .append("text")
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#b3b3b3")
+ .attr("font-size", "12px")
+ .attr("transform", "translate(" + 180 + "," + (135 + i * 25.4) + ")")
+ .text(numFormat(tickScale(i)));
+ }
+};
+
+export default LeftAxis;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Legend.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Legend.js
new file mode 100644
index 00000000..3388a5d0
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Legend.js
@@ -0,0 +1,148 @@
+import { straightLine } from "./Lines.js";
+
+const Legend = (svg) => {
+ svg
+ .append("g")
+ .attr("class", "legend")
+ .append("text")
+ .attr("dx", 1020)
+ .attr("dy", 60)
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.5em")
+ .text("Legend");
+ svg
+ .select("g.legend")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [1010, 70],
+ [1220, 70],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("fill", "#B3B3B3")
+ .attr("stroke-width", 3);
+ //create lake level icon
+ svg
+ .select("g.legend")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [1010, 90],
+ [1010, 80],
+ [1020, 80],
+ [1020, 90],
+ [1010, 90],
+ ]),
+ )
+ .attr("fill", "#DCF1F9")
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 1);
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1030)
+ .attr("dy", 90)
+ .attr("font-family", "sans-serif")
+ .text("Current Lake Level");
+ //create tail water icon
+ svg
+ .select("g.legend")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [1010, 100],
+ [1010, 110],
+ [1020, 110],
+ [1020, 100],
+ [1010, 100],
+ ]),
+ )
+ .attr("fill", "#83BADF")
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 1);
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1030)
+ .attr("dy", 110)
+ .attr("font-family", "sans-serif")
+ .text("Tail Water");
+ //create inflow icon
+ svg
+ .select("g.legend")
+ .append("circle")
+ .attr("r", 12)
+ .attr("cx", 1015)
+ .attr("cy", 130)
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1009)
+ .attr("dy", 134)
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#fff")
+ .attr("font-size", "12px")
+ .text("IN");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1030)
+ .attr("dy", 135)
+ .attr("font-family", "sans-serif")
+ .text("Inflow");
+ //create surcharge icon
+ svg
+ .select("g.legend")
+ .append("circle")
+ .attr("r", 12)
+ .attr("cx", 1015)
+ .attr("cy", 160)
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1005)
+ .attr("dy", 164)
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#fff")
+ .attr("font-size", "10px")
+ .text("SUR");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1030)
+ .attr("dy", 165)
+ .attr("font-family", "sans-serif")
+ .text("Surcharge Release");
+ //create outflow icon
+ svg
+ .select("g.legend")
+ .append("circle")
+ .attr("r", 12)
+ .attr("cx", 1015)
+ .attr("cy", 190)
+ .attr("fill", "#0F4868");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1005)
+ .attr("dy", 194)
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#fff")
+ .attr("font-size", "10px")
+ .text("OUT");
+ svg
+ .select("g.legend")
+ .append("text")
+ .attr("dx", 1030)
+ .attr("dy", 195)
+ .attr("font-family", "sans-serif")
+ .text("Outflow");
+};
+
+export default Legend;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Levels.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Levels.js
new file mode 100644
index 00000000..274c8d0c
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Levels.js
@@ -0,0 +1,153 @@
+import { createLine, straightLine } from "./Lines.js";
+const debug = parseInt(import.meta.env.VITE_APP_DEBUG);
+
+const Levels = (svg, damScale, damBottom, damTop, levels = []) => {
+ // short circuit
+ if (!levels.length || !damTop || isNaN(damBottom) || !damScale) {
+ if (debug) {
+ console.log("Unable to draw dam profile levels");
+ console.log(levels);
+ }
+
+ return;
+ }
+
+ const x = { left: 160, right: 410 };
+ const length = 290;
+ const radius = 4;
+
+ var baseModifier = Math.abs(Math.round((damTop - damBottom) * 0.17));
+
+ var moveByModifier = Math.max(
+ 27,
+ Math.abs(Math.round((damTop - damBottom) * baseModifier > 65 ? 0.055 : 0.073)),
+ );
+
+ // Default Level Properties if not speficied
+ const _levels = levels.map((l) => ({
+ side:
+ l.name === "Top of Surcharge" || l.name === "Spillway Crest" ? "right" : "left", // default; labels will be on left side of image
+ showLine: true,
+ ...l,
+ }));
+
+ // reverse order sort
+ // making sure no undefined values
+ var lineData = _levels
+ .filter((l) => l.value)
+ .sort((a, b) => {
+ return a.value > b.value ? -1 : a.value < b.value ? 1 : 0;
+ });
+
+ //Check if the text and lines are too close, and then re-position
+ let priorVal = -1,
+ priorMod = 0,
+ priorSide = "left";
+ lineData.forEach((d, i) => {
+ const valueDiff = Math.abs(priorVal - d.value);
+ // add second previous value check in case there's a group of levels
+ let secondPriorItem = i >= 2 ? lineData[i - 2] : null;
+ let secondPriorVal = secondPriorItem?.value;
+ let secondPriorMod = secondPriorItem?.modifier;
+ const secondValueDiff = secondPriorVal - d.value;
+ const isReset =
+ (d.side === "right" && priorSide === "left") ||
+ priorVal === -1 ||
+ valueDiff > baseModifier ||
+ (priorSide !== d.side && secondValueDiff > baseModifier - 5);
+ const effectivelyEqual = valueDiff < 2;
+ const hasExistingMod = priorMod !== 0;
+
+ if (isReset) {
+ [d.modifier, d.lineType, priorMod, priorVal] = [0, "straight", 0, d.value];
+ } else {
+ const modStep = hasExistingMod
+ ? priorMod + moveByModifier
+ : // here check prior side
+ priorSide !== d.side
+ ? moveByModifier + (!isNaN(secondPriorMod) ? secondPriorMod : 0)
+ : moveByModifier;
+ const nextPrior =
+ !hasExistingMod && !effectivelyEqual
+ ? d.value - moveByModifier
+ : !hasExistingMod && effectivelyEqual
+ ? d.value - baseModifier
+ : hasExistingMod && !effectivelyEqual
+ ? d.value - baseModifier
+ : d.value - (priorMod + moveByModifier);
+
+ [d.modifier, d.lineType, priorMod, priorVal] = [
+ modStep,
+ "angled",
+ modStep,
+ nextPrior,
+ ];
+ }
+ [secondPriorVal, priorVal, priorSide] = [priorVal, d.value, d.side];
+ });
+
+ var lines = svg
+ .selectAll("g.dashedLines")
+ .data(lineData, (d) => `${d.name}-${d.value}`)
+ .enter()
+ .append("g")
+ .attr("class", "dashedLines");
+
+ lines
+ .append("path")
+ .attr("d", (d) => {
+ if (d.lineType === "straight") {
+ return d.showLine ? createLine(length) : createLine(20);
+ } else {
+ return d.showLine
+ ? d.side === "left"
+ ? straightLine([
+ [0, d.modifier],
+ [20, 0],
+ [length, 0],
+ ])
+ : straightLine([
+ [0, 0],
+ [length - 20, 0],
+ [length, d.modifier],
+ ])
+ : straightLine([
+ [0, d.modifier],
+ [d.modifier, 0],
+ ]);
+ }
+ })
+ .attr("transform", (d) => `translate(${x[d.side]},${damScale(d.value)})`)
+ .attr("stroke", "#FF0000")
+ .attr("fill", "none")
+ .style("stroke-dasharray", ("3", "3"));
+
+ //create red dot at the end of the dashed line
+ lines
+ .append("circle")
+ .attr("r", radius)
+ .attr("cx", (d) => (d.side === "left" ? x[d.side] : x[d.side] + length))
+ .attr("cy", (d) => damScale(d.value) + d.modifier)
+ .attr("fill", "#FF0000");
+
+ lines
+ .append("text")
+ .attr("text-anchor", (d) => (d.side === "left" ? "end" : "start"))
+ .attr("x", (d) => (d.side === "left" ? x[d.side] - 10 : x[d.side] + length + 10))
+ .attr("y", (d) => damScale(d.value) + d.modifier + radius - 4.5)
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#FF0000")
+ .attr("font-size", "1.0em")
+ .text((d) => d.name);
+
+ lines
+ .append("text")
+ .attr("text-anchor", (d) => (d.side === "left" ? "end" : "start"))
+ .attr("x", (d) => (d.side === "left" ? x[d.side] - 10 : x[d.side] + length + 10))
+ .attr("y", (d) => damScale(d.value) + d.modifier + radius + 10)
+ .attr("font-family", "sans-serif")
+ .attr("fill", "#000000")
+ .attr("font-size", "1.em")
+ .text((d) => `${d.value}'`);
+};
+export default Levels;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Lines.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Lines.js
new file mode 100644
index 00000000..8af593f0
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Lines.js
@@ -0,0 +1,21 @@
+import * as d3 from "d3";
+
+const curvedLine = d3
+ .line()
+ .x((d) => d[0])
+ .y((d) => d[1])
+ .curve(d3.curveBasis);
+
+const straightLine = d3
+ .line()
+ .x((d) => d[0])
+ .y((d) => d[1]);
+
+const createLine = (length) => {
+ return straightLine([
+ [0, 0],
+ [length, 0],
+ ]);
+};
+
+export { createLine, curvedLine, straightLine };
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Mountain.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Mountain.js
new file mode 100644
index 00000000..afed87df
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Mountain.js
@@ -0,0 +1,73 @@
+import { straightLine } from "./Lines.js";
+
+const Mountain = (svg) => {
+ svg
+ .append("g")
+ .attr("class", "mountain")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [10, 100],
+ [20, 60],
+ [35, 45],
+ [40, 35],
+ [60, 25],
+ [75, 65],
+ [90, 70],
+ [110, 100],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3");
+ //draw mountain accent 1
+ svg
+ .select("g.mountain")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [20, 100],
+ [25, 90],
+ [22, 75],
+ [30, 55],
+ [27, 75],
+ [40, 90],
+ [40, 100],
+ ]),
+ )
+ .attr("fill", "#58595D");
+ //draw mountain accent 2
+ svg
+ .select("g.mountain")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [60, 72],
+ [95, 100],
+ [85, 100],
+ [70, 90],
+ [65, 92],
+ ]),
+ )
+ .attr("fill", "#58595D");
+ // draw mountain accent 3
+ svg
+ .select("g.mountain")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [60, 60],
+ [55, 40],
+ [63, 55],
+ [62, 57],
+ [70, 65],
+ ]),
+ )
+ .attr("fill", "#58595D");
+};
+
+export default Mountain;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/NoLockTurbine.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/NoLockTurbine.js
new file mode 100644
index 00000000..ff4b6afd
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/NoLockTurbine.js
@@ -0,0 +1,122 @@
+import { straightLine } from "./Lines.js";
+import { createArrow } from "./Shapes.js";
+
+const noLockTurbine = (svg) => {
+ //bottom of lock
+ svg
+ .append("g")
+ .attr("class", "lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [240, 560],
+ [300, 560],
+ [300, 610],
+ [840, 560],
+ [1209, 560],
+ [1209, 640],
+ [240, 640],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3")
+ .attr("stroke-linejoin", "bevel");
+ var lockGradient = svg
+ .select("defs")
+ .append("linearGradient")
+ .attr("id", "LockGradient")
+ .attr("x1", "0%")
+ .attr("x2", "50%")
+ .attr("y1", "0%")
+ .attr("y2", "0%")
+ .attr("gradientUnits", "userSpaceOnUse");
+ lockGradient.append("stop").attr("offset", "40%").attr("stop-color", "#DCF1F9");
+ lockGradient.append("stop").attr("offset", "80%").attr("stop-color", "#82B8DC");
+ //create water underneath lock (with the above gradient)
+ svg
+ .select("g.lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [300, 559],
+ [830, 559],
+ [830, 610],
+ [300, 610],
+ ]),
+ )
+ .attr("fill", "url(#LockGradient)");
+ //create arrows in the water
+ createArrow(svg, 310, 570, 0);
+ createArrow(svg, 520, 605, -90);
+ createArrow(svg, 787, 590, 180);
+ createArrow(svg, 822, 590, 180);
+ //water outside of lock
+ svg
+ .select("g.lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [611, 510],
+ [611, 560],
+ [1210, 560],
+ [1210, 510],
+ ]),
+ )
+ .attr("fill", "#85BBDF");
+ //rectangle below dam
+ svg
+ .select("g.lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [330, 560],
+ [330, 590],
+ [770, 590],
+ [770, 560],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3")
+ .attr("stroke-linejoin", "bevel");
+ //separator between outlet arrows
+ svg
+ .select("g.lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [795, 560],
+ [805, 560],
+ [805, 590],
+ [795, 590],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3");
+ //grey region separating tailwater from outlet
+ svg
+ .select("g.lock")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [830, 560],
+ [850, 560],
+ [1020, 560],
+ [1020, 590],
+ [830, 590],
+ ]),
+ )
+ .attr("stroke", "#B3B3B3")
+ .attr("stroke-width", 2)
+ .attr("fill", "#B3B3B3");
+};
+
+export default noLockTurbine;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon.js
new file mode 100644
index 00000000..5d6d04d1
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon.js
@@ -0,0 +1,94 @@
+import { straightLine } from "./Lines.js";
+
+const OutflowIcon = (svg, outflow, outflowTotals = [], position = { x: 0, y: 0 }) => {
+ // const outflowCircle = {
+ // dam: { x: 600, y: 360 },
+ // lock: { x: 1000, y: 360 },
+ // lockTurbine: { x: 1000, y: 360 },
+ // turbine: { x: 600, y: 360 },
+ // };
+ // Note: arrow coordinates are relative to the position provided to OutflowIcon
+ // Absolute coordinates for SVG are calculated using position coordinates
+ const arrow = [
+ [20, -5],
+ [55, -5],
+ [55, -20],
+ [80, 0],
+ [55, 20],
+ [55, 5],
+ [20, 5],
+ ];
+ // circle
+ svg
+ .append("g")
+ .attr("class", "outflowIcon")
+ .append("circle")
+ .attr("r", 25)
+ .attr("cx", position.x)
+ .attr("cy", position.y)
+ .attr("fill", "#0F4868");
+ // arrow
+ svg
+ .select("g.outflowIcon")
+ .append("path")
+ .attr(
+ "d",
+ straightLine(
+ arrow.map((point) => [point[0] + position.x, point[1] + position.y]),
+ ),
+ )
+ .attr("fill", "#0F4868");
+ // label 'OUT'
+ svg
+ .select("g.outflowIcon")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", position.x)
+ .attr("dy", position.y + 5)
+ .attr("fill", "#fff")
+ .attr("font-family", "sans-serif")
+ .text("OUT");
+
+ // label value
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", 710)
+ .attr("dy", 325)
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.2em")
+ .attr("class", "outflowText")
+ .text(
+ outflow !== null && !isNaN(outflow)
+ ? outflow.toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ }) + " cfs"
+ : "Not Available",
+ );
+ outflowTotals?.map((level, i) => {
+ const { value, name } = level;
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "left")
+ .attr("dx", 680)
+ .attr("dy", 405 + i * 20)
+ .attr("font-family", "sans-serif")
+ .attr("font-size", ".8em")
+ .attr("class", "outflowText")
+ .text(
+ value !== null && !isNaN(value)
+ ? value.toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ }) +
+ " cfs - " +
+ name
+ : null,
+ );
+ });
+};
+
+export default OutflowIcon;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon_old.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon_old.js
new file mode 100644
index 00000000..8aa562dc
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/OutflowIcon_old.js
@@ -0,0 +1,72 @@
+import { straightLine } from "./Lines.js";
+
+const OutflowIcon = (svg, outflow, position = { x: 0, y: 0 }) => {
+ // const outflowCircle = {
+ // dam: { x: 600, y: 360 },
+ // lock: { x: 1000, y: 360 },
+ // lockTurbine: { x: 1000, y: 360 },
+ // turbine: { x: 600, y: 360 },
+ // };
+ // Note: arrow coordinates are relative to the position provided to OutflowIcon
+ // Absolute coordinates for SVG are calculated using position coordinates
+ const arrow = [
+ [20, -5],
+ [55, -5],
+ [55, -20],
+ [80, 0],
+ [55, 20],
+ [55, 5],
+ [20, 5],
+ ];
+ // circle
+ svg
+ .append("g")
+ .attr("class", "outflowIcon")
+ .append("circle")
+ .attr("r", 25)
+ .attr("cx", position.x)
+ .attr("cy", position.y)
+ .attr("fill", "#0F4868");
+ // arrow
+ svg
+ .select("g.outflowIcon")
+ .append("path")
+ .attr(
+ "d",
+ straightLine(
+ arrow.map((point) => [point[0] + position.x, point[1] + position.y]),
+ ),
+ )
+ .attr("fill", "#0F4868");
+ // label 'OUT'
+ svg
+ .select("g.outflowIcon")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", position.x)
+ .attr("dy", position.y + 5)
+ .attr("fill", "#fff")
+ .attr("font-family", "sans-serif")
+ .text("OUT");
+
+ // label value
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", 610)
+ .attr("dy", 325)
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.2em")
+ .attr("class", "outflowText")
+ .text(
+ outflow !== null && !isNaN(outflow)
+ ? outflow.toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ }) + " cfs"
+ : "Not Available",
+ );
+};
+
+export default OutflowIcon;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Precipitation.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Precipitation.js
new file mode 100644
index 00000000..c85e30a3
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Precipitation.js
@@ -0,0 +1,80 @@
+import { straightLine } from "./Lines.js";
+// import * as d3 from 'd3';
+const Precipitation = (svg, precip) => {
+ svg
+ .append("g")
+ .attr("class", "precipitation")
+ .append("path")
+ .attr("transform", "translate(75,-10)")
+ .attr("d", "M25,50 a10,10 0 1,1 0,-20 a15,15 0 0,1 40,0 a10,10 0 1,1 0,20 z")
+ .attr("fill", "#58595D");
+
+ svg
+ .select("g.precipitation")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [100, 40],
+ [90, 50],
+ ]),
+ )
+ .attr("stroke", "#66AAD7")
+ .attr("stroke-width", 3);
+ svg
+ .select("g.precipitation")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [115, 40],
+ [105, 50],
+ ]),
+ )
+ .attr("stroke", "#66AAD7")
+ .attr("stroke-width", 3);
+ svg
+ .select("g.precipitation")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [130, 40],
+ [120, 50],
+ ]),
+ )
+ .attr("stroke", "#66AAD7")
+ .attr("stroke-width", 3);
+ // precipitation text label
+ svg
+ .append("g")
+ .attr("class", "precipitationLabel")
+ .append("text")
+ .attr("text-anchor", "start")
+ .attr("x", 107)
+ .attr("y", 30)
+ .attr("class", "precipText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", ".9em")
+ .attr("fill", "white")
+ .text("24 hr");
+ svg
+ .append("g")
+ .attr("class", "precipitationLabel")
+ .append("text")
+ .attr("text-anchor", "start")
+ .attr("dx", 160)
+ .attr("dy", 35)
+ .attr("class", "precipText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1.3em")
+ .text(
+ precip !== null && !isNaN(precip)
+ ? precip?.toLocaleString(undefined, {
+ maximumFractionDigits: 2,
+ }) + " in"
+ : "Not Available",
+ );
+};
+
+export default Precipitation;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Shapes.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Shapes.js
new file mode 100644
index 00000000..4fba322c
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Shapes.js
@@ -0,0 +1,24 @@
+//import * as d3 from 'd3';
+import { straightLine } from "./Lines";
+
+//create arrows at x and y coordinate. rotate by rotation degrees
+const createArrow = (svg, x, y, rotation) => {
+ return svg
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [4, 0],
+ [6, 0],
+ [6, 20],
+ [10, 20],
+ [5, 28],
+ [0, 20],
+ [4, 20],
+ ]),
+ )
+ .attr("fill", "#000000")
+ .attr("transform", "translate(" + x + "," + y + ") rotate(" + rotation + ")");
+};
+
+export { createArrow };
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/SurchargeIcon.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/SurchargeIcon.js
new file mode 100644
index 00000000..29bc7054
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/SurchargeIcon.js
@@ -0,0 +1,57 @@
+import { straightLine } from "./Lines.js";
+
+const SurchargeIcon = (svg, surcharge, position = { x: 570, y: 120 }) => {
+ // Note: arrow coordinates are relative to the position provided to OutflowIcon
+ // Absolute coordinates for SVG are calculated using position coordinates
+ const arrow = [
+ [-5, 10],
+ [-5, 40],
+ [-20, 40],
+ [0, 60],
+ [20, 40],
+ [5, 40],
+ [5, 10],
+ ];
+ svg
+ .append("g")
+ .attr("class", "surchargeIcon")
+ .append("circle")
+ .attr("r", 25)
+ .attr("cx", position.x)
+ .attr("cy", position.y)
+ .attr("fill", "#66AAD7");
+
+ svg
+ .select("g.surchargeIcon")
+ .append("path")
+ .attr(
+ "d",
+ straightLine(
+ arrow.map((point) => [point[0] + position.x, point[1] + position.y]),
+ ),
+ )
+ .attr("fill", "#66AAD7");
+ svg
+ .select("g.surchargeIcon")
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dx", 570)
+ .attr("dy", 126)
+ .attr("fill", "#fff")
+ .attr("font-family", "sans-serif")
+ .text("SUR");
+
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "start")
+ .attr("dx", 605)
+ .attr("dy", 124)
+ .attr("class", "surchargeText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "1em")
+ .text(!isNaN(surcharge) ? surcharge + " cfs" : "");
+};
+
+export default SurchargeIcon;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/TailwaterLevel.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/TailwaterLevel.js
new file mode 100644
index 00000000..f9a16b7f
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/TailwaterLevel.js
@@ -0,0 +1,36 @@
+import { straightLine } from "./Lines.js";
+
+const TailwaterLevel = (svg, damScale, tailwater) => {
+ svg.append("g").attr("class", "tailwater-level");
+ svg
+ .select("g.tailwater-level")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ // [490, damScale(tailwater)], // this scales tailwater to actual elevation
+ [490, 530],
+ [490, 590],
+ [1210, 590],
+ // [1210, damScale(tailwater)], // this scales tailwater to actual elevation
+ [1210, 530],
+ ]),
+ )
+ .attr("fill", "#85BBDF");
+
+ svg
+ .append("g")
+ .attr("class", "labelText")
+ .append("text")
+ .attr("text-anchor", "end")
+ .attr("dx", 1200)
+ // .attr('dy', damScale(tailwater) + 20) // labels tailwater above scaled elevation
+ .attr("dy", 530 + 20)
+ .attr("fill", "#fff")
+ .attr("class", "tailwaterText")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "18px")
+ .text(!isNaN(tailwater) & tailwater ? tailwater + "'" : "");
+};
+
+export default TailwaterLevel;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Turbine.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Turbine.js
new file mode 100644
index 00000000..fea6dd1b
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/Turbine.js
@@ -0,0 +1,108 @@
+import { straightLine, curvedLine } from "./Lines.js";
+
+const Turbine = (svg) => {
+ //create top part of turbine
+ svg
+ .append("g")
+ .attr("class", "turbine")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [565, 440],
+ [565, 430],
+ [575, 430],
+ [575, 425],
+ [595, 425],
+ [595, 430],
+ [605, 430],
+ [605, 440],
+ [590, 440],
+ [590, 540],
+ [580, 540],
+ [580, 440],
+ ]),
+ )
+ .attr("stroke", "#606063")
+ .attr("stroke-width", 2)
+ .attr("fill", "#606063");
+
+ //create circular part of turbine
+ svg
+ .select("g.turbine")
+ .append("path")
+ .attr(
+ "d",
+ curvedLine([
+ [580, 540],
+ [560, 560],
+ [570, 570],
+ [600, 570],
+ [610, 560],
+ [590, 540],
+ ]),
+ )
+ .attr("stroke", "#606063")
+ .attr("stroke-width", 2)
+ .attr("fill", "#606063");
+ //create bottom part of turbine
+ svg
+ .select("g.turbine")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [580, 590],
+ [580, 575],
+ [583, 575],
+ [583, 560],
+ [587, 560],
+ [587, 575],
+ [590, 575],
+ [590, 575],
+ [590, 590],
+ ]),
+ )
+ .attr("stroke", "#606063")
+ .attr("stroke-width", 2)
+ .attr("fill", "#606063");
+ //create turbine propeller part 1
+ svg
+ .select("g.turbine")
+ .append("path")
+ .attr(
+ "d",
+ curvedLine([
+ [575, 595],
+ [580, 598],
+ [590, 592],
+ [595, 595],
+ [590, 598],
+ [580, 592],
+ [575, 595],
+ ]),
+ )
+ .attr("stroke", "#8D8986")
+ .attr("stroke-width", 2)
+ .attr("fill", "#8D8986");
+ //create turbine propeller part 2
+ svg
+ .select("g.turbine")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [584, 590],
+ [586, 590],
+ [586, 600],
+ [586, 600],
+ [584, 600],
+ [584, 600],
+ ]),
+ )
+ .attr("stroke", "#221E1F")
+ .attr("stroke-width", 2)
+ .attr("fill", "#221E1F");
+};
+
+export default Turbine;
diff --git a/lib/components/data/plots/dam-profile/dam-profile-chart/elements/WaterLevel.js b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/WaterLevel.js
new file mode 100644
index 00000000..6a0e00a9
--- /dev/null
+++ b/lib/components/data/plots/dam-profile/dam-profile-chart/elements/WaterLevel.js
@@ -0,0 +1,34 @@
+import { straightLine } from "./Lines.js";
+
+const WaterLevel = (svg, damScale, waterLevel) => {
+ svg.append("g").attr("class", "water-level");
+ // water level line
+ svg
+ .select("g.water-level")
+ .append("path")
+ .attr(
+ "d",
+ straightLine([
+ [247, damScale(waterLevel)],
+ [247, 560],
+ [409, 560],
+ [409, damScale(waterLevel)],
+ ]),
+ )
+ .attr("fill", "#DCF1F9");
+ // water level label (e.g. 12')
+ svg
+ .select("g.water-level")
+ .append("text")
+ .attr("text-anchor", "start")
+ .attr("dx", 250)
+ .attr("dy", damScale(waterLevel) + 20)
+ .attr("fill", "#666666")
+ .attr("font-family", "sans-serif")
+ .attr("font-size", "24px")
+ .text(function () {
+ return waterLevel + "'";
+ });
+};
+
+export default WaterLevel;
diff --git a/lib/index.jsx b/lib/index.jsx
index 1ceb760e..458abc0e 100644
--- a/lib/index.jsx
+++ b/lib/index.jsx
@@ -14,6 +14,7 @@ import CWMSPlot from "./components/data/plots/CWMSPlot";
import BasinPie from "./components/data/plots/BasinPie";
import RadialFillChart from "./components/data/plots/RadialFillChart";
import CdaLatestValueCard from "./components/data/cards/CdaLatestValueCard";
+import DamProfile from "./components/data/plots/dam-profile/chart";
import DataStatus from "./components/data/summary/DataStatus";
// Import input components
@@ -148,5 +149,6 @@ export {
updateToast,
formatSubmissionMessage,
showDetailedError,
+ DamProfile,
};
// export { helperFunction };
diff --git a/package-lock.json b/package-lock.json
index fcc8539b..effecb3d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
"name": "@usace-watermanagement/groundwork-water",
- "version": "3.11.0",
+ "version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@usace-watermanagement/groundwork-water",
- "version": "3.11.0",
+ "version": "4.0.0",
"license": "MIT",
"dependencies": {
"@tanstack/react-virtual": "^3.14.6",
"cwmsjs": "^2.3.0-2024.12.10",
+ "d3": "^7.9.0",
"dayjs": "^1.11.11",
"deepmerge": "^4.3.1",
"oidc-client-ts": "^3.5.0",
@@ -1562,9 +1563,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1582,9 +1580,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1602,9 +1597,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1622,9 +1614,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1642,9 +1631,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1662,9 +1648,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3464,6 +3447,428 @@
"resolved": "https://registry.npmjs.org/cwmsjs/-/cwmsjs-2.3.0-2024.12.10.tgz",
"integrity": "sha512-5uPKl24caND+JR0C/3LLQ9cXmHy/lW4//PB+bykQ2kCKGvKOu8sdmeOaP3NbMGyHdsc6kGWJplZKmcsJEp0tBA=="
},
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@@ -3615,6 +4020,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -5276,6 +5690,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -6122,9 +6545,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6146,9 +6566,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6170,9 +6587,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6194,9 +6608,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7803,6 +8214,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
"node_modules/rolldown": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz",
@@ -7859,6 +8276,12 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -7918,7 +8341,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
"license": "MIT"
},
"node_modules/saxes": {
diff --git a/package.json b/package.json
index 5fb63e49..38c72a81 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"dependencies": {
"@tanstack/react-virtual": "^3.14.6",
"cwmsjs": "^2.3.0-2024.12.10",
+ "d3": "^7.9.0",
"dayjs": "^1.11.11",
"deepmerge": "^4.3.1",
"oidc-client-ts": "^3.5.0",