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
|
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 channelKeys = ['USB', 'MAIN', 'VIN'];
|
||||||
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
|
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.
|
* Creates a common configuration object for a single line chart.
|
||||||
* @param {string} title - The title of the chart (e.g., 'Power (W)').
|
* @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.
|
* @returns {Object} A Chart.js options object.
|
||||||
*/
|
*/
|
||||||
function createChartOptions(title, minValue, maxValue) {
|
function createChartOptions(title) {
|
||||||
return {
|
return {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -55,12 +60,9 @@ function createChartOptions(title, minValue, maxValue) {
|
|||||||
scales: {
|
scales: {
|
||||||
x: {ticks: {autoSkipPadding: 10, maxRotation: 0, minRotation: 0}},
|
x: {ticks: {autoSkipPadding: 10, maxRotation: 0, minRotation: 0}},
|
||||||
y: {
|
y: {
|
||||||
min: minValue,
|
min: 0,
|
||||||
max: maxValue,
|
beginAtZero: true,
|
||||||
beginAtZero: true, // Start at zero for better comparison
|
ticks: {}
|
||||||
ticks: {
|
|
||||||
stepSize: (maxValue - minValue) / 8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -78,10 +80,32 @@ function createDatasets(unit) {
|
|||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: false,
|
fill: false,
|
||||||
tension: 0.2,
|
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).
|
* Initializes all three charts (Power, Voltage, Current).
|
||||||
@@ -89,50 +113,15 @@ function createDatasets(unit) {
|
|||||||
*/
|
*/
|
||||||
export function initCharts() {
|
export function initCharts() {
|
||||||
// Destroy existing charts if they exist
|
// Destroy existing charts if they exist
|
||||||
for (const key in charts) {
|
Object.keys(charts).forEach(key => {
|
||||||
if (charts[key]) {
|
if (charts[key]) {
|
||||||
charts[key].destroy();
|
charts[key].destroy();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Create Power Chart
|
charts.power = initializeSingleChart(powerChartCtx, 'Power', 'power', 'W');
|
||||||
if (powerChartCtx) {
|
charts.voltage = initializeSingleChart(voltageChartCtx, 'Voltage', 'voltage', 'V');
|
||||||
const powerOptions = createChartOptions('Power', 0, 50); // Adjusted max value
|
charts.current = initializeSingleChart(currentChartCtx, 'Current', 'current', 'A');
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,11 +133,10 @@ export function applyChartsTheme(themeName) {
|
|||||||
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
const labelColor = isDark ? '#dee2e6' : '#212529';
|
const labelColor = isDark ? '#dee2e6' : '#212529';
|
||||||
|
|
||||||
// Define colors for each channel. These could be from CSS variables.
|
|
||||||
const channelColors = [
|
const channelColors = [
|
||||||
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd', // Blue
|
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd',
|
||||||
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754', // Green
|
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754',
|
||||||
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545' // Red
|
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545'
|
||||||
];
|
];
|
||||||
|
|
||||||
const updateThemeForChart = (chart) => {
|
const updateThemeForChart = (chart) => {
|
||||||
@@ -167,11 +155,62 @@ export function applyChartsTheme(themeName) {
|
|||||||
chart.update('none');
|
chart.update('none');
|
||||||
};
|
};
|
||||||
|
|
||||||
updateThemeForChart(charts.power);
|
Object.values(charts).forEach(updateThemeForChart);
|
||||||
updateThemeForChart(charts.voltage);
|
|
||||||
updateThemeForChart(charts.current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Updates all charts with new sensor data.
|
||||||
* @param {Object} data - The new sensor data object from the WebSocket.
|
* @param {Object} data - The new sensor data object from the WebSocket.
|
||||||
@@ -179,43 +218,18 @@ export function applyChartsTheme(themeName) {
|
|||||||
export function updateCharts(data) {
|
export function updateCharts(data) {
|
||||||
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
|
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
|
||||||
|
|
||||||
const updateSingleChart = (chart, metric) => {
|
updateSingleChart(charts.power, 'power', data, timeLabel);
|
||||||
if (!chart) return;
|
updateSingleChart(charts.voltage, 'voltage', data, timeLabel);
|
||||||
|
updateSingleChart(charts.current, 'current', data, timeLabel);
|
||||||
// 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.
|
* Resizes all chart canvases. This is typically called on window resize events.
|
||||||
*/
|
*/
|
||||||
export function resizeCharts() {
|
export function resizeCharts() {
|
||||||
for (const key in charts) {
|
Object.values(charts).forEach(chart => {
|
||||||
if (charts[key]) {
|
if (chart) {
|
||||||
charts[key].resize();
|
chart.resize();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user