Add Plotly line charts view to Measurements page

Users can now switch to a Charts view that renders a separate
time-series line chart for each tracked quantity, using Plotly.js
loaded via CDN. Charts are sorted chronologically and styled to
match the app palette. A dedicated toggle button and matching
CSS visibility rules mirror the existing Compact/Wide view pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 22:36:24 +00:00
parent bfd427c9b2
commit 71c22f2280
6 changed files with 85 additions and 3 deletions

View File

@@ -117,6 +117,60 @@ function buildWideTable() {
wideContainer.appendChild(table);
}
function buildCharts() {
var tbody = document.getElementById('measurements');
var container = document.getElementById('measurements-charts');
if (!tbody || !container) return;
var rows = Array.from(tbody.querySelectorAll('tr[data-taken-at]'));
container.innerHTML = '';
if (rows.length === 0) return;
// Collect data per quantity, preserving insertion order
var quantities = new Map();
rows.forEach(function(r) {
var qid = r.dataset.quantityId;
if (!qid) return;
if (!quantities.has(qid)) {
quantities.set(qid, { name: r.dataset.quantityName, unit: r.dataset.unit, x: [], y: [] });
}
var q = quantities.get(qid);
q.x.push(r.dataset.takenAt);
q.y.push(parseFloat(r.dataset.value));
});
quantities.forEach(function(q) {
// Sort by time ascending for the line
var pairs = q.x.map(function(x, i) { return [x, q.y[i]]; });
pairs.sort(function(a, b) { return a[0] < b[0] ? -1 : 1; });
var div = document.createElement('div');
div.className = 'chart-panel';
container.appendChild(div);
(function(el, name, unit, data) {
Plotly.newPlot(el, [{
x: data.map(function(p) { return p[0]; }),
y: data.map(function(p) { return p[1]; }),
mode: 'lines+markers',
type: 'scatter',
name: name,
line: { color: '#009ade' },
marker: { color: '#009ade', size: 6 }
}], {
title: { text: name, font: { size: 14 } },
height: 280,
xaxis: { type: 'date', tickformat: '%Y-%m-%d' },
yaxis: { title: { text: unit } },
margin: { t: 40, r: 20, b: 60, l: 60 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
font: { family: 'system-ui' }
}, { responsive: true, displayModeBar: false });
}(div, q.name, q.unit, pairs));
});
}
function getMeasurementsView() {
return localStorage.getItem('measurements-view') || 'compact';
}
@@ -124,6 +178,7 @@ function getMeasurementsView() {
function applyMeasurementsView(view) {
document.body.dataset.measurementsView = view;
if (view === 'wide') buildWideTable();
if (view === 'charts') buildCharts();
}
function setMeasurementsView(view) {
@@ -139,7 +194,9 @@ document.addEventListener('turbo:load', function() {
applyMeasurementsView(getMeasurementsView());
new MutationObserver(function() {
groupMeasurements();
if (getMeasurementsView() === 'wide') buildWideTable();
var view = getMeasurementsView();
if (view === 'wide') buildWideTable();
if (view === 'charts') buildCharts();
}).observe(tbody, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['style']