Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
24e8435
feat: use ECharts to make the asset charts faster and more interactive
Ahmad-Wahid Jun 12, 2026
a22650a
feat: use ECharts to make the asset charts faster and more interactive
Ahmad-Wahid Jun 12, 2026
27e32b8
add changelog
Ahmad-Wahid Jun 12, 2026
9ad09bd
improve the EChart by adding missing features as we have in the exist…
Ahmad-Wahid Jun 12, 2026
0b5955c
drop the changes that were made for speeding the vegalite
Ahmad-Wahid Jun 12, 2026
ee2d367
connect histgram horizentally and move x-axis to the top of the chart
Ahmad-Wahid Jun 12, 2026
828415d
stack the legends below the subplots if option is chosen
Ahmad-Wahid Jun 12, 2026
6ed06f0
fix: use asset name with sensor name in the chart legends
Ahmad-Wahid Jun 12, 2026
3a50883
fix: show source chart lines
Ahmad-Wahid Jun 12, 2026
9572d44
fix: vertical concatination of subplots
Ahmad-Wahid Jun 12, 2026
1f56d00
fix: ECharts
Ahmad-Wahid Jun 13, 2026
af2a6e1
fix: derive sensor-type from shared unit and improve ECharts legends
Ahmad-Wahid Jun 23, 2026
383993a
fix: export full legends without toolbox and drop restore button
Ahmad-Wahid Jun 23, 2026
db29e6a
fix: match axis and title font size and color to Vega-Lite
Ahmad-Wahid Jun 23, 2026
cecdd86
fix: show sensor-page legend with Source title and Vega-Lite-style la…
Ahmad-Wahid Jun 23, 2026
49558ad
fix: add minor gridlines to line and bar charts like Vega-Lite
Ahmad-Wahid Jun 23, 2026
e07f66c
feat: match fast ECharts charts to Vega-Lite (annotations, colors, ra…
Ahmad-Wahid Jun 24, 2026
ddca605
perf: smooth fast-chart pan/zoom and speed up rendering
Ahmad-Wahid Jun 24, 2026
5fa86d1
fix: complete heatmap PNG/SVG export and reorder sensor-page toolbox
Ahmad-Wahid Jun 27, 2026
b927611
Merge remote-tracking branch 'origin/main' into feat/better-and-fast-…
Ahmad-Wahid Jul 1, 2026
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
18 changes: 18 additions & 0 deletions flexmeasures/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ def basic_admin_auth():

def add_jinja_filters(app):
from flexmeasures.ui.utils.view_utils import asset_icon_name, username, accountname
from flexmeasures.data.models.charts.belief_charts import (
determine_shared_sensor_type,
)

def shared_sensor_type(sensors):
"""Mirror the Vega-Lite y-axis title logic: if all sensors share a
sensor type use it, otherwise derive the dimension from the shared unit
(e.g. a group of kW sensors becomes "power").

``sensor_type`` is normally set lazily inside the chart methods, so we
populate it here (as ``get_attribute("sensor_type", name)``) before
delegating to the shared helper.
"""
sensors = list(sensors)
for sensor in sensors:
sensor.sensor_type = sensor.get_attribute("sensor_type", sensor.name)
return determine_shared_sensor_type(sensors)

app.jinja_env.filters["zip"] = zip # Allow zip function in templates
app.jinja_env.add_extension(
Expand All @@ -143,6 +160,7 @@ def add_jinja_filters(app):
app.jinja_env.filters["capitalize"] = capitalize
app.jinja_env.filters["pluralize"] = pluralize
app.jinja_env.filters["parameterize"] = parameterize
app.jinja_env.filters["shared_sensor_type"] = shared_sensor_type
app.jinja_env.filters["isnull"] = pd.isnull
app.jinja_env.filters["hide_nan_if_desired"] = lambda x: (
""
Expand Down
172 changes: 172 additions & 0 deletions flexmeasures/ui/static/js/chart-perf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Chart load performance reporting.
*
* Measures every chart load (initial page load, date range changes, data
* refreshes, renderer switches) and produces a report with:
* - per-request network timings and payload sizes (via the Resource Timing API)
* - chart render time (measured by the caller around the render calls)
* - number of data rows and total wall-clock time
*
* Reports are shown in a small floating panel (only on pages that load a
* chart), logged with console.table, and downloadable as JSON.
*
* Usage:
* const report = beginChartLoad("initial page load", "standard (Vega-Lite)");
* ... fetch data and render, wrapping render calls with recordRender ...
* finishChartLoad(report, { rows: data.length });
*/

const history = [];
const MAX_HISTORY = 20;

// Endpoints that count as chart traffic
const CHART_ENDPOINT_PATTERNS = [
"/chart_data",
"/chart?",
"/chart_annotations",
"/kpis",
];

/**
* Start measuring a chart load.
*
* @param {string} label - What triggered the load (e.g. "initial page load").
* @param {string} mode - Which renderer is active (e.g. "fast (ECharts)").
* @returns {Object} - The report object to pass to recordRender/finishChartLoad.
*/
export function beginChartLoad(label, mode) {
return {
label: label,
mode: mode,
startedAt: new Date().toISOString(),
t0: performance.now(),
renderMs: 0,
rows: null,
};
}

/**
* Add chart render time (call with performance.now() deltas around render calls).
*/
export function recordRender(report, ms) {
report.renderMs += ms;
}

/**
* Finalize the report: collect network timings, log and display it.
*
* @param {Object} report - The report from beginChartLoad.
* @param {Object} [extra] - Extra fields, e.g. { rows: 1234 }.
*/
export function finishChartLoad(report, extra) {
Object.assign(report, extra || {});
report.totalMs = round(performance.now() - report.t0);
report.renderMs = round(report.renderMs);
report.requests = performance
.getEntriesByType("resource")
.filter(
(e) =>
e.startTime >= report.t0 - 1 &&
CHART_ENDPOINT_PATTERNS.some((p) => e.name.includes(p))
)
.map((e) => ({
endpoint: shortEndpoint(e.name),
durationMs: round(e.duration),
transferKB: round((e.transferSize || 0) / 1024),
decodedKB: round((e.decodedBodySize || 0) / 1024),
fromCache: e.transferSize === 0 && e.decodedBodySize > 0,
}));
report.networkMs = round(
report.requests.reduce((sum, r) => Math.max(sum, r.durationMs), 0)
);
delete report.t0;

history.push(report);
if (history.length > MAX_HISTORY) {
history.shift();
}

console.groupCollapsed(
"[chart-perf] " +
report.label +
" | " +
report.mode +
" | total " +
report.totalMs +
" ms"
);
console.table(report.requests);
console.log(report);
console.groupEnd();

renderPanel(report);
}

function round(x) {
return Math.round((x + Number.EPSILON) * 10) / 10;
}

function shortEndpoint(url) {
try {
const u = new URL(url, window.location.origin);
return u.pathname;
} catch (e) {
return url;
}
}

function renderPanel(report) {
let panel = document.getElementById("chart-perf-panel");
if (!panel) {
panel = document.createElement("div");
panel.id = "chart-perf-panel";
panel.style.cssText =
"position:fixed;bottom:16px;right:16px;z-index:10000;max-width:340px;" +
"background:#fff;color:#222;border:1px solid #ccc;border-radius:8px;" +
"box-shadow:0 2px 12px rgba(0,0,0,.25);font:12px/1.5 monospace;padding:10px 12px;";
document.body.appendChild(panel);
}
const requestRows = report.requests
.map(
(r) =>
"<tr><td style='padding-right:8px;'>" +
r.endpoint +
"</td><td style='text-align:right;'>" +
r.durationMs +
" ms</td><td style='text-align:right;padding-left:8px;'>" +
(r.fromCache ? "cache" : r.transferKB + " KB") +
"</td></tr>"
)
.join("");
panel.innerHTML =
"<div style='display:flex;justify-content:space-between;align-items:center;gap:8px;'>" +
"<b>Chart load report #" + history.length + "</b>" +
"<span id='chart-perf-close' style='cursor:pointer;font-size:14px;' title='Hide report'>&times;</span>" +
"</div>" +
"<div>" + report.label + " &mdash; " + report.mode + "</div>" +
"<table style='margin-top:4px;'>" +
"<tr><td>data rows</td><td style='text-align:right;'><b>" + (report.rows === null ? "n/a" : report.rows) + "</b></td></tr>" +
"<tr><td>network (longest request)</td><td style='text-align:right;'><b>" + report.networkMs + " ms</b></td></tr>" +
"<tr><td>chart render</td><td style='text-align:right;'><b>" + report.renderMs + " ms</b></td></tr>" +
"<tr><td>total</td><td style='text-align:right;'><b>" + report.totalMs + " ms</b></td></tr>" +
"</table>" +
"<details style='margin-top:4px;'><summary style='cursor:pointer;'>" +
report.requests.length + " request(s)</summary>" +
"<table>" + requestRows + "</table></details>" +
"<a href='#' id='chart-perf-download' style='display:inline-block;margin-top:4px;'>Download history (JSON)</a>";

panel.querySelector("#chart-perf-close").addEventListener("click", () => {
panel.remove(); // reappears on the next chart load
});
panel.querySelector("#chart-perf-download").addEventListener("click", (e) => {
e.preventDefault();
const blob = new Blob([JSON.stringify(history, null, 2)], {
type: "application/json",
});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "chart-load-reports.json";
a.click();
URL.revokeObjectURL(a.href);
});
}
Loading
Loading