Skip to content

Commit

Permalink
Display historical counts of observations used (#472)
Browse files Browse the repository at this point in the history
Hacked in another line chart to display the count of used observations
per run.
  • Loading branch information
esheehan-gsl committed Dec 18, 2023
2 parents d8dfd0d + c69d028 commit 78eb565
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 14 deletions.
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

0 comments on commit 78eb565

Please sign in to comment.