diff --git a/app/assets/images/pictograms/chart-line.svg b/app/assets/images/pictograms/chart-line.svg new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 01cbe75..581a509 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -111,6 +111,11 @@ svg { svg:last-child { margin-right: 0; } +.chart-panel svg { + height: auto; + margin: 0; + width: auto; +} textarea { margin: 0; } @@ -656,16 +661,30 @@ li::marker { overflow-x: auto; } body[data-measurements-view=wide] .measurements-compact, -body[data-measurements-view=compact] .measurements-wide { +body[data-measurements-view=wide] .measurements-charts, +body[data-measurements-view=compact] .measurements-wide, +body[data-measurements-view=compact] .measurements-charts, +body[data-measurements-view=charts] .measurements-compact, +body[data-measurements-view=charts] .measurements-wide { display: none; } body[data-measurements-view=compact] .view-toggle[data-view=compact], -body[data-measurements-view=wide] .view-toggle[data-view=wide] { +body[data-measurements-view=wide] .view-toggle[data-view=wide], +body[data-measurements-view=charts] .view-toggle[data-view=charts] { background-color: var(--color-blue); border-color: var(--color-blue); color: white; fill: white; } +.measurements-charts { + display: flex; + flex-wrap: wrap; + gap: 1em; +} +.chart-panel { + flex: 1 1 400px; + min-width: 300px; +} #measurements tr.grouped td { border-top: none; } diff --git a/app/javascript/application.js b/app/javascript/application.js index 4eb29d3..bcea64f 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -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'] diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6167976..1fe0cc7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,6 +14,7 @@ <%= csp_meta_tag %> <%= stylesheet_link_tag "spreadsheet" %> + <%= javascript_importmap_tags %> <%#= turbo_page_requires_reload_tag %> diff --git a/app/views/measurements/index.html.erb b/app/views/measurements/index.html.erb index 2aac86b..5d4d00c 100644 --- a/app/views/measurements/index.html.erb +++ b/app/views/measurements/index.html.erb @@ -11,6 +11,9 @@ <%= image_button_tag '', 'view-columns', name: nil, type: 'button', class: 'view-toggle', title: t('.view_wide'), data: {view: 'wide'}, onclick: "setMeasurementsView('wide')" %> + <%= image_button_tag '', 'chart-line', name: nil, type: 'button', + class: 'view-toggle', title: t('.view_charts'), + data: {view: 'charts'}, onclick: "setMeasurementsView('charts')" %>
@@ -34,5 +37,6 @@
+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 5f70cc5..d5589c0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -99,6 +99,7 @@ en: new_measurement: Add measurement view_compact: Compact view view_wide: Wide view + view_charts: Charts readout: edit: Edit destroy: Delete