forked from fixin.me/fixin.me
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:
0
app/assets/images/pictograms/chart-line.svg
Normal file
0
app/assets/images/pictograms/chart-line.svg
Normal file
@@ -111,6 +111,11 @@ svg {
|
|||||||
svg:last-child {
|
svg:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
.chart-panel svg {
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -656,16 +661,30 @@ li::marker {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
body[data-measurements-view=wide] .measurements-compact,
|
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;
|
display: none;
|
||||||
}
|
}
|
||||||
body[data-measurements-view=compact] .view-toggle[data-view=compact],
|
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);
|
background-color: var(--color-blue);
|
||||||
border-color: var(--color-blue);
|
border-color: var(--color-blue);
|
||||||
color: white;
|
color: white;
|
||||||
fill: 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 {
|
#measurements tr.grouped td {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,60 @@ function buildWideTable() {
|
|||||||
wideContainer.appendChild(table);
|
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() {
|
function getMeasurementsView() {
|
||||||
return localStorage.getItem('measurements-view') || 'compact';
|
return localStorage.getItem('measurements-view') || 'compact';
|
||||||
}
|
}
|
||||||
@@ -124,6 +178,7 @@ function getMeasurementsView() {
|
|||||||
function applyMeasurementsView(view) {
|
function applyMeasurementsView(view) {
|
||||||
document.body.dataset.measurementsView = view;
|
document.body.dataset.measurementsView = view;
|
||||||
if (view === 'wide') buildWideTable();
|
if (view === 'wide') buildWideTable();
|
||||||
|
if (view === 'charts') buildCharts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMeasurementsView(view) {
|
function setMeasurementsView(view) {
|
||||||
@@ -139,7 +194,9 @@ document.addEventListener('turbo:load', function() {
|
|||||||
applyMeasurementsView(getMeasurementsView());
|
applyMeasurementsView(getMeasurementsView());
|
||||||
new MutationObserver(function() {
|
new MutationObserver(function() {
|
||||||
groupMeasurements();
|
groupMeasurements();
|
||||||
if (getMeasurementsView() === 'wide') buildWideTable();
|
var view = getMeasurementsView();
|
||||||
|
if (view === 'wide') buildWideTable();
|
||||||
|
if (view === 'charts') buildCharts();
|
||||||
}).observe(tbody, {
|
}).observe(tbody, {
|
||||||
childList: true, subtree: true,
|
childList: true, subtree: true,
|
||||||
attributes: true, attributeFilter: ['style']
|
attributes: true, attributeFilter: ['style']
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
<%= stylesheet_link_tag "spreadsheet" %>
|
<%= stylesheet_link_tag "spreadsheet" %>
|
||||||
|
<script src="https://cdn.plot.ly/plotly-basic-2.35.2.min.js"></script>
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
<%#= turbo_page_requires_reload_tag %>
|
<%#= turbo_page_requires_reload_tag %>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<%= image_button_tag '', 'view-columns', name: nil, type: 'button',
|
<%= image_button_tag '', 'view-columns', name: nil, type: 'button',
|
||||||
class: 'view-toggle', title: t('.view_wide'),
|
class: 'view-toggle', title: t('.view_wide'),
|
||||||
data: {view: 'wide'}, onclick: "setMeasurementsView('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')" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-area measurements-section">
|
<div class="main-area measurements-section">
|
||||||
@@ -34,5 +37,6 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id="measurements-wide" class="measurements-wide"></div>
|
<div id="measurements-wide" class="measurements-wide"></div>
|
||||||
|
<div id="measurements-charts" class="measurements-charts"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ en:
|
|||||||
new_measurement: Add measurement
|
new_measurement: Add measurement
|
||||||
view_compact: Compact view
|
view_compact: Compact view
|
||||||
view_wide: Wide view
|
view_wide: Wide view
|
||||||
|
view_charts: Charts
|
||||||
readout:
|
readout:
|
||||||
edit: Edit
|
edit: Edit
|
||||||
destroy: Delete
|
destroy: Delete
|
||||||
|
|||||||
Reference in New Issue
Block a user