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:
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user