Files
odroid-power-mate/page/src/chart.js
YoungSoo Shin 2dc5798b0a Apply modifications to mass-produced boards
- 3 channel power sensor ina3221
- io expander pca9557
- Some gpio moves
- ...

Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
2025-09-01 11:56:49 +09:00

222 lines
6.8 KiB
JavaScript

/**
* @file chart.js
* @description This module manages the Chart.js instances for visualizing sensor data.
* It handles initialization, theme updates, data updates, and resizing for the three separate charts.
*/
import { Chart, registerables } from 'chart.js';
import { powerChartCtx, voltageChartCtx, currentChartCtx, htmlEl, graphTabPane } from './dom.js';
// Register all necessary Chart.js components
Chart.register(...registerables);
// Store chart instances in an object
export const charts = {
power: null,
voltage: null,
current: null
};
const channelKeys = ['USB', 'MAIN', 'VIN'];
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
/**
* Creates an array of empty labels for initial chart rendering.
* @returns {Array<string>} An array of empty strings.
*/
function initialLabels() {
return Array(CHART_DATA_POINTS).fill('');
}
/**
* Creates an array of null data points for initial chart rendering.
* @returns {Array<null>} An array of nulls.
*/
function initialData() {
return Array(CHART_DATA_POINTS).fill(null);
}
/**
* Creates a common configuration object for a single line chart.
* @param {string} title - The title of the chart (e.g., 'Power (W)').
* @param {number} minValue - The minimum value for Y-axis.
* @param {number} maxValue - The maximum value for Y-axis.
* @returns {Object} A Chart.js options object.
*/
function createChartOptions(title, minValue, maxValue) {
return {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'top' },
title: { display: true, text: title }
},
scales: {
x: { ticks: { autoSkipPadding: 10, maxRotation: 0, minRotation: 0 } },
y: {
min: minValue,
max: maxValue,
beginAtZero: true, // Start at zero for better comparison
ticks: {
stepSize: (maxValue - minValue) / 8
}
}
}
};
}
/**
* Creates the dataset objects for a chart.
* @param {string} unit - The unit for the dataset label (e.g., 'W', 'V', 'A').
* @returns {Array<Object>} An array of Chart.js dataset objects.
*/
function createDatasets(unit) {
return channelKeys.map(channel => ({
label: `${channel} (${unit})`,
data: initialData(),
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 2
}));
}
/**
* Initializes all three charts (Power, Voltage, Current).
* If chart instances already exist, they are destroyed and new ones are created.
*/
export function initCharts() {
// Destroy existing charts if they exist
for (const key in charts) {
if (charts[key]) {
charts[key].destroy();
}
}
// Create Power Chart
if (powerChartCtx) {
const powerOptions = createChartOptions('Power', 0, 50); // Adjusted max value
charts.power = new Chart(powerChartCtx, {
type: 'line',
data: {
labels: initialLabels(),
datasets: createDatasets('W')
},
options: powerOptions
});
}
// Create Voltage Chart
if (voltageChartCtx) {
const voltageOptions = createChartOptions('Voltage', 0, 24);
charts.voltage = new Chart(voltageChartCtx, {
type: 'line',
data: {
labels: initialLabels(),
datasets: createDatasets('V')
},
options: voltageOptions
});
}
// Create Current Chart
if (currentChartCtx) {
const currentOptions = createChartOptions('Current', 0, 5); // Adjusted max value
charts.current = new Chart(currentChartCtx, {
type: 'line',
data: {
labels: initialLabels(),
datasets: createDatasets('A')
},
options: currentOptions
});
}
}
/**
* Applies a new theme (light or dark) to all charts.
* @param {string} themeName - The name of the theme to apply ('light' or 'dark').
*/
export function applyChartsTheme(themeName) {
const isDark = themeName === 'dark';
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
const labelColor = isDark ? '#dee2e6' : '#212529';
// Define colors for each channel. These could be from CSS variables.
const channelColors = [
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd', // Blue
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754', // Green
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545' // Red
];
const updateThemeForChart = (chart) => {
if (!chart) return;
chart.options.scales.x.grid.color = gridColor;
chart.options.scales.y.grid.color = gridColor;
chart.options.scales.x.ticks.color = labelColor;
chart.options.scales.y.ticks.color = labelColor;
chart.options.plugins.legend.labels.color = labelColor;
chart.options.plugins.title.color = labelColor;
chart.data.datasets.forEach((dataset, index) => {
dataset.borderColor = channelColors[index];
});
chart.update('none');
};
updateThemeForChart(charts.power);
updateThemeForChart(charts.voltage);
updateThemeForChart(charts.current);
}
/**
* Updates all charts with new sensor data.
* @param {Object} data - The new sensor data object from the WebSocket.
*/
export function updateCharts(data) {
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
const updateSingleChart = (chart, metric) => {
if (!chart) return;
// Shift old data
chart.data.labels.shift();
chart.data.datasets.forEach(dataset => dataset.data.shift());
// Push new data
chart.data.labels.push(timeLabel);
channelKeys.forEach((key, index) => {
if (data[key] && data[key][metric] !== undefined) {
const value = data[key][metric];
chart.data.datasets[index].data.push(value.toFixed(2));
} else {
chart.data.datasets[index].data.push(null); // Push null if data for a channel is missing
}
});
// Only update the chart if the tab is visible
if (graphTabPane.classList.contains('show')) {
chart.update('none');
}
};
updateSingleChart(charts.power, 'power');
updateSingleChart(charts.voltage, 'voltage');
updateSingleChart(charts.current, 'current');
}
/**
* Resizes all chart canvases. This is typically called on window resize events.
*/
export function resizeCharts() {
for (const key in charts) {
if (charts[key]) {
charts[key].resize();
}
}
}