diff --git a/src/unified_graphics/static/js/component/ChartTimeSeries.js b/src/unified_graphics/static/js/component/ChartOMBHistory.js similarity index 96% rename from src/unified_graphics/static/js/component/ChartTimeSeries.js rename to src/unified_graphics/static/js/component/ChartOMBHistory.js index 1b49f62e..50f801a6 100644 --- a/src/unified_graphics/static/js/component/ChartTimeSeries.js +++ b/src/unified_graphics/static/js/component/ChartOMBHistory.js @@ -32,7 +32,7 @@ import ChartElement from "./ChartElement.js"; * in an HTML attribute called `src` * @property {number[]} data Values visualizaed by the time series. */ -export default class ChartTimeSeries extends ChartElement { +export default class ChartOMBHistory extends ChartElement { static #TEMPLATE = ` @@ -76,8 +76,8 @@ export default class ChartTimeSeries extends ChartElement { super(); const root = this.attachShadow({ mode: "open" }); - root.innerHTML = ` - ${ChartTimeSeries.#TEMPLATE}`; + root.innerHTML = ` + ${ChartOMBHistory.#TEMPLATE}`; this.#svg = select(root.querySelector("svg")); } @@ -243,4 +243,4 @@ export default class ChartTimeSeries extends ChartElement { } } -customElements.define("chart-timeseries", ChartTimeSeries); +customElements.define("chart-ombhistory", ChartOMBHistory); diff --git a/src/unified_graphics/static/js/component/ChartObsCount.js b/src/unified_graphics/static/js/component/ChartObsCount.js new file mode 100644 index 00000000..c867a872 --- /dev/null +++ b/src/unified_graphics/static/js/component/ChartObsCount.js @@ -0,0 +1,223 @@ +import { + axisBottom, + axisLeft, + extent, + format, + line, + scaleLinear, + scaleTime, + select, + timeFormat, +} from "../vendor/d3.js"; + +import ChartElement from "./ChartElement.js"; + +/** + * Time series chart + * + * @property {string} current A UTC date string + * @property {string} formatX + * A d3 format string used to format values along the x-axis. This property + * is reflected in an HTML attribute on the custom element called + * `format-x`. + * @property {string} formatY + * A d3 format string used to format values along the y-axis. This property + * is reflected in an HTML attribute on the custom element called + * `format-y`. + * @property {string} src + * A URL for JSON data used in the time series. This property is reflected + * in an HTML attribute called `src` + * @property {number[]} data Values visualizaed by the time series. + */ +export default class ChartObsCount extends ChartElement { + static #TEMPLATE = ` + + + + + + + `; + + static #STYLE = `:host { + display: block; + user-select: none; + } + + #current, + #value { + fill: transparent; + stroke: #1b1b1b; + }`; + + static get observedAttributes() { + return ["current", "format-x", "format-y", "src"].concat( + ChartElement.observedAttributes + ); + } + + #data = []; + #svg = null; + + constructor() { + super(); + + const root = this.attachShadow({ mode: "open" }); + root.innerHTML = ` + ${ChartObsCount.#TEMPLATE}`; + this.#svg = select(root.querySelector("svg")); + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "current": + this.update(); + break; + case "format-x": + case "format-y": + this.update(); + break; + case "src": + fetch(newValue) + .then((response) => response.json()) + .then((data) => { + // Convert dates from strings to Date objects. + data.forEach((d) => { + d.initialization_time = new Date(d.initialization_time); + }); + return data; + }) + .then((data) => (this.data = data)); + break; + default: + super.attributeChangedCallback(name, oldValue, newValue); + break; + } + } + + get current() { + return this.getAttribute("current"); + } + set current(value) { + if (!value) { + this.removeAttribute("current"); + } else { + this.setAttribute("current", value); + } + } + + get data() { + return structuredClone(this.#data); + } + set data(value) { + this.#data = value; + this.update(); + } + + get formatX() { + return this.getAttribute("format-x") ?? "%Y-%m-%d"; + } + set formatX(value) { + if (!value) { + this.removeAttribute("format-x"); + } else { + this.setAttribute("format-x", value); + } + } + + get formatY() { + return this.getAttribute("format-y") ?? ","; + } + set formatY(value) { + if (!value) { + this.removeAttribute("format-y"); + } else { + this.setAttribute("format-y", value); + } + } + + // FIXME: This is copied from Chart2DHistogram + // We should probably have a more uniform interface for all of our chart components. + get margin() { + const fontSize = parseInt(getComputedStyle(this).fontSize); + + return { + top: fontSize, + right: fontSize, + bottom: fontSize, + left: fontSize * 3, + }; + } + + get src() { + return this.getAttribute("src"); + } + set src(value) { + if (!value) { + this.removeAttribute("src"); + } else { + this.setAttribute("src", value); + } + } + + get xScale() { + const domain = extent(this.#data, (d) => d.initialization_time); + const { left, right } = this.margin; + const width = this.width - left - right; + return scaleTime().domain(domain).range([0, width]).nice(); + } + + get yScale() { + const domain = extent(this.#data, (d) => d.count); + const { top, bottom } = this.margin; + const height = this.height - top - bottom; + + return scaleLinear().domain(domain).range([height, 0]).nice(); + } + + render() { + if (!(this.width && this.height)) return; + + const data = this.data; + if (!data) return; + + const { xScale, yScale } = this; + const valueLine = line() + .x((d) => xScale(d.initialization_time)) + .y((d) => yScale(d.count)); + + this.#svg.attr("viewBox", `0 0 ${this.width} ${this.height}`); + this.#svg + .select(".data") + .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`); + this.#svg.select("#value").datum(data).attr("d", valueLine); + + if (this.current) { + this.#svg + .select("#current") + .datum(new Date(this.current)) + .attr("x1", xScale) + .attr("x2", xScale) + .attr("y1", yScale.range()[0]) + .attr("y2", yScale.range()[1]); + } + + const xAxis = axisBottom(xScale).tickFormat(timeFormat(this.formatX)); + const yAxis = axisLeft(yScale).tickFormat(format(this.formatY)); + + this.#svg + .select(".x-axis") + .attr( + "transform", + `translate(${this.margin.left}, ${this.height - this.margin.bottom})` + ) + .call(xAxis); + + this.#svg + .select(".y-axis") + .attr("transform", `translate(${this.margin.left}, ${this.margin.top})`) + .call(yAxis); + } +} + +customElements.define("chart-obscount", ChartObsCount); diff --git a/src/unified_graphics/static/js/scalardiag.js b/src/unified_graphics/static/js/scalardiag.js index 02fc3670..c402d35a 100644 --- a/src/unified_graphics/static/js/scalardiag.js +++ b/src/unified_graphics/static/js/scalardiag.js @@ -2,5 +2,6 @@ import ChartContainer from "./component/ChartContainer.js"; import ChartHistogram from "./component/ChartHistogram.js"; import ChartMap from "./component/ChartMap.js"; -import ChartTimeseries from "./component/ChartTimeSeries.js"; +import ChartObsCount from "./component/ChartObsCount.js"; +import ChartOMBHistory from "./component/ChartOMBHistory.js"; import ColorRamp from "./component/ColorRamp.js"; diff --git a/src/unified_graphics/static/js/vectordiag.js b/src/unified_graphics/static/js/vectordiag.js index 7efef1a8..f94755df 100644 --- a/src/unified_graphics/static/js/vectordiag.js +++ b/src/unified_graphics/static/js/vectordiag.js @@ -2,4 +2,5 @@ import Chart2DHistogram from "./component/Chart2DHistogram.js"; import ChartContainer from "./component/ChartContainer.js"; import ChartMap from "./component/ChartMap.js"; +import ChartObsCount from "./component/ChartObsCount.js"; import ColorRamp from "./component/ColorRamp.js"; diff --git a/src/unified_graphics/templates/layouts/diag.html b/src/unified_graphics/templates/layouts/diag.html index 8a1b6bca..c242a741 100644 --- a/src/unified_graphics/templates/layouts/diag.html +++ b/src/unified_graphics/templates/layouts/diag.html @@ -31,20 +31,28 @@

{% if minim_loop == "ges" %}Guess{% else %}Analysis Observation − Forecast - + Initialization Time {%- endif %} - - - Observation − Forecast - +
+ + + Observation − Forecast + + + Used Observation Count + + Initialization Time + +
{%- endfor %}