fixed line chart noise x axis and add more date marker to split them up

This commit is contained in:
RNL
2026-02-25 23:05:24 +11:00
parent 7ca4cd57b5
commit 6d284ae79c
5 changed files with 463 additions and 137 deletions

View File

@@ -0,0 +1,302 @@
// Filament Detail Chart — Usage History
// Depends on: chart.js, chartjs-plugin-annotation
// Config injected by template: FILAMENT_USAGE_API_URL
let usageChart = null;
// Register annotation plugin once it's available
if (typeof ChartAnnotation !== 'undefined') {
Chart.register(ChartAnnotation);
}
// ── Time-select population ──────────────────────────────────────────────────
const startTimeSelect = document.getElementById('filamentStartTime');
const endTimeSelect = document.getElementById('filamentEndTime');
if (startTimeSelect && endTimeSelect) {
for (let h = 0; h < 24; h++) {
for (let m = 0; m < 60; m += 30) {
const t = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
startTimeSelect.add(new Option(t, t));
endTimeSelect.add(new Option(t, t));
}
}
// End-time gets one extra option so the last minute of the day is reachable
endTimeSelect.add(new Option('23:59', '23:59'));
startTimeSelect.value = '00:00';
endTimeSelect.value = '23:59';
}
// ── Default date inputs (last 24 h) ────────────────────────────────────────
(function setDefaultDates() {
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const sd = document.getElementById('filamentStartDate');
const ed = document.getElementById('filamentEndDate');
if (sd) sd.value = yesterday.toISOString().split('T')[0];
if (ed) ed.value = now.toISOString().split('T')[0];
}());
// ── Full-day checkbox ───────────────────────────────────────────────────────
const fullDayCheckbox = document.getElementById('filamentFullDayCheckbox');
if (fullDayCheckbox) {
fullDayCheckbox.addEventListener('change', function () {
const isFullDay = this.checked;
if (startTimeSelect) startTimeSelect.disabled = isFullDay;
if (endTimeSelect) endTimeSelect.disabled = isFullDay;
});
}
// ── Helpers ─────────────────────────────────────────────────────────────────
/**
* Build date-separator annotations from "YYYY-MM-DD HH:MM" timestamp strings.
* Places a vertical dotted line at each day boundary, label at the bottom.
*/
function buildFilamentDateSeparators(timestamps) {
const annotations = {};
if (!timestamps || timestamps.length < 2) return annotations;
let count = 0;
for (let i = 1; i < timestamps.length; i++) {
const prevDate = timestamps[i - 1].split(' ')[0];
const currDate = timestamps[i].split(' ')[0];
if (currDate !== prevDate) {
const d = new Date(currDate + 'T00:00:00');
const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
annotations['dateSep_' + count] = {
type: 'line',
scaleID: 'x',
value: i,
borderColor: 'rgba(128, 128, 128, 0.45)',
borderWidth: 1,
borderDash: [4, 4],
drawTime: 'beforeDatasetsDraw',
label: {
display: true,
content: label,
position: 'end',
backgroundColor: 'rgba(100, 100, 100, 0.65)',
color: '#fff',
font: { size: 9 },
padding: { x: 4, y: 2 }
}
};
count++;
}
}
return annotations;
}
/**
* Build x-axis tick options that adapt to the date span.
*
* autoSkip: true — Chart.js selects evenly-spaced tick positions.
* maxTicksLimit — caps how many ticks are drawn.
* callback — formats the label at each chosen tick position.
*
* ≤1 day : up to 12 ticks, show "HH:MM"
* 27 days: up to dayCount×4 ticks (≤28), show "Feb 22 06:00"
* >7 days : up to min(dayCount, 20) ticks, show "Feb 22"
*/
function filamentXAxisTicks(isDarkMode, timestamps) {
const tickColor = isDarkMode ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)';
const dayCount = (timestamps && timestamps.length > 0)
? new Set(timestamps.map(t => t.split(' ')[0])).size
: 1;
let maxTicksLimit, formatCb;
if (dayCount <= 1) {
maxTicksLimit = 12;
formatCb = function (val) {
const label = this.getLabelForValue(val);
return label ? label.slice(11, 16) : ''; // "HH:MM"
};
} else if (dayCount <= 7) {
maxTicksLimit = Math.min(dayCount * 4, 28);
formatCb = function (val) {
const label = this.getLabelForValue(val);
if (!label) return '';
const datePart = label.split(' ')[0];
const timePart = label.length >= 16 ? label.slice(11, 16) : '';
const d = new Date(datePart + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' + timePart;
};
} else {
maxTicksLimit = Math.min(dayCount, 20);
formatCb = function (val) {
const label = this.getLabelForValue(val);
if (!label) return '';
const datePart = label.split(' ')[0];
const d = new Date(datePart + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
}
return {
color: tickColor,
autoSkip: true,
maxTicksLimit: maxTicksLimit,
maxRotation: 45,
minRotation: 0,
callback: formatCb
};
}
// ── Chart fetch / render ────────────────────────────────────────────────────
/**
* Fetch and render the usage chart.
*
* @param {boolean} sendDates When false (initial load / reset), no date params
* are sent so the backend can apply its default
* "last 24h or fallback to last available" logic.
* When true (explicit Refresh), the current input
* values are sent as-is.
*/
async function fetchFilamentUsageData(sendDates = true) {
const startDate = document.getElementById('filamentStartDate').value;
const endDate = document.getElementById('filamentEndDate').value;
const isFullDay = fullDayCheckbox ? fullDayCheckbox.checked : true;
const startTime = isFullDay ? '00:00' : (startTimeSelect ? startTimeSelect.value : '00:00');
const endTime = isFullDay ? '23:59' : (endTimeSelect ? endTimeSelect.value : '23:59');
const params = new URLSearchParams();
if (sendDates) {
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (startTime) params.append('start_time', startTime);
if (endTime) params.append('end_time', endTime);
}
try {
const response = await fetch(FILAMENT_USAGE_API_URL + '?' + params.toString());
const data = await response.json();
// If the backend used the fallback window, sync the date inputs so the
// user can see and extend the range from that starting point.
if (data.fallback_used && data.timestamps && data.timestamps.length > 0) {
const firstDate = data.timestamps[0].split(' ')[0];
const lastDate = data.timestamps[data.timestamps.length - 1].split(' ')[0];
const sd = document.getElementById('filamentStartDate');
const ed = document.getElementById('filamentEndDate');
if (sd) sd.value = firstDate;
if (ed) ed.value = lastDate;
}
// Update date-range label
const dateRangeSpan = document.getElementById('filamentDateRange');
if (dateRangeSpan) {
if (data.fallback_used) {
dateRangeSpan.textContent = '(Last available data — 24h window)';
} else if (startDate && endDate && sendDates) {
dateRangeSpan.textContent = `(${startDate} to ${endDate})`;
} else {
dateRangeSpan.textContent = '(Last 24 Hours)';
}
}
const isDarkMode = document.documentElement.getAttribute('data-coreui-theme') === 'dark';
const tickColor = isDarkMode ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)';
const gridColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
const sepAnnotations = buildFilamentDateSeparators(data.timestamps);
const xTicks = filamentXAxisTicks(isDarkMode, data.timestamps);
if (usageChart) {
usageChart.data.labels = data.timestamps;
usageChart.data.datasets[0].data = data.remaining;
usageChart.options.plugins.annotation.annotations = sepAnnotations;
usageChart.options.scales.x.ticks = xTicks;
usageChart.update();
} else {
const ctx = document.getElementById('usageChart').getContext('2d');
usageChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'Remaining %',
data: data.remaining,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
fill: true,
pointRadius: 0,
pointHoverRadius: 3,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
annotation: { annotations: sepAnnotations },
legend: {
position: 'top',
labels: { color: tickColor }
},
tooltip: {
callbacks: {
label: function (ctx) {
return 'Remaining: ' + ctx.parsed.y + '%';
}
}
}
},
scales: {
x: {
ticks: xTicks,
grid: { color: gridColor }
},
y: {
beginAtZero: true,
max: 100,
ticks: {
color: tickColor,
callback: function (v) { return v + '%'; }
},
grid: { color: gridColor }
}
}
}
});
}
} catch (error) {
console.error('Error fetching filament usage data:', error);
}
}
// ── Event listeners ─────────────────────────────────────────────────────────
const refreshBtn = document.getElementById('refreshFilamentChart');
const resetBtn = document.getElementById('resetFilamentChart');
if (refreshBtn) {
// Refresh: honour whatever the user has typed in the date inputs
refreshBtn.addEventListener('click', function () { fetchFilamentUsageData(true); });
}
if (resetBtn) {
resetBtn.addEventListener('click', function () {
// Reset inputs to "last 24 hours" defaults, then let the backend
// decide (fallback if no recent data).
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const sd = document.getElementById('filamentStartDate');
const ed = document.getElementById('filamentEndDate');
if (sd) sd.value = yesterday.toISOString().split('T')[0];
if (ed) ed.value = now.toISOString().split('T')[0];
if (fullDayCheckbox) fullDayCheckbox.checked = true;
if (startTimeSelect) startTimeSelect.disabled = true;
if (endTimeSelect) endTimeSelect.disabled = true;
fetchFilamentUsageData(false);
});
}
// ── Initial load — no dates so backend fallback can fire ───────────────────
fetchFilamentUsageData(false);

View File

@@ -55,7 +55,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
}, },
{ {
@@ -67,7 +67,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
} }
] ]
@@ -90,7 +90,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
}, },
{ {
@@ -102,7 +102,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
} }
] ]
@@ -125,7 +125,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
fill: true fill: true
} }
] ]
@@ -148,7 +148,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
}, },
{ {
@@ -159,7 +159,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
} }
] ]
@@ -182,7 +182,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
} }
] ]
@@ -246,7 +246,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
yAxisID: 'y', yAxisID: 'y',
spanGaps: true spanGaps: true
}, },
@@ -258,7 +258,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
yAxisID: 'y1', yAxisID: 'y1',
spanGaps: true spanGaps: true
} }
@@ -342,7 +342,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
fill: true fill: true
}, },
{ {
@@ -354,7 +354,7 @@ function initPrinterCharts(printerData, apiUrl) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: true spanGaps: true
} }
] ]
@@ -452,6 +452,11 @@ function initPrinterCharts(printerData, apiUrl) {
} }
}); });
// Add date separator markers when data spans multiple days
if (printerData.dates && printerData.dates.length > 0) {
applyDateSeparatorsToAllPrinterCharts(printerData.timestamps, printerData.dates);
}
// Set up theme observer for dynamic theme switching // Set up theme observer for dynamic theme switching
setupThemeObserver(); setupThemeObserver();
} }
@@ -623,7 +628,7 @@ function createFilamentDatasets(filamentTimeline, timestamps) {
tension: 0.3, tension: 0.3,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
pointHoverRadius: 5, pointHoverRadius: 3,
spanGaps: false // Don't connect across null values (filament changes) spanGaps: false // Don't connect across null values (filament changes)
}); });
}); });
@@ -731,3 +736,79 @@ function setupThemeObserver() {
attributeFilter: ['data-coreui-theme'] attributeFilter: ['data-coreui-theme']
}); });
} }
/**
* Build date-separator annotations for multi-day charts.
* Detects where consecutive dates differ and returns a vertical dotted line
* annotation at each boundary index, labelled with the new date.
*
* @param {string[]} timestamps - HH:MM display labels (one per data point)
* @param {string[]} dates - YYYY-MM-DD dates (same length as timestamps)
* @returns {Object} chartjs-plugin-annotation annotations keyed as "dateSep_N"
*/
function buildDateSeparatorAnnotations(timestamps, dates) {
const annotations = {};
if (!dates || dates.length < 2) return annotations;
let count = 0;
for (let i = 1; i < dates.length; i++) {
if (dates[i] !== dates[i - 1]) {
// Format date as "Feb 25" for a compact label
const d = new Date(dates[i] + 'T00:00:00');
const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
annotations['dateSep_' + count] = {
type: 'line',
scaleID: 'x',
value: i,
borderColor: 'rgba(128, 128, 128, 0.45)',
borderWidth: 1,
borderDash: [4, 4],
drawTime: 'beforeDatasetsDraw',
label: {
display: true,
content: label,
position: 'end',
backgroundColor: 'rgba(100, 100, 100, 0.65)',
color: '#fff',
font: { size: 9 },
padding: { x: 4, y: 2 }
}
};
count++;
}
}
return annotations;
}
/**
* Apply date-separator annotations to all printer charts.
* Preserves any existing "marker_*" (project marker) annotations.
*
* @param {string[]} timestamps
* @param {string[]} dates
*/
function applyDateSeparatorsToAllPrinterCharts(timestamps, dates) {
const sepAnnotations = buildDateSeparatorAnnotations(timestamps, dates);
const charts = [
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
charts.forEach(chart => {
if (!chart) return;
if (!chart.options.plugins.annotation) {
chart.options.plugins.annotation = { annotations: {} };
}
const existing = chart.options.plugins.annotation.annotations;
// Remove any old dateSep_* entries then re-add updated ones
Object.keys(existing).forEach(key => {
if (key.startsWith('dateSep_')) delete existing[key];
});
Object.assign(existing, sepAnnotations);
chart.update('none');
});
}

View File

@@ -77,11 +77,12 @@ function populateTimeDropdowns(startSelect, endSelect) {
} }
times.forEach(time => { times.forEach(time => {
const option1 = new Option(time, time); startSelect.add(new Option(time, time));
const option2 = new Option(time, time); endSelect.add(new Option(time, time));
startSelect.add(option1);
endSelect.add(option2);
}); });
// End-time gets one extra option so the last minute of the day is reachable
endSelect.add(new Option('23:59', '23:59'));
} }
/** /**
@@ -235,6 +236,11 @@ function updateAllPrinterCharts(data) {
filamentTimelineChart.update(); filamentTimelineChart.update();
} }
// Apply date separator markers (multi-day views)
if (data.dates && data.dates.length > 0) {
applyDateSeparatorsToAllPrinterCharts(data.timestamps, data.dates);
}
// Add project markers to all charts // Add project markers to all charts
if (data.project_markers) { if (data.project_markers) {
addProjectMarkersToCharts(data.project_markers, data.timestamps); addProjectMarkersToCharts(data.project_markers, data.timestamps);
@@ -275,8 +281,11 @@ function addProjectMarkersToCharts(markers, timestamps) {
chart.options.plugins.annotation = { annotations: {} }; chart.options.plugins.annotation = { annotations: {} };
} }
// Clear existing project markers // Clear existing project markers but preserve date-separator annotations
chart.options.plugins.annotation.annotations = {}; const allAnnotations = chart.options.plugins.annotation.annotations;
Object.keys(allAnnotations).forEach(key => {
if (!key.startsWith('dateSep_')) delete allAnnotations[key];
});
// Track active tooltip // Track active tooltip
let activeMarkerTooltip = null; let activeMarkerTooltip = null;

View File

@@ -84,7 +84,7 @@
<div class="card-header"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2"> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div> <div>
<strong>Chart Filters</strong> <strong>Filament Usage History</strong>
<span class="text-muted" id="filamentDateRange">(Last 24 Hours)</span> <span class="text-muted" id="filamentDateRange">(Last 24 Hours)</span>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <div class="d-flex align-items-center gap-2 flex-wrap">
@@ -111,11 +111,11 @@
</div> </div>
<!-- Buttons --> <!-- Buttons -->
<button type="button" class="btn btn-primary btn-sm" id="refreshFilamentChart"> <button type="button" class="btn btn-primary btn-sm" id="refreshFilamentChart">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-reload"></use></svg> <svg class="icon"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-reload"></use></svg>
Refresh Refresh
</button> </button>
<button type="button" class="btn btn-secondary btn-sm" id="resetFilamentChart"> <button type="button" class="btn btn-secondary btn-sm" id="resetFilamentChart">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-action-undo"></use></svg> <svg class="icon"><use href="{% static 'bambu_run/vendors/coreui-icons-free.svg' %}#cil-action-undo"></use></svg>
Reset Reset
</button> </button>
</div> </div>
@@ -203,126 +203,39 @@
{% block extra_js %} {% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1"></script>
{% if not is_basic_user %} {% if not is_basic_user %}
{# Inject Django-specific values that the static JS file cannot know #}
<script> <script>
const filamentId = {{ filament.pk }}; const FILAMENT_USAGE_API_URL = "{% url 'bambu_run:filament_usage_api' filament.pk %}";
let usageChart = null; </script>
<script src="{% static 'bambu_run/js/filament_detail.js' %}"></script>
// Populate time selects {% else %}
const startTimeSelect = document.getElementById('filamentStartTime'); <script>
const endTimeSelect = document.getElementById('filamentEndTime'); document.addEventListener('DOMContentLoaded', function () {
for (let h = 0; h < 24; h++) { const ctx = document.getElementById('usageChart');
for (let m = 0; m < 60; m += 30) { if (ctx) {
const timeStr = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`; new Chart(ctx.getContext('2d'), {
startTimeSelect.add(new Option(timeStr, timeStr));
endTimeSelect.add(new Option(timeStr, timeStr));
}
}
startTimeSelect.value = '00:00';
endTimeSelect.value = '23:30';
// Initialize date inputs to last 24 hours
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
document.getElementById('filamentStartDate').value = yesterday.toISOString().split('T')[0];
document.getElementById('filamentEndDate').value = now.toISOString().split('T')[0];
// Full day checkbox handler
document.getElementById('filamentFullDayCheckbox').addEventListener('change', function() {
const isFullDay = this.checked;
startTimeSelect.disabled = isFullDay;
endTimeSelect.disabled = isFullDay;
});
// Fetch and render chart
async function fetchFilamentUsageData() {
const startDate = document.getElementById('filamentStartDate').value;
const endDate = document.getElementById('filamentEndDate').value;
const isFullDay = document.getElementById('filamentFullDayCheckbox').checked;
const startTime = isFullDay ? '00:00' : startTimeSelect.value;
const endTime = isFullDay ? '23:59' : endTimeSelect.value;
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (startTime) params.append('start_time', startTime);
if (endTime) params.append('end_time', endTime);
try {
const response = await fetch(`{% url 'bambu_run:filament_usage_api' filament.pk %}?${params.toString()}`);
const data = await response.json();
// Update date range display
const dateRangeSpan = document.getElementById('filamentDateRange');
if (startDate && endDate) {
dateRangeSpan.textContent = `(${startDate} to ${endDate})`;
} else {
dateRangeSpan.textContent = '(Last 24 Hours)';
}
// Update chart
if (usageChart) {
usageChart.data.labels = data.timestamps;
usageChart.data.datasets[0].data = data.remaining;
usageChart.update();
} else {
const ctx = document.getElementById('usageChart').getContext('2d');
usageChart = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: data.timestamps, labels: [],
datasets: [{ datasets: [{
label: 'Remaining %', label: 'Remaining %',
data: data.remaining, data: [],
borderColor: 'rgb(75, 192, 192)', borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)', backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3, tension: 0.3,
fill: true fill: true,
pointRadius: 0,
pointHoverRadius: 3,
borderWidth: 2
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
scales: { scales: { y: { beginAtZero: true, max: 100 } }
y: {
beginAtZero: true,
max: 100
} }
}
}
});
}
} catch (error) {
console.error('Error fetching filament usage data:', error);
}
}
// Event listeners
document.getElementById('refreshFilamentChart').addEventListener('click', fetchFilamentUsageData);
document.getElementById('resetFilamentChart').addEventListener('click', function() {
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
document.getElementById('filamentStartDate').value = yesterday.toISOString().split('T')[0];
document.getElementById('filamentEndDate').value = now.toISOString().split('T')[0];
document.getElementById('filamentFullDayCheckbox').checked = true;
startTimeSelect.disabled = true;
endTimeSelect.disabled = true;
fetchFilamentUsageData();
});
// Initial load
fetchFilamentUsageData();
</script>
{% else %}
<script>
// Basic user: render static chart from server-provided data if available
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('usageChart');
if (ctx) {
new Chart(ctx.getContext('2d'), {
type: 'line',
data: { labels: [], datasets: [{ label: 'Remaining %', data: [], borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: true }] },
options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100 } } }
}); });
} }
}); });

View File

@@ -54,6 +54,9 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView):
"timestamps": [ "timestamps": [
m.timestamp.astimezone(tz).strftime("%H:%M") for m in metrics m.timestamp.astimezone(tz).strftime("%H:%M") for m in metrics
], ],
"dates": [
m.timestamp.astimezone(tz).strftime("%Y-%m-%d") for m in metrics
],
"nozzle_temp": [ "nozzle_temp": [
float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics
], ],
@@ -266,6 +269,7 @@ class PrinterDataAPIView(LoginRequiredMixin, View):
data = { data = {
"timestamps": [m.timestamp.astimezone(tz).strftime('%H:%M') for m in metrics], "timestamps": [m.timestamp.astimezone(tz).strftime('%H:%M') for m in metrics],
"timestamps_iso": [m.timestamp.astimezone(tz).isoformat() for m in metrics], "timestamps_iso": [m.timestamp.astimezone(tz).isoformat() for m in metrics],
"dates": [m.timestamp.astimezone(tz).strftime('%Y-%m-%d') for m in metrics],
"nozzle_temp": [float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics], "nozzle_temp": [float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics],
"nozzle_target_temp": [float(m.nozzle_target_temp) if m.nozzle_target_temp else None for m in metrics], "nozzle_target_temp": [float(m.nozzle_target_temp) if m.nozzle_target_temp else None for m in metrics],
"bed_temp": [float(m.bed_temp) if m.bed_temp else None for m in metrics], "bed_temp": [float(m.bed_temp) if m.bed_temp else None for m in metrics],
@@ -410,15 +414,32 @@ class FilamentUsageDataAPIView(LoginRequiredMixin, View):
end_dt = end_dt_naive.replace(tzinfo=tz) end_dt = end_dt_naive.replace(tzinfo=tz)
query = query.filter(printer_metric__timestamp__lte=end_dt) query = query.filter(printer_metric__timestamp__lte=end_dt)
fallback_used = False
if not start_date and not end_date: if not start_date and not end_date:
time_24h_ago = timezone.now() - timedelta(hours=24) time_24h_ago = timezone.now() - timedelta(hours=24)
query = query.filter(printer_metric__timestamp__gte=time_24h_ago) default_query = query.filter(printer_metric__timestamp__gte=time_24h_ago)
if default_query.exists():
snapshots = default_query.order_by('printer_metric__timestamp')
else:
# Fallback: show 24h window ending at the most recent available snapshot
last_snapshot = query.order_by('-printer_metric__timestamp').first()
if last_snapshot:
last_ts = last_snapshot.printer_metric.timestamp
fallback_start = last_ts - timedelta(hours=24)
snapshots = query.filter(
printer_metric__timestamp__gte=fallback_start,
printer_metric__timestamp__lte=last_ts
).order_by('printer_metric__timestamp')
fallback_used = True
else:
snapshots = query.none()
else:
snapshots = query.order_by('printer_metric__timestamp') snapshots = query.order_by('printer_metric__timestamp')
data = { data = {
"timestamps": [s.printer_metric.timestamp.astimezone(tz).strftime('%Y-%m-%d %H:%M') for s in snapshots], "timestamps": [s.printer_metric.timestamp.astimezone(tz).strftime('%Y-%m-%d %H:%M') for s in snapshots],
"remaining": [s.remain_percent for s in snapshots] "remaining": [s.remain_percent for s in snapshots],
"fallback_used": fallback_used,
} }
return JsonResponse(data) return JsonResponse(data)