Files
Bambu-Run/bambu_run/static/bambu_run/js/printer_charts.js
RunLit 5984bd6fa0 Filament tools that help upload bambu colors and filament types easily (#3)
* added cover image

* bambu color import manage tool added

* added AMS hex color trimming

* updated instructions

* touch up readme

* fixed line chart noise x axis and add more date marker to split them up
2026-02-25 23:07:24 +11:00

815 lines
27 KiB
JavaScript

// 3D Printer Charts Initialization and Management
// Chart.js implementation for printer metrics visualization
let nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart;
let wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart;
function showNoDataMessage(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const container = canvas.closest('.chart-container');
if (!container) return;
canvas.style.display = 'none';
const msg = document.createElement('div');
msg.className = 'no-data-message d-flex align-items-center justify-content-center h-100 text-body-secondary';
msg.textContent = 'No data available for this period';
container.appendChild(msg);
}
function initPrinterCharts(printerData, apiUrl) {
// Apply filament card colors
applyFilamentColors();
// If no data, show placeholder messages and exit early
if (!printerData.timestamps || printerData.timestamps.length === 0) {
['nozzleTempChart', 'bedTempChart', 'printProgressChart', 'fanSpeedsChart',
'wifiSignalChart', 'amsConditionsChart', 'layerProgressChart', 'filamentTimelineChart'
].forEach(showNoDataMessage);
return;
}
// 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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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: 3,
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 }
}
}
}
});
// 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
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: 3,
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']
});
}
/**
* 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');
});
}