Refactor: Simplify chart initialization and add dynamic Y-axis scaling logic

- Consolidate chart setup into `initializeSingleChart` for reusability.
- Introduce `scaleConfig` for step-wise Y-axis scaling.
- Enhance scaling logic to dynamically adjust `max` and `stepSize` based on data.

Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
This commit is contained in:
2025-09-03 14:05:53 +09:00
parent be52da0604
commit feb16beb0f

View File

@@ -17,6 +17,13 @@ export const charts = {
current: null
};
// Configuration for dynamic, step-wise Y-axis scaling
const scaleConfig = {
power: {steps: [5, 20, 50, 150]}, // in Watts
voltage: {steps: [5, 10, 15, 25]}, // in Volts
current: {steps: [1, 2.5, 5, 10]} // in Amps
};
const channelKeys = ['USB', 'MAIN', 'VIN'];
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
@@ -39,11 +46,9 @@ function initialData() {
/**
* 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) {
function createChartOptions(title) {
return {
responsive: true,
maintainAspectRatio: false,
@@ -55,12 +60,9 @@ function createChartOptions(title, minValue, maxValue) {
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
}
min: 0,
beginAtZero: true,
ticks: {}
}
}
};
@@ -78,10 +80,32 @@ function createDatasets(unit) {
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 2
pointRadius: 0
}));
}
/**
* Initializes a single chart with its specific configuration.
* @param {CanvasRenderingContext2D} context - The canvas context for the chart.
* @param {string} title - The chart title.
* @param {string} metric - The metric key ('power', 'voltage', 'current').
* @param {string} unit - The data unit ('W', 'V', 'A').
* @returns {Chart} A new Chart.js instance.
*/
function initializeSingleChart(context, title, metric, unit) {
if (!context) return null;
const options = createChartOptions(title);
const initialMax = scaleConfig[metric].steps[0];
options.scales.y.max = initialMax;
options.scales.y.ticks.stepSize = initialMax / 5; // Initial step size
return new Chart(context, {
type: 'line',
data: {labels: initialLabels(), datasets: createDatasets(unit)},
options: options
});
}
/**
* Initializes all three charts (Power, Voltage, Current).
@@ -89,50 +113,15 @@ function createDatasets(unit) {
*/
export function initCharts() {
// Destroy existing charts if they exist
for (const key in charts) {
Object.keys(charts).forEach(key => {
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
});
}
charts.power = initializeSingleChart(powerChartCtx, 'Power', 'power', 'W');
charts.voltage = initializeSingleChart(voltageChartCtx, 'Voltage', 'voltage', 'V');
charts.current = initializeSingleChart(currentChartCtx, 'Current', 'current', 'A');
}
/**
@@ -144,11 +133,10 @@ export function applyChartsTheme(themeName) {
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
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd',
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754',
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545'
];
const updateThemeForChart = (chart) => {
@@ -167,11 +155,62 @@ export function applyChartsTheme(themeName) {
chart.update('none');
};
updateThemeForChart(charts.power);
updateThemeForChart(charts.voltage);
updateThemeForChart(charts.current);
Object.values(charts).forEach(updateThemeForChart);
}
/**
* Updates a single chart with new data and dynamically adjusts its Y-axis scale.
* @param {Chart} chart - The Chart.js instance to update.
* @param {string} metric - The metric key (e.g., 'power', 'voltage').
* @param {Object} data - The new sensor data object.
* @param {string} timeLabel - The timestamp label for the new data point.
*/
function updateSingleChart(chart, metric, data, timeLabel) {
if (!chart) return;
// Shift old data and push new data
chart.data.labels.shift();
chart.data.labels.push(timeLabel);
chart.data.datasets.forEach((dataset, index) => {
dataset.data.shift();
const channel = channelKeys[index];
const value = data[channel]?.[metric];
dataset.data.push(value !== undefined ? value.toFixed(2) : null);
});
// --- DYNAMIC STEP-WISE Y-AXIS SCALING ---
const config = scaleConfig[metric];
if (config?.steps) {
const allData = chart.data.datasets
.flatMap(dataset => dataset.data)
.filter(v => v !== null)
.map(v => parseFloat(v));
const maxDataValue = allData.length > 0 ? Math.max(...allData) : 0;
// Find the smallest step that is >= maxDataValue
let newMax = config.steps.find(step => maxDataValue <= step);
// If value exceeds all steps, use the largest step. If no data, use the smallest.
if (newMax === undefined) {
newMax = config.steps[config.steps.length - 1];
}
if (chart.options.scales.y.max !== newMax) {
chart.options.scales.y.max = newMax;
// Dynamically adjust stepSize for clearer grid lines
chart.options.scales.y.ticks.stepSize = newMax / 5;
}
}
// --- END DYNAMIC SCALING ---
// Update chart only if its tab is visible for performance.
if (graphTabPane.classList.contains('show')) {
chart.update('none');
}
}
/**
* Updates all charts with new sensor data.
* @param {Object} data - The new sensor data object from the WebSocket.
@@ -179,43 +218,18 @@ export function applyChartsTheme(themeName) {
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');
updateSingleChart(charts.power, 'power', data, timeLabel);
updateSingleChart(charts.voltage, 'voltage', data, timeLabel);
updateSingleChart(charts.current, 'current', data, timeLabel);
}
/**
* 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();
Object.values(charts).forEach(chart => {
if (chart) {
chart.resize();
}
}
});
}