forked from fixin.me/fixin.me
Refactor charts: dedicated nav tab, JSON data transport, tests
Replace the toggle-view approach and hidden DOM data carrier with a proper dedicated Charts page: - Move Charts out of Measurements view toggles into its own nav tab and route (GET /charts) - ChartsController serializes readout data as JSON (ordered by taken_at); the view embeds it in a <script type="application/json"> element instead of rendering a hidden copy of the measurements partial just to ferry data attributes to JS - buildCharts() reads from the JSON element directly — no DOM parsing, no sorting in JS (server already orders the data) - Turbo load handler detects the charts page via #charts-data presence - Add controller tests (authentication, data shape, ordering, data isolation between users) and system tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,57 +118,51 @@ function buildWideTable() {
|
||||
}
|
||||
|
||||
function buildCharts() {
|
||||
var tbody = document.getElementById('measurements');
|
||||
var container = document.getElementById('measurements-charts');
|
||||
if (!tbody || !container) return;
|
||||
var dataEl = document.getElementById('charts-data');
|
||||
if (!container || !dataEl) return;
|
||||
|
||||
var rows = Array.from(tbody.querySelectorAll('tr[data-taken-at]'));
|
||||
var readouts = JSON.parse(dataEl.textContent);
|
||||
container.innerHTML = '';
|
||||
if (rows.length === 0) return;
|
||||
if (readouts.length === 0) return;
|
||||
|
||||
// Collect data per quantity, preserving insertion order
|
||||
// Data arrives sorted by taken_at from the server; group into per-quantity traces
|
||||
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: [] });
|
||||
readouts.forEach(function(r) {
|
||||
if (!r.takenAt) return;
|
||||
if (!quantities.has(r.quantityId)) {
|
||||
quantities.set(r.quantityId, { name: r.quantityName, unit: r.unit, x: [], y: [] });
|
||||
}
|
||||
var q = quantities.get(qid);
|
||||
q.x.push(r.dataset.takenAt);
|
||||
q.y.push(parseFloat(r.dataset.value));
|
||||
var q = quantities.get(r.quantityId);
|
||||
q.x.push(r.takenAt);
|
||||
q.y.push(r.value);
|
||||
});
|
||||
|
||||
var traces = [];
|
||||
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));
|
||||
traces.push({
|
||||
x: q.x,
|
||||
y: q.y,
|
||||
mode: 'lines+markers',
|
||||
type: 'scatter',
|
||||
name: q.name + ' (' + q.unit + ')',
|
||||
marker: { size: 5 }
|
||||
});
|
||||
});
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.className = 'chart-panel';
|
||||
container.appendChild(div);
|
||||
|
||||
Plotly.newPlot(div, traces, {
|
||||
xaxis: { type: 'date', tickformat: '%Y-%m-%d %H:%M' },
|
||||
yaxis: {},
|
||||
margin: { t: 20, r: 20, b: 80, l: 60 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
font: { family: 'system-ui' },
|
||||
legend: { orientation: 'h', y: -0.25 }
|
||||
}, { responsive: true, displayModeBar: false });
|
||||
}
|
||||
|
||||
function getMeasurementsView() {
|
||||
@@ -178,7 +172,6 @@ function getMeasurementsView() {
|
||||
function applyMeasurementsView(view) {
|
||||
document.body.dataset.measurementsView = view;
|
||||
if (view === 'wide') buildWideTable();
|
||||
if (view === 'charts') buildCharts();
|
||||
}
|
||||
|
||||
function setMeasurementsView(view) {
|
||||
@@ -188,6 +181,10 @@ function setMeasurementsView(view) {
|
||||
window.setMeasurementsView = setMeasurementsView
|
||||
|
||||
document.addEventListener('turbo:load', function() {
|
||||
if (document.getElementById('charts-data')) {
|
||||
buildCharts();
|
||||
return;
|
||||
}
|
||||
var tbody = document.getElementById('measurements');
|
||||
if (!tbody) return;
|
||||
groupMeasurements();
|
||||
@@ -196,7 +193,6 @@ document.addEventListener('turbo:load', function() {
|
||||
groupMeasurements();
|
||||
var view = getMeasurementsView();
|
||||
if (view === 'wide') buildWideTable();
|
||||
if (view === 'charts') buildCharts();
|
||||
}).observe(tbody, {
|
||||
childList: true, subtree: true,
|
||||
attributes: true, attributeFilter: ['style']
|
||||
|
||||
Reference in New Issue
Block a user