From 0b253049bfec5aa04a56522c51abddd353889fe4 Mon Sep 17 00:00:00 2001 From: Markus Hauru Date: Fri, 12 Apr 2024 14:12:09 +0100 Subject: [PATCH] Fix model plotting --- .../app/base/static/typescript/models.ts | 139 +++++++++--------- dtbase/frontend/app/models/routes.py | 17 ++- .../frontend/app/models/templates/models.html | 47 +----- 3 files changed, 87 insertions(+), 116 deletions(-) diff --git a/dtbase/frontend/app/base/static/typescript/models.ts b/dtbase/frontend/app/base/static/typescript/models.ts index a099a5f..3820187 100644 --- a/dtbase/frontend/app/base/static/typescript/models.ts +++ b/dtbase/frontend/app/base/static/typescript/models.ts @@ -32,71 +32,72 @@ Chart.register( Filler, ) +// Colours for the lines for different model measures. +const COLOR_ALPHA = 0.7 +const COLORS = [ + `rgba(54, 162, 235, ${COLOR_ALPHA})`, // Blue + `rgba(255, 99, 132, ${COLOR_ALPHA})`, // Red + `rgba(255, 159, 64, ${COLOR_ALPHA})`, // Orange + `rgba(255, 205, 86, ${COLOR_ALPHA})`, // Yellow + `rgba(75, 192, 192, ${COLOR_ALPHA})`, // Green + `rgba(153, 102, 255, ${COLOR_ALPHA})`, // Purple +] + +function getColor(i: number) { + return COLORS[i % COLORS.length] +} + export function plot( - top_json: TimeseriesDataPoint[] | null, - mid_json: TimeseriesDataPoint[] | null, - bot_json: TimeseriesDataPoint[] | null, - sensor_json: TimeseriesDataPoint[] | null, - sensor_measure: { - name: string - units: string | null + model_data: { + pred_data: { + measure: { + name: string + units: string | null + } + values: TimeseriesDataPoint[] + }[] + sensor_data: { + measure: { + name: string + units: string | null + } + sensor_uniq_id: string + readings: TimeseriesDataPoint[] + } }, canvas_name: string, - y_label: string, - show_legend: boolean, ): Chart { const datasets = [] - if (top_json !== null) { - const values_top = top_json.map((e) => e["value"]) - const times_top = top_json.map((e) => new Date(e["timestamp"])) - const top_scatter = dictionary_scatter(times_top, values_top) + const pred_data = model_data["pred_data"] + for (let i = 0; i < pred_data.length; i++) { + const results = pred_data[i] + const name = results["measure"]["name"] + const units = results["measure"]["units"] + const values = results["values"].map((e) => e["value"]) + const times = results["values"].map((e) => new Date(e["timestamp"])) + const scatter = dictionary_scatter(times, values) datasets.push({ - label: "Upper bound", - data: top_scatter, - borderColor: "#ee978c", + label: `${name} (${units})`, + data: scatter, + borderColor: getColor(i), fill: false, - borderDash: [1], - pointRadius: 1, - showLine: true, - }) - } - if (mid_json !== null) { - const values_mid = mid_json.map((e) => e["value"]) - const times_mid = mid_json.map((e) => new Date(e["timestamp"])) - const mid_scatter = dictionary_scatter(times_mid, values_mid) - datasets.push({ - label: "Mean", - data: mid_scatter, - borderColor: "#ff0000", - fill: "-1", - borderDash: [2], - pointRadius: 1.5, - showLine: true, - }) - } - if (bot_json !== null) { - const values_bot = bot_json.map((e) => e["value"]) - const times_bot = bot_json.map((e) => new Date(e["timestamp"])) - const bot_scatter = dictionary_scatter(times_bot, values_bot) - datasets.push({ - label: "Lower bound", - data: bot_scatter, - borderColor: "#ee978c", - fill: "-1", - borderDash: [1], pointRadius: 1, showLine: true, }) } - if (sensor_json !== null) { - const values_sensor = sensor_json.map((e) => e["value"]) - const times_sensor = sensor_json.map((e) => new Date(e["timestamp"])) - const sensor_scatter = dictionary_scatter(times_sensor, values_sensor) + const sensor_data = model_data["sensor_data"] + if (sensor_data !== null && sensor_data !== undefined) { + const sensor_unique_id = sensor_data["sensor_uniq_id"] + const name = sensor_data["measure"]["name"] + const units = sensor_data["measure"]["units"] + const values = sensor_data["readings"].map((e) => e["value"]) + const times = sensor_data["readings"].map((e) => new Date(e["timestamp"])) + const scatter = dictionary_scatter(times, values) datasets.push({ - label: "Sensor reading", - data: sensor_scatter, - borderColor: "#a0a0a0", + label: `${sensor_unique_id} readings: ${name} (${units})`, + data: scatter, + borderColor: `rgba(201, 203, 207, ${COLOR_ALPHA})`, fill: false, pointRadius: 1.5, showLine: true, @@ -108,6 +109,7 @@ export function plot( datasets: datasets, } + const show_legend = true const config = { type: "scatter", data: data, @@ -121,15 +123,13 @@ export function plot( legend: { display: show_legend, }, + colors: { + enabled: true, + }, }, scales: { y: { display: true, - title: { - display: true, - text: y_label, - font: { size: 18 }, - }, }, x: { type: "time", @@ -192,17 +192,24 @@ export function updateScenarioSelector( declare global { interface Window { plot: ( - top_json: TimeseriesDataPoint[] | null, - mid_json: TimeseriesDataPoint[] | null, - bot_json: TimeseriesDataPoint[] | null, - sensor_json: TimeseriesDataPoint[] | null, - sensor_measure: { - name: string - units: string | null + model_data: { + pred_data: { + measure: { + name: string + units: string | null + } + values: TimeseriesDataPoint[] + }[] + sensor_data: { + measure: { + name: string + units: string | null + } + sensor_uniq_id: string + readings: TimeseriesDataPoint[] + } }, canvas_name: string, - y_label: string, - show_legend: boolean, ) => void updateScenarioSelector: ( scenarios: ModelScenario[], diff --git a/dtbase/frontend/app/models/routes.py b/dtbase/frontend/app/models/routes.py index 562af6b..97f6767 100644 --- a/dtbase/frontend/app/models/routes.py +++ b/dtbase/frontend/app/models/routes.py @@ -94,7 +94,7 @@ def get_run_sensor_data( run_id: int | str, earliest_timestamp: dt.datetime, last_timestamp: Optional[dt.datetime] = None, -) -> Dict[str, Any]: +) -> Optional[Dict[str, Any]]: """ Get the real data to which the prediction of a ModelRun should be compared @@ -104,8 +104,9 @@ def get_run_sensor_data( last_timestamp: Timestamp of the last prediction point. Optional, by default now. Returns: - dict, with keys "sensor_uniq_id", "measure_name", "readings", where "readings" is - a list of (value, timestamp) tuples. + Assuming this model run has sensor data associated with it, the return value is a + dic, with keys "sensor_uniq_id", "measure_name", "readings", where "readings" is + a list of (value, timestamp) tuples. Otherwise None. """ response = current_user.backend_call( "post", "/model/get-model-run-sensor-measure", {"run_id": run_id} @@ -114,6 +115,9 @@ def get_run_sensor_data( raise RuntimeError(f"A backend call failed: {response}") measure = response.json()["sensor_measure"] sensor_uniq_id = response.json()["sensor_unique_id"] + if not measure or not sensor_uniq_id: + # No sensor data is associated with this model run. + return None dt_from = earliest_timestamp if last_timestamp: dt_to = last_timestamp @@ -163,9 +167,14 @@ def fetch_run_data(run_id: int | str) -> Dict[str, Any]: if last_timestamp is None or end_timestamp > last_timestamp: last_timestamp = end_timestamp if earliest_timestamp and last_timestamp: + # Extend the period for which we have measure data backwards by 10%, to provide + # some context for the model predictions. + earliest_timestamp = earliest_timestamp - 0.1 * ( + last_timestamp - earliest_timestamp + ) sensor_data = get_run_sensor_data(run_id, earliest_timestamp, last_timestamp) else: - sensor_data = {} + sensor_data = None return {"pred_data": pred_data, "sensor_data": sensor_data} diff --git a/dtbase/frontend/app/models/templates/models.html b/dtbase/frontend/app/models/templates/models.html index cd7465d..7e83257 100644 --- a/dtbase/frontend/app/models/templates/models.html +++ b/dtbase/frontend/app/models/templates/models.html @@ -86,54 +86,9 @@

Model predictions

{{ super()}}