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

+ +
+ `` +
+ ); +} + +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",