mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 22:19:03 +01:00
Initial spin-off of bambu-run from my private project separation
This commit is contained in:
61
bambu_run/static/bambu_run/css/dashboard.css
Normal file
61
bambu_run/static/bambu_run/css/dashboard.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/* Bambu Run Dashboard Styles */
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.infra-card-warning {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #ffb300 100%);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.infra-card-info {
|
||||
background: linear-gradient(135deg, #0dcaf0 0%, #0bb5d6 100%);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.infra-card-danger {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.infra-card-success {
|
||||
background: linear-gradient(135deg, #198754 0%, #157347 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
[data-coreui-theme="dark"] .infra-card-warning {
|
||||
background: linear-gradient(135deg, #ffb300 0%, #ff8f00 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-coreui-theme="dark"] .infra-card-info {
|
||||
background: linear-gradient(135deg, #0bb5d6 0%, #099cbd 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Stat display styling */
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.infra-card-warning .card-text,
|
||||
.infra-card-info .card-text {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
[data-coreui-theme="dark"] .infra-card-warning .card-text,
|
||||
[data-coreui-theme="dark"] .infra-card-info .card-text {
|
||||
opacity: 0.9;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
61
bambu_run/static/bambu_run/js/filament_type_form.js
Normal file
61
bambu_run/static/bambu_run/js/filament_type_form.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Dropdown-assisted text inputs for FilamentType add/edit form.
|
||||
* Reads existing DB values and preset suggestions from json_script tags,
|
||||
* then populates dropdown menus that fill the adjacent text input on click.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build a dropdown menu with existing DB values and preset suggestions.
|
||||
* @param {string} dropdownId - ID of the <ul> dropdown menu element
|
||||
* @param {string} inputId - ID of the text input to fill on click
|
||||
* @param {Array<string>} existingValues - Values already in the database
|
||||
* @param {Array<string>} presetValues - Pre-coded suggestion values
|
||||
*/
|
||||
function buildDropdown(dropdownId, inputId, existingValues, presetValues) {
|
||||
const menu = document.getElementById(dropdownId);
|
||||
|
||||
// Add existing DB values
|
||||
existingValues.forEach(val => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<a class="dropdown-item" href="#">${val}</a>`;
|
||||
li.querySelector('a').addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
document.getElementById(inputId).value = val;
|
||||
});
|
||||
menu.appendChild(li);
|
||||
});
|
||||
|
||||
// Add dotted separator if there were DB values
|
||||
if (existingValues.length > 0) {
|
||||
const sep = document.createElement('li');
|
||||
sep.innerHTML = '<hr class="dropdown-divider" style="border-style: dotted;">';
|
||||
menu.appendChild(sep);
|
||||
}
|
||||
|
||||
// Add preset values (skip duplicates already in DB)
|
||||
const existingSet = new Set(existingValues);
|
||||
presetValues.forEach(val => {
|
||||
if (existingSet.has(val)) return;
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<a class="dropdown-item text-muted" href="#">${val}</a>`;
|
||||
li.querySelector('a').addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
document.getElementById(inputId).value = val;
|
||||
});
|
||||
menu.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Parse data from json_script tags and build all three dropdowns
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const existingTypes = JSON.parse(document.getElementById('existing-types').textContent);
|
||||
const existingSubTypes = JSON.parse(document.getElementById('existing-sub-types').textContent);
|
||||
const existingBrands = JSON.parse(document.getElementById('existing-brands').textContent);
|
||||
const presetTypes = JSON.parse(document.getElementById('preset-types').textContent);
|
||||
const presetSubTypes = JSON.parse(document.getElementById('preset-sub-types').textContent);
|
||||
const presetBrands = JSON.parse(document.getElementById('preset-brands').textContent);
|
||||
|
||||
buildDropdown('type-dropdown', 'id_type', existingTypes, presetTypes);
|
||||
buildDropdown('sub-type-dropdown', 'id_sub_type', existingSubTypes, presetSubTypes);
|
||||
buildDropdown('brand-dropdown', 'id_brand', existingBrands, presetBrands);
|
||||
});
|
||||
713
bambu_run/static/bambu_run/js/printer_charts.js
Normal file
713
bambu_run/static/bambu_run/js/printer_charts.js
Normal file
@@ -0,0 +1,713 @@
|
||||
// 3D Printer Charts Initialization and Management
|
||||
// Chart.js implementation for printer metrics visualization
|
||||
|
||||
let nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart;
|
||||
let wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart;
|
||||
|
||||
function initPrinterCharts(printerData, apiUrl) {
|
||||
// Apply filament card colors
|
||||
applyFilamentColors();
|
||||
|
||||
// Register the annotation plugin
|
||||
if (typeof Chart !== 'undefined' && typeof ChartAnnotation !== 'undefined') {
|
||||
Chart.register(ChartAnnotation);
|
||||
}
|
||||
|
||||
// Detect current theme
|
||||
const isDarkMode = document.documentElement.getAttribute('data-coreui-theme') === 'dark';
|
||||
|
||||
// Set colors based on theme
|
||||
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)';
|
||||
|
||||
// Initialize Nozzle Temperature Chart
|
||||
const nozzleCtx = document.getElementById('nozzleTempChart').getContext('2d');
|
||||
nozzleTempChart = new Chart(nozzleCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Actual Temp',
|
||||
data: printerData.nozzle_temp,
|
||||
borderColor: 'rgb(255, 159, 64)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
},
|
||||
{
|
||||
label: 'Target Temp',
|
||||
data: printerData.nozzle_target_temp,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.05)',
|
||||
borderDash: [5, 5],
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: getTemperatureChartOptions(tickColor, gridColor, '°C')
|
||||
});
|
||||
|
||||
// Initialize Bed Temperature Chart
|
||||
const bedCtx = document.getElementById('bedTempChart').getContext('2d');
|
||||
bedTempChart = new Chart(bedCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Actual Temp',
|
||||
data: printerData.bed_temp,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
},
|
||||
{
|
||||
label: 'Target Temp',
|
||||
data: printerData.bed_target_temp,
|
||||
borderColor: 'rgb(255, 159, 64)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.05)',
|
||||
borderDash: [5, 5],
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: getTemperatureChartOptions(tickColor, gridColor, '°C')
|
||||
});
|
||||
|
||||
// Initialize Print Progress Chart
|
||||
const progressCtx = document.getElementById('printProgressChart').getContext('2d');
|
||||
printProgressChart = new Chart(progressCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Print Progress',
|
||||
data: printerData.print_percent,
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: getPercentageChartOptions(tickColor, gridColor, 'Print Progress')
|
||||
});
|
||||
|
||||
// Initialize Fan Speeds Chart
|
||||
const fanCtx = document.getElementById('fanSpeedsChart').getContext('2d');
|
||||
fanSpeedsChart = new Chart(fanCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cooling Fan',
|
||||
data: printerData.cooling_fan_speed,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
},
|
||||
{
|
||||
label: 'Heatbreak Fan',
|
||||
data: printerData.heatbreak_fan_speed,
|
||||
borderColor: 'rgb(153, 102, 255)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: getPercentageChartOptions(tickColor, gridColor, 'Fan Speed')
|
||||
});
|
||||
|
||||
// Initialize WiFi Signal Chart
|
||||
const wifiCtx = document.getElementById('wifiSignalChart').getContext('2d');
|
||||
wifiSignalChart = new Chart(wifiCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'WiFi Signal',
|
||||
data: printerData.wifi_signal_dbm,
|
||||
borderColor: 'rgb(255, 205, 86)',
|
||||
backgroundColor: 'rgba(255, 205, 86, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return 'Signal: ' + context.parsed.y + ' dBm';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: tickColor },
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
y: {
|
||||
reverse: false, // -30 dBm (better) should be higher than -40 dBm (worse)
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
callback: function(value) {
|
||||
return value + ' dBm';
|
||||
}
|
||||
},
|
||||
grid: { color: gridColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize AMS Conditions Chart
|
||||
const amsCtx = document.getElementById('amsConditionsChart').getContext('2d');
|
||||
amsConditionsChart = new Chart(amsCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Humidity (Raw)',
|
||||
data: printerData.ams_humidity_raw,
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
yAxisID: 'y',
|
||||
spanGaps: true
|
||||
},
|
||||
{
|
||||
label: 'Temperature',
|
||||
data: printerData.ams_temp,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
yAxisID: 'y1',
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: tickColor },
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Humidity',
|
||||
color: tickColor
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgb(54, 162, 235)',
|
||||
callback: function(value) {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Temperature (°C)',
|
||||
color: tickColor
|
||||
},
|
||||
ticks: {
|
||||
color: 'rgb(255, 99, 132)',
|
||||
callback: function(value) {
|
||||
return value + '°C';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Layer Progress Chart
|
||||
const layerCtx = document.getElementById('layerProgressChart').getContext('2d');
|
||||
layerProgressChart = new Chart(layerCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Current Layer',
|
||||
data: printerData.layer_num,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Total Layers',
|
||||
data: printerData.total_layer_num,
|
||||
borderColor: 'rgb(201, 203, 207)',
|
||||
backgroundColor: 'rgba(201, 203, 207, 0.05)',
|
||||
borderDash: [5, 5],
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: tickColor },
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
stepSize: 1
|
||||
},
|
||||
grid: { color: gridColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Filament Timeline Chart
|
||||
const filamentCtx = document.getElementById('filamentTimelineChart').getContext('2d');
|
||||
const filamentDatasets = createFilamentDatasets(printerData.filament_timeline, printerData.timestamps);
|
||||
filamentTimelineChart = new Chart(filamentCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: printerData.timestamps,
|
||||
datasets: filamentDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor,
|
||||
boxWidth: 12,
|
||||
padding: 8
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const datasetLabel = context.dataset.label || '';
|
||||
const value = context.parsed.y;
|
||||
return datasetLabel + ': ' + value + '% remaining';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: tickColor },
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
y: {
|
||||
min: -10, // Allow for negative filament readings (e.g., -4%)
|
||||
max: 110, // 10% higher than 100% to make 100% line more visible
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
},
|
||||
grid: { color: gridColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set up theme observer for dynamic theme switching
|
||||
setupThemeObserver();
|
||||
}
|
||||
|
||||
function getTemperatureChartOptions(tickColor, gridColor, unit) {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toFixed(1) + unit;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: tickColor
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
callback: function(value) {
|
||||
return value + unit;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getPercentageChartOptions(tickColor, gridColor, label) {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: {}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: tickColor
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return label + ': ' + context.parsed.y + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: tickColor
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: gridColor
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createFilamentDatasets(filamentTimeline, timestamps) {
|
||||
const datasets = [];
|
||||
const filamentKeys = Object.keys(filamentTimeline);
|
||||
|
||||
// Convert to array for sorting
|
||||
const filamentEntries = filamentKeys.map(key => ({
|
||||
key: key,
|
||||
data: filamentTimeline[key]
|
||||
}));
|
||||
|
||||
// Sort by tray_id (numeric first, External last), then by start_idx (chronological)
|
||||
filamentEntries.sort((a, b) => {
|
||||
const trayA = a.data.tray_id;
|
||||
const trayB = b.data.tray_id;
|
||||
|
||||
// Handle External vs numeric
|
||||
if (trayA === 'External' && trayB !== 'External') return 1;
|
||||
if (trayB === 'External' && trayA !== 'External') return -1;
|
||||
if (trayA === 'External' && trayB === 'External') {
|
||||
return a.data.start_idx - b.data.start_idx;
|
||||
}
|
||||
|
||||
// Both numeric - sort by tray_id first, then by start_idx
|
||||
const trayNumA = parseInt(trayA);
|
||||
const trayNumB = parseInt(trayB);
|
||||
if (trayNumA !== trayNumB) {
|
||||
return trayNumA - trayNumB;
|
||||
}
|
||||
return a.data.start_idx - b.data.start_idx;
|
||||
});
|
||||
|
||||
// Create datasets
|
||||
filamentEntries.forEach(entry => {
|
||||
const filament = entry.data;
|
||||
const color = '#' + filament.color.substring(0, 6);
|
||||
|
||||
// Build descriptive label
|
||||
let displayLabel;
|
||||
if (filament.tray_id === 'External') {
|
||||
displayLabel = `External (${filament.type})`;
|
||||
} else {
|
||||
displayLabel = `Tray ${filament.tray_id} (${filament.type})`;
|
||||
}
|
||||
|
||||
// Add brand if it's different from type (avoid redundancy)
|
||||
if (filament.brand && filament.brand !== filament.type && filament.brand !== 'External') {
|
||||
displayLabel += ` - ${filament.brand}`;
|
||||
}
|
||||
|
||||
datasets.push({
|
||||
label: displayLabel,
|
||||
data: filament.remain_data,
|
||||
borderColor: color,
|
||||
backgroundColor: hexToRgba(color, 0.1),
|
||||
tension: 0.3,
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
spanGaps: false // Don't connect across null values (filament changes)
|
||||
});
|
||||
});
|
||||
|
||||
return datasets;
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function applyFilamentColors() {
|
||||
// Apply colors to filament cards
|
||||
document.querySelectorAll('.filament-card').forEach(card => {
|
||||
const colorHex = card.getAttribute('data-filament-color');
|
||||
if (colorHex) {
|
||||
const color = '#' + colorHex;
|
||||
|
||||
// Set card background with gradient
|
||||
card.style.background = `linear-gradient(135deg, ${hexToRgba(color, 0.12)} 0%, ${hexToRgba(color, 0.03)} 100%)`;
|
||||
card.style.borderLeft = `4px solid ${color}`;
|
||||
|
||||
// Set badge color
|
||||
const badge = card.querySelector('.filament-badge');
|
||||
if (badge) {
|
||||
badge.style.backgroundColor = color;
|
||||
badge.style.color = getContrastColor(color);
|
||||
}
|
||||
|
||||
// Set progress bar color
|
||||
const progressBar = card.querySelector('.filament-progress');
|
||||
if (progressBar) {
|
||||
progressBar.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getContrastColor(hexColor) {
|
||||
// Convert hex to RGB
|
||||
const r = parseInt(hexColor.slice(1, 3), 16);
|
||||
const g = parseInt(hexColor.slice(3, 5), 16);
|
||||
const b = parseInt(hexColor.slice(5, 7), 16);
|
||||
|
||||
// Calculate luminance
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
// Return black or white based on luminance
|
||||
return luminance > 0.5 ? '#000000' : '#ffffff';
|
||||
}
|
||||
|
||||
function updateChartTheme() {
|
||||
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)';
|
||||
|
||||
// Update all charts
|
||||
const charts = [
|
||||
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
|
||||
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
|
||||
];
|
||||
|
||||
charts.forEach(chart => {
|
||||
if (chart) {
|
||||
// Update legend colors
|
||||
chart.options.plugins.legend.labels.color = tickColor;
|
||||
|
||||
// Update x-axis colors
|
||||
chart.options.scales.x.ticks.color = tickColor;
|
||||
chart.options.scales.x.grid.color = gridColor;
|
||||
|
||||
// Update y-axis colors
|
||||
if (chart.options.scales.y) {
|
||||
chart.options.scales.y.ticks.color = tickColor;
|
||||
chart.options.scales.y.grid.color = gridColor;
|
||||
}
|
||||
|
||||
// Update y1-axis if exists (for dual-axis charts)
|
||||
if (chart.options.scales.y1) {
|
||||
if (chart.options.scales.y1.title) {
|
||||
chart.options.scales.y1.title.color = tickColor;
|
||||
}
|
||||
}
|
||||
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupThemeObserver() {
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-coreui-theme') {
|
||||
updateChartTheme();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-coreui-theme']
|
||||
});
|
||||
}
|
||||
418
bambu_run/static/bambu_run/js/printer_charts_control.js
Normal file
418
bambu_run/static/bambu_run/js/printer_charts_control.js
Normal file
@@ -0,0 +1,418 @@
|
||||
// 3D Printer Charts Control - Date/Time Filtering and Project Markers
|
||||
// Handles date range picker, time selection, and chart updates with annotations
|
||||
|
||||
// Global state
|
||||
const printerChartControls = {
|
||||
isFullDay: true,
|
||||
isCustomRange: false,
|
||||
apiUrl: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize on page load
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const apiUrlElement = document.getElementById('printerApiUrl');
|
||||
if (apiUrlElement) {
|
||||
printerChartControls.apiUrl = apiUrlElement.dataset.url;
|
||||
initializePrinterControls();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize printer chart date/time controls
|
||||
*/
|
||||
function initializePrinterControls() {
|
||||
const startDateInput = document.getElementById('printerStartDate');
|
||||
const endDateInput = document.getElementById('printerEndDate');
|
||||
const startTimeSelect = document.getElementById('printerStartTime');
|
||||
const endTimeSelect = document.getElementById('printerEndTime');
|
||||
const fullDayCheckbox = document.getElementById('printerFullDayCheckbox');
|
||||
const refreshBtn = document.getElementById('refreshPrinterCharts');
|
||||
const resetBtn = document.getElementById('resetPrinterCharts');
|
||||
|
||||
// Set max date to today
|
||||
const today = formatDate(new Date());
|
||||
startDateInput.max = today;
|
||||
endDateInput.max = today;
|
||||
|
||||
// Populate time dropdowns with 30-minute intervals
|
||||
populateTimeDropdowns(startTimeSelect, endTimeSelect);
|
||||
|
||||
// Set default values
|
||||
setDefaultPrinterDateTimeValues();
|
||||
|
||||
// Date input change handling
|
||||
startDateInput.addEventListener('change', handlePrinterDateChange);
|
||||
endDateInput.addEventListener('change', handlePrinterDateChange);
|
||||
|
||||
// Full Day checkbox toggle
|
||||
fullDayCheckbox.addEventListener('change', function() {
|
||||
printerChartControls.isFullDay = this.checked;
|
||||
togglePrinterTimeControls(!this.checked);
|
||||
updatePrinterDateRangeLabel();
|
||||
});
|
||||
|
||||
// Refresh button
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
refreshPrinterChartsData();
|
||||
});
|
||||
|
||||
// Reset button
|
||||
resetBtn.addEventListener('click', function() {
|
||||
resetPrinterControls();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate time dropdowns with 30-minute intervals
|
||||
*/
|
||||
function populateTimeDropdowns(startSelect, endSelect) {
|
||||
const times = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 30) {
|
||||
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
times.push(timeStr);
|
||||
}
|
||||
}
|
||||
|
||||
times.forEach(time => {
|
||||
const option1 = new Option(time, time);
|
||||
const option2 = new Option(time, time);
|
||||
startSelect.add(option1);
|
||||
endSelect.add(option2);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle time picker controls
|
||||
*/
|
||||
function togglePrinterTimeControls(enabled) {
|
||||
document.getElementById('printerStartTime').disabled = !enabled;
|
||||
document.getElementById('printerEndTime').disabled = !enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default date/time values (last 24 hours)
|
||||
*/
|
||||
function setDefaultPrinterDateTimeValues() {
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
document.getElementById('printerStartDate').value = formatDate(yesterday);
|
||||
document.getElementById('printerEndDate').value = formatDate(now);
|
||||
document.getElementById('printerStartTime').value = '00:00';
|
||||
document.getElementById('printerEndTime').value = '23:59';
|
||||
|
||||
const fullDayCheckbox = document.getElementById('printerFullDayCheckbox');
|
||||
fullDayCheckbox.checked = true;
|
||||
printerChartControls.isFullDay = true;
|
||||
togglePrinterTimeControls(false);
|
||||
|
||||
document.getElementById('printerDateRange').textContent = '(Last 24 Hours)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle date input changes
|
||||
*/
|
||||
function handlePrinterDateChange() {
|
||||
const startDate = document.getElementById('printerStartDate').value;
|
||||
const endDate = document.getElementById('printerEndDate').value;
|
||||
|
||||
// Ensure end date is not before start date
|
||||
if (startDate && endDate && startDate > endDate) {
|
||||
document.getElementById('printerEndDate').value = startDate;
|
||||
}
|
||||
|
||||
printerChartControls.isCustomRange = true;
|
||||
updatePrinterDateRangeLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the date range label
|
||||
*/
|
||||
function updatePrinterDateRangeLabel() {
|
||||
const startDate = document.getElementById('printerStartDate').value;
|
||||
const endDate = document.getElementById('printerEndDate').value;
|
||||
|
||||
let label = '';
|
||||
if (startDate === endDate) {
|
||||
label = '(' + startDate + ')';
|
||||
} else {
|
||||
label = '(' + startDate + ' to ' + endDate + ')';
|
||||
}
|
||||
document.getElementById('printerDateRange').textContent = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh printer charts data from API
|
||||
*/
|
||||
async function refreshPrinterChartsData() {
|
||||
const startDate = document.getElementById('printerStartDate').value;
|
||||
const endDate = document.getElementById('printerEndDate').value;
|
||||
const isFullDay = printerChartControls.isFullDay;
|
||||
|
||||
const startTime = isFullDay ? '00:00' : document.getElementById('printerStartTime').value;
|
||||
const endTime = isFullDay ? '23:59' : document.getElementById('printerEndTime').value;
|
||||
|
||||
// Show loading state (you can add a spinner here if needed)
|
||||
console.log('Refreshing printer charts...');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
start_time: startTime,
|
||||
end_time: endTime
|
||||
});
|
||||
|
||||
const response = await fetch(printerChartControls.apiUrl + '?' + params.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Update all charts with new data and project markers
|
||||
updateAllPrinterCharts(data);
|
||||
updatePrinterDateRangeLabel();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing printer charts:', error);
|
||||
alert('Error loading chart data: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all printer charts with new data
|
||||
*/
|
||||
function updateAllPrinterCharts(data) {
|
||||
// Update chart data
|
||||
updateChartData(nozzleTempChart, data.timestamps, [
|
||||
{ data: data.nozzle_temp, datasetIndex: 0 },
|
||||
{ data: data.nozzle_target_temp, datasetIndex: 1 }
|
||||
]);
|
||||
|
||||
updateChartData(bedTempChart, data.timestamps, [
|
||||
{ data: data.bed_temp, datasetIndex: 0 },
|
||||
{ data: data.bed_target_temp, datasetIndex: 1 }
|
||||
]);
|
||||
|
||||
updateChartData(printProgressChart, data.timestamps, [
|
||||
{ data: data.print_percent, datasetIndex: 0 }
|
||||
]);
|
||||
|
||||
updateChartData(fanSpeedsChart, data.timestamps, [
|
||||
{ data: data.cooling_fan_speed, datasetIndex: 0 },
|
||||
{ data: data.heatbreak_fan_speed, datasetIndex: 1 }
|
||||
]);
|
||||
|
||||
updateChartData(wifiSignalChart, data.timestamps, [
|
||||
{ data: data.wifi_signal_dbm, datasetIndex: 0 }
|
||||
]);
|
||||
|
||||
updateChartData(amsConditionsChart, data.timestamps, [
|
||||
{ data: data.ams_humidity_raw, datasetIndex: 0 },
|
||||
{ data: data.ams_temp, datasetIndex: 1 }
|
||||
]);
|
||||
|
||||
updateChartData(layerProgressChart, data.timestamps, [
|
||||
{ data: data.layer_num, datasetIndex: 0 },
|
||||
{ data: data.total_layer_num, datasetIndex: 1 }
|
||||
]);
|
||||
|
||||
// Update filament timeline chart
|
||||
if (data.filament_timeline) {
|
||||
const filamentDatasets = createFilamentDatasets(data.filament_timeline, data.timestamps);
|
||||
filamentTimelineChart.data.labels = data.timestamps;
|
||||
filamentTimelineChart.data.datasets = filamentDatasets;
|
||||
filamentTimelineChart.update();
|
||||
}
|
||||
|
||||
// Add project markers to all charts
|
||||
if (data.project_markers) {
|
||||
addProjectMarkersToCharts(data.project_markers, data.timestamps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to update chart data
|
||||
*/
|
||||
function updateChartData(chart, labels, datasets) {
|
||||
if (!chart) return;
|
||||
|
||||
chart.data.labels = labels;
|
||||
datasets.forEach(({ data, datasetIndex }) => {
|
||||
if (chart.data.datasets[datasetIndex]) {
|
||||
chart.data.datasets[datasetIndex].data = data;
|
||||
}
|
||||
});
|
||||
chart.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add project markers (start/end lines) to all charts
|
||||
*/
|
||||
function addProjectMarkersToCharts(markers, timestamps) {
|
||||
console.log('Adding project markers:', markers);
|
||||
|
||||
const charts = [
|
||||
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
|
||||
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
|
||||
];
|
||||
|
||||
charts.forEach(chart => {
|
||||
if (!chart) return;
|
||||
|
||||
// Initialize annotations plugin if not already
|
||||
if (!chart.options.plugins.annotation) {
|
||||
chart.options.plugins.annotation = { annotations: {} };
|
||||
}
|
||||
|
||||
// Clear existing project markers
|
||||
chart.options.plugins.annotation.annotations = {};
|
||||
|
||||
// Track active tooltip
|
||||
let activeMarkerTooltip = null;
|
||||
|
||||
// Add markers
|
||||
markers.forEach((marker, idx) => {
|
||||
const isStart = marker.type === 'start';
|
||||
const xValue = marker.index; // Use the index directly, not the timestamp string
|
||||
|
||||
const projectName = marker.project_name || 'Unknown';
|
||||
const markerId = `marker_${idx}`;
|
||||
|
||||
chart.options.plugins.annotation.annotations[markerId] = {
|
||||
type: 'line',
|
||||
scaleID: 'x',
|
||||
value: xValue,
|
||||
borderColor: isStart ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.7)',
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 5],
|
||||
drawTime: 'beforeDatasetsDraw',
|
||||
// Tighter hit detection - only trigger when very close to the line
|
||||
borderDashOffset: 0,
|
||||
display: true,
|
||||
enter: (ctx, event) => {
|
||||
// Verify we're actually hovering over THIS specific annotation line
|
||||
// Check if mouse X position is close to the line's X position
|
||||
if (event && event.native) {
|
||||
const chartArea = chart.chartArea;
|
||||
const xScale = chart.scales.x;
|
||||
const lineXPixel = xScale.getPixelForValue(xValue);
|
||||
const mouseX = event.native.offsetX;
|
||||
|
||||
// Only show tooltip if mouse is within 10 pixels of the line
|
||||
const distance = Math.abs(mouseX - lineXPixel);
|
||||
if (distance > 10) {
|
||||
return; // Too far from this line, don't show tooltip
|
||||
}
|
||||
}
|
||||
|
||||
// Only show tooltip if not already showing from another marker
|
||||
if (activeMarkerTooltip && activeMarkerTooltip !== markerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeMarkerTooltip = markerId;
|
||||
|
||||
const tooltipText = isStart
|
||||
? `Print Start: ${projectName}`
|
||||
: `Print End: ${projectName}`;
|
||||
|
||||
// Change line appearance on hover
|
||||
ctx.element.options.borderWidth = 3;
|
||||
ctx.element.options.borderColor = isStart ? 'rgba(34, 197, 94, 1)' : 'rgba(239, 68, 68, 1)';
|
||||
chart.update('none');
|
||||
|
||||
// Create or update tooltip element
|
||||
let tooltip = document.getElementById('annotation-tooltip');
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement('div');
|
||||
tooltip.id = 'annotation-tooltip';
|
||||
tooltip.style.position = 'fixed';
|
||||
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
||||
tooltip.style.color = 'white';
|
||||
tooltip.style.padding = '6px 10px';
|
||||
tooltip.style.borderRadius = '4px';
|
||||
tooltip.style.fontSize = '13px';
|
||||
tooltip.style.pointerEvents = 'none';
|
||||
tooltip.style.zIndex = '9999';
|
||||
tooltip.style.display = 'none';
|
||||
tooltip.style.whiteSpace = 'nowrap';
|
||||
document.body.appendChild(tooltip);
|
||||
}
|
||||
tooltip.textContent = tooltipText;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.dataset.markerId = markerId;
|
||||
|
||||
// Position at mouse location
|
||||
if (event && event.native) {
|
||||
tooltip.style.left = (event.native.clientX + 12) + 'px';
|
||||
tooltip.style.top = (event.native.clientY - 10) + 'px';
|
||||
}
|
||||
},
|
||||
leave: (ctx) => {
|
||||
// Only hide if this is the active marker
|
||||
if (activeMarkerTooltip === markerId) {
|
||||
activeMarkerTooltip = null;
|
||||
|
||||
// Restore line appearance
|
||||
ctx.element.options.borderWidth = 2;
|
||||
ctx.element.options.borderColor = isStart ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.7)';
|
||||
chart.update('none');
|
||||
|
||||
const tooltip = document.getElementById('annotation-tooltip');
|
||||
if (tooltip && tooltip.dataset.markerId === markerId) {
|
||||
tooltip.style.display = 'none';
|
||||
tooltip.dataset.markerId = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
chart.update();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset printer controls to default
|
||||
*/
|
||||
function resetPrinterControls() {
|
||||
setDefaultPrinterDateTimeValues();
|
||||
|
||||
// Clear annotations and reload with original data
|
||||
const charts = [
|
||||
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
|
||||
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
|
||||
];
|
||||
|
||||
charts.forEach(chart => {
|
||||
if (chart && chart.options.plugins.annotation) {
|
||||
chart.options.plugins.annotation.annotations = {};
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
// Reload page to get default data
|
||||
location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as YYYY-MM-DD
|
||||
*/
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user