Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Display historical counts of observations used #472

Merged
merged 2 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<svg>
<g class="data">
<path id="range"></path>
Expand Down Expand Up @@ -76,8 +76,8 @@ export default class ChartTimeSeries extends ChartElement {
super();

const root = this.attachShadow({ mode: "open" });
root.innerHTML = `<style>${ChartTimeSeries.#STYLE}</style>
${ChartTimeSeries.#TEMPLATE}`;
root.innerHTML = `<style>${ChartOMBHistory.#STYLE}</style>
${ChartOMBHistory.#TEMPLATE}`;
this.#svg = select(root.querySelector("svg"));
}

Expand Down Expand Up @@ -243,4 +243,4 @@ export default class ChartTimeSeries extends ChartElement {
}
}

customElements.define("chart-timeseries", ChartTimeSeries);
customElements.define("chart-ombhistory", ChartOMBHistory);
223 changes: 223 additions & 0 deletions src/unified_graphics/static/js/component/ChartObsCount.js
Original file line number Diff line number Diff line change
@@ -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 = `<svg>
<g class="data">
<path id="value"></path>
<line id="current"></line>
</g>
<g class="x-axis"></g>
<g class="y-axis"></g>
</svg>`;

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 = `<style>${ChartObsCount.#STYLE}</style>
${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);
3 changes: 2 additions & 1 deletion src/unified_graphics/static/js/scalardiag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
1 change: 1 addition & 0 deletions src/unified_graphics/static/js/vectordiag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
26 changes: 17 additions & 9 deletions src/unified_graphics/templates/layouts/diag.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,28 @@ <h2 class="heading-2 flex-0">{% if minim_loop == "ges" %}Guess{% else %}Analysis

<chart-container class="padding-2 radius-md bg-white shadow-1">
<span class="axis-y title" slot="title-y">Observation &minus; Forecast</span>
<chart-timeseries id="history-{{ minim_loop }}" src="{{ history_url[minim_loop] }}?initialization_time={{ form.get("initialization_time") }}"
current="{{ form.get("initialization_time") }}"></chart-timeseries>
<chart-ombhistory id="history-{{ minim_loop }}" src="{{ history_url[minim_loop] }}?initialization_time={{ form.get("initialization_time") }}"
current="{{ form.get("initialization_time") }}"></chart-ombhistory>
<span class="axis-x title" slot="title-x">Initialization Time</span>
</chart-container>
</div>
{%- endif %}

<chart-container class="padding-2 radius-md bg-white shadow-1">
<chart-map id="observations-{{ minim_loop }}"
src="{{ map_url[minim_loop] }}"
fill="obs_minus_forecast_adjusted"></chart-map>
<color-ramp slot="legend" for="observations-{{ minim_loop }}" class="font-ui-3xs" format="s"
>Observation &minus; Forecast</color-ramp>
</chart-container>
<div class="grid">
<chart-container class="padding-2 radius-md bg-white shadow-1">
<chart-map id="observations-{{ minim_loop }}"
src="{{ map_url[minim_loop] }}"
fill="obs_minus_forecast_adjusted"></chart-map>
<color-ramp slot="legend" for="observations-{{ minim_loop }}" class="font-ui-3xs" format="s"
>Observation &minus; Forecast</color-ramp>
</chart-container>
<chart-container class="padding-2 radius-md bg-white shadow-1">
<span class="axis-y title" slot="title-y">Used Observation Count</span>
<chart-obscount id="history-{{ minim_loop }}" src="{{ history_url[minim_loop] }}?initialization_time={{ form.get("initialization_time") }}"
current="{{ form.get("initialization_time") }}"></chart-obscount>
<span class="axis-x title" slot="title-x">Initialization Time</span>
</chart-container>
</div>
</div>
{%- endfor %}

Expand Down
Loading