Commit all changes
This commit is contained in:
+361
-49
@@ -88,6 +88,25 @@ class ChartsManager {
|
||||
return chartContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Calculate linear trend (slope, intercept, trend line) for a dataset
|
||||
* @param {Array<number>} y - Array of y values (e.g., totalSleep)
|
||||
* @returns {Object} { slope, intercept, trendLine }
|
||||
*/
|
||||
getLinearTrend(y) {
|
||||
const n = y.length;
|
||||
if (n < 2) return { slope: 0, intercept: y[0] || 0, trendLine: y.map(() => y[0] || 0) };
|
||||
const x = Array.from({ length: n }, (_, i) => i);
|
||||
const sumX = x.reduce((a, b) => a + b, 0);
|
||||
const sumY = y.reduce((a, b) => a + b, 0);
|
||||
const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0);
|
||||
const sumXX = x.reduce((acc, xi) => acc + xi * xi, 0);
|
||||
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
const trendLine = x.map(xi => slope * xi + intercept);
|
||||
return { slope, intercept, trendLine };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sleep patterns chart
|
||||
* @param {Array} metricsData - Processed metrics data
|
||||
@@ -110,6 +129,17 @@ class ChartsManager {
|
||||
const deepSleep = sleepData.map(day => day.metrics['Time In Deep Sleep'] || 0);
|
||||
const lightSleep = sleepData.map(day => day.metrics['Time In Light Sleep'] || 0);
|
||||
const remSleep = sleepData.map(day => day.metrics['Time In REM Sleep'] || 0);
|
||||
|
||||
// --- Trend Detection for Sleep Duration ---
|
||||
const { slope, trendLine } = this.getLinearTrend(totalSleep);
|
||||
let trendLabel = '';
|
||||
if (slope > 0.1) {
|
||||
trendLabel = 'Improving Sleep Duration';
|
||||
} else if (slope < -0.1) {
|
||||
trendLabel = 'Declining Sleep Duration';
|
||||
} else {
|
||||
trendLabel = 'Stable Sleep Duration';
|
||||
}
|
||||
|
||||
// Create container for stacked bar chart
|
||||
const stackedContainer = this.createChartContainer(wrapper);
|
||||
@@ -187,18 +217,51 @@ class ChartsManager {
|
||||
const trendCanvas = document.createElement('canvas');
|
||||
trendContainer.appendChild(trendCanvas);
|
||||
|
||||
// --- Goal Line and Progress ---
|
||||
let goalDataset = null;
|
||||
let goalLabelDiv = null;
|
||||
if (typeof this.sleepGoal === "number" && !isNaN(this.sleepGoal) && this.sleepGoal > 0) {
|
||||
goalDataset = {
|
||||
label: `Goal (${this.sleepGoal}h)`,
|
||||
data: totalSleep.map(() => this.sleepGoal),
|
||||
borderColor: "#2ecc71",
|
||||
backgroundColor: "rgba(46, 204, 113, 0.08)",
|
||||
fill: false,
|
||||
borderDash: [2, 6],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
order: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Compose datasets for trend chart
|
||||
const trendDatasets = [
|
||||
{
|
||||
label: 'Sleep Duration',
|
||||
data: totalSleep,
|
||||
borderColor: this.colors.sleep.total,
|
||||
backgroundColor: 'rgba(74, 111, 165, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
},
|
||||
{
|
||||
label: 'Trend',
|
||||
data: trendLine,
|
||||
borderColor: '#ff7e67',
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.08)',
|
||||
fill: false,
|
||||
borderDash: [8, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0
|
||||
}
|
||||
];
|
||||
if (goalDataset) trendDatasets.push(goalDataset);
|
||||
|
||||
const trendChart = new Chart(trendCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: [{
|
||||
label: 'Sleep Duration',
|
||||
data: totalSleep,
|
||||
borderColor: this.colors.sleep.total,
|
||||
backgroundColor: 'rgba(74, 111, 165, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
datasets: trendDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
@@ -207,6 +270,9 @@ class ChartsManager {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Sleep Duration Trend'
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@@ -221,6 +287,28 @@ class ChartsManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add trend label below the chart
|
||||
const trendLabelDiv = document.createElement('div');
|
||||
trendLabelDiv.className = 'trend-label';
|
||||
trendLabelDiv.style.margin = '8px 0 0 0';
|
||||
trendLabelDiv.style.fontWeight = 'bold';
|
||||
trendLabelDiv.style.color = slope > 0.1 ? '#2ecc71' : (slope < -0.1 ? '#e74c3c' : '#3498db');
|
||||
trendLabelDiv.textContent = trendLabel;
|
||||
trendContainer.appendChild(trendLabelDiv);
|
||||
|
||||
// Add goal progress below the trend label if goal is set
|
||||
if (goalDataset) {
|
||||
const nightsMet = totalSleep.filter(h => h >= this.sleepGoal).length;
|
||||
const percentMet = Math.round((nightsMet / totalSleep.length) * 100);
|
||||
const goalLabelDiv = document.createElement('div');
|
||||
goalLabelDiv.className = 'goal-label';
|
||||
goalLabelDiv.style.margin = '4px 0 16px 0';
|
||||
goalLabelDiv.style.fontWeight = 'bold';
|
||||
goalLabelDiv.style.color = '#2ecc71';
|
||||
goalLabelDiv.textContent = `Goal met on ${nightsMet} of ${totalSleep.length} nights (${percentMet}%)`;
|
||||
trendContainer.appendChild(goalLabelDiv);
|
||||
}
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(trendChart);
|
||||
@@ -313,18 +401,51 @@ class ChartsManager {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Trendline and Goal for Stress Level Timeline ---
|
||||
const { slope: stressSlope, trendLine: stressTrendLine } = this.getLinearTrend(stressValues);
|
||||
|
||||
// Default goal: 2 ("balanced")
|
||||
const stressGoal = 2;
|
||||
const goalDataset = {
|
||||
label: 'Goal (Balanced)',
|
||||
data: stressValues.map(() => stressGoal),
|
||||
borderColor: "#2ecc71",
|
||||
backgroundColor: "rgba(46, 204, 113, 0.08)",
|
||||
fill: false,
|
||||
borderDash: [2, 6],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
order: 0
|
||||
};
|
||||
|
||||
// Compose datasets for timeline chart
|
||||
const timelineDatasets = [
|
||||
{
|
||||
label: 'Stress Level',
|
||||
data: stressValues,
|
||||
borderColor: this.colors.accent,
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.1)',
|
||||
fill: true,
|
||||
stepped: true
|
||||
},
|
||||
{
|
||||
label: 'Trend',
|
||||
data: stressTrendLine,
|
||||
borderColor: '#ff7e67',
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.08)',
|
||||
fill: false,
|
||||
borderDash: [8, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0
|
||||
},
|
||||
goalDataset
|
||||
];
|
||||
|
||||
const timelineChart = new Chart(timelineCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: [{
|
||||
label: 'Stress Level',
|
||||
data: stressValues,
|
||||
borderColor: this.colors.accent,
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.1)',
|
||||
fill: true,
|
||||
stepped: true
|
||||
}]
|
||||
datasets: timelineDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
@@ -339,7 +460,6 @@ class ChartsManager {
|
||||
label: (context) => {
|
||||
const value = context.raw;
|
||||
let label = '';
|
||||
|
||||
switch(value) {
|
||||
case 1: label = 'Calm'; break;
|
||||
case 2: label = 'Balanced'; break;
|
||||
@@ -347,16 +467,22 @@ class ChartsManager {
|
||||
case 4: label = 'Very Stressful'; break;
|
||||
default: label = 'Unknown';
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0.5,
|
||||
max: 4.5,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Stress Level'
|
||||
},
|
||||
ticks: {
|
||||
callback: (value) => {
|
||||
switch(value) {
|
||||
@@ -372,7 +498,30 @@ class ChartsManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add trend label below the chart
|
||||
const trendLabelDiv = document.createElement('div');
|
||||
trendLabelDiv.className = 'trend-label';
|
||||
trendLabelDiv.style.margin = '8px 0 0 0';
|
||||
trendLabelDiv.style.fontWeight = 'bold';
|
||||
trendLabelDiv.style.color = stressSlope < -0.1 ? '#2ecc71' : (stressSlope > 0.1 ? '#e74c3c' : '#3498db');
|
||||
trendLabelDiv.textContent =
|
||||
stressSlope < -0.1
|
||||
? 'Improving Stress Levels'
|
||||
: (stressSlope > 0.1 ? 'Declining Stress Levels' : 'Stable Stress Levels');
|
||||
timelineContainer.appendChild(trendLabelDiv);
|
||||
|
||||
// Add goal progress below the trend label
|
||||
const daysMet = stressValues.filter(v => v <= stressGoal).length;
|
||||
const percentMet = Math.round((daysMet / stressValues.length) * 100);
|
||||
const goalLabelDiv = document.createElement('div');
|
||||
goalLabelDiv.className = 'goal-label';
|
||||
goalLabelDiv.style.margin = '4px 0 16px 0';
|
||||
goalLabelDiv.style.fontWeight = 'bold';
|
||||
goalLabelDiv.style.color = '#2ecc71';
|
||||
goalLabelDiv.textContent = `Goal met on ${daysMet} of ${stressValues.length} days (${percentMet}%)`;
|
||||
timelineContainer.appendChild(goalLabelDiv);
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(timelineChart);
|
||||
}
|
||||
@@ -550,34 +699,93 @@ class ChartsManager {
|
||||
const hrvValues = hrvData.map(day => day.metrics['HRV']);
|
||||
const pulseValues = hrvData.map(day => day.metrics['Pulse']);
|
||||
|
||||
// --- Trendline and Goal for HRV and Resting Pulse ---
|
||||
const { slope: hrvSlope, trendLine: hrvTrendLine } = this.getLinearTrend(hrvValues);
|
||||
const { slope: pulseSlope, trendLine: pulseTrendLine } = this.getLinearTrend(pulseValues);
|
||||
|
||||
// Default goals
|
||||
const hrvGoal = 50;
|
||||
const pulseGoal = 60;
|
||||
const hrvGoalDataset = {
|
||||
label: 'Goal (HRV 50ms)',
|
||||
data: hrvValues.map(() => hrvGoal),
|
||||
borderColor: "#2ecc71",
|
||||
backgroundColor: "rgba(46, 204, 113, 0.08)",
|
||||
fill: false,
|
||||
borderDash: [2, 6],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
yAxisID: 'y',
|
||||
order: 0
|
||||
};
|
||||
const pulseGoalDataset = {
|
||||
label: 'Goal (Pulse 60bpm)',
|
||||
data: pulseValues.map(() => pulseGoal),
|
||||
borderColor: "#3498db",
|
||||
backgroundColor: "rgba(52, 152, 219, 0.08)",
|
||||
fill: false,
|
||||
borderDash: [2, 6],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
yAxisID: 'y1',
|
||||
order: 0
|
||||
};
|
||||
|
||||
// Compose datasets for HRV/Pulse chart
|
||||
const hrvPulseDatasets = [
|
||||
{
|
||||
label: 'HRV',
|
||||
data: hrvValues,
|
||||
borderColor: this.colors.primary,
|
||||
backgroundColor: 'rgba(74, 111, 165, 0.1)',
|
||||
fill: true,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'HRV Trend',
|
||||
data: hrvTrendLine,
|
||||
borderColor: '#ff7e67',
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.08)',
|
||||
fill: false,
|
||||
borderDash: [8, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
hrvGoalDataset,
|
||||
{
|
||||
label: 'Resting Pulse',
|
||||
data: pulseValues,
|
||||
borderColor: this.colors.accent,
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.1)',
|
||||
fill: true,
|
||||
yAxisID: 'y1'
|
||||
},
|
||||
{
|
||||
label: 'Pulse Trend',
|
||||
data: pulseTrendLine,
|
||||
borderColor: '#8e44ad',
|
||||
backgroundColor: 'rgba(142, 68, 173, 0.08)',
|
||||
fill: false,
|
||||
borderDash: [8, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
yAxisID: 'y1'
|
||||
},
|
||||
pulseGoalDataset
|
||||
];
|
||||
|
||||
// Create container for HRV/Pulse chart
|
||||
const hrvContainer = this.createChartContainer(wrapper);
|
||||
const canvas = document.createElement('canvas');
|
||||
hrvContainer.appendChild(canvas);
|
||||
|
||||
|
||||
// Create chart
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: [
|
||||
{
|
||||
label: 'HRV',
|
||||
data: hrvValues,
|
||||
borderColor: this.colors.primary,
|
||||
backgroundColor: 'rgba(74, 111, 165, 0.1)',
|
||||
fill: true,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Resting Pulse',
|
||||
data: pulseValues,
|
||||
borderColor: this.colors.accent,
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.1)',
|
||||
fill: true,
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
datasets: hrvPulseDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
@@ -586,6 +794,9 @@ class ChartsManager {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'HRV and Resting Pulse'
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@@ -611,10 +822,55 @@ class ChartsManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add trend and goal progress labels below the chart
|
||||
// HRV
|
||||
const hrvTrendLabelDiv = document.createElement('div');
|
||||
hrvTrendLabelDiv.className = 'trend-label';
|
||||
hrvTrendLabelDiv.style.margin = '8px 0 0 0';
|
||||
hrvTrendLabelDiv.style.fontWeight = 'bold';
|
||||
hrvTrendLabelDiv.style.color = hrvSlope > 0.1 ? '#2ecc71' : (hrvSlope < -0.1 ? '#e74c3c' : '#3498db');
|
||||
hrvTrendLabelDiv.textContent =
|
||||
hrvSlope > 0.1
|
||||
? 'Improving HRV'
|
||||
: (hrvSlope < -0.1 ? 'Declining HRV' : 'Stable HRV');
|
||||
hrvContainer.appendChild(hrvTrendLabelDiv);
|
||||
|
||||
const hrvDaysMet = hrvValues.filter(v => v >= hrvGoal).length;
|
||||
const hrvPercentMet = Math.round((hrvDaysMet / hrvValues.length) * 100);
|
||||
const hrvGoalLabelDiv = document.createElement('div');
|
||||
hrvGoalLabelDiv.className = 'goal-label';
|
||||
hrvGoalLabelDiv.style.margin = '4px 0 0 0';
|
||||
hrvGoalLabelDiv.style.fontWeight = 'bold';
|
||||
hrvGoalLabelDiv.style.color = '#2ecc71';
|
||||
hrvGoalLabelDiv.textContent = `HRV Goal met on ${hrvDaysMet} of ${hrvValues.length} days (${hrvPercentMet}%)`;
|
||||
hrvContainer.appendChild(hrvGoalLabelDiv);
|
||||
|
||||
// Pulse
|
||||
const pulseTrendLabelDiv = document.createElement('div');
|
||||
pulseTrendLabelDiv.className = 'trend-label';
|
||||
pulseTrendLabelDiv.style.margin = '8px 0 0 0';
|
||||
pulseTrendLabelDiv.style.fontWeight = 'bold';
|
||||
pulseTrendLabelDiv.style.color = pulseSlope < -0.1 ? '#2ecc71' : (pulseSlope > 0.1 ? '#e74c3c' : '#3498db');
|
||||
pulseTrendLabelDiv.textContent =
|
||||
pulseSlope < -0.1
|
||||
? 'Improving Resting Pulse'
|
||||
: (pulseSlope > 0.1 ? 'Declining Resting Pulse' : 'Stable Resting Pulse');
|
||||
hrvContainer.appendChild(pulseTrendLabelDiv);
|
||||
|
||||
const pulseDaysMet = pulseValues.filter(v => v <= pulseGoal).length;
|
||||
const pulsePercentMet = Math.round((pulseDaysMet / pulseValues.length) * 100);
|
||||
const pulseGoalLabelDiv = document.createElement('div');
|
||||
pulseGoalLabelDiv.className = 'goal-label';
|
||||
pulseGoalLabelDiv.style.margin = '4px 0 16px 0';
|
||||
pulseGoalLabelDiv.style.fontWeight = 'bold';
|
||||
pulseGoalLabelDiv.style.color = '#2ecc71';
|
||||
pulseGoalLabelDiv.textContent = `Pulse Goal met on ${pulseDaysMet} of ${pulseValues.length} days (${pulsePercentMet}%)`;
|
||||
hrvContainer.appendChild(pulseGoalLabelDiv);
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(chart);
|
||||
|
||||
|
||||
// Create body battery chart if data is available
|
||||
const bodyBatteryData = metricsData.filter(day =>
|
||||
day.metrics['Body Battery'] !== undefined
|
||||
@@ -629,23 +885,54 @@ class ChartsManager {
|
||||
return avgMatch ? parseInt(avgMatch[1]) : null;
|
||||
}).filter(val => val !== null);
|
||||
|
||||
// --- Trendline and Goal for Body Battery ---
|
||||
const { slope: bbSlope, trendLine: bbTrendLine } = this.getLinearTrend(batteryValues);
|
||||
const bbGoal = 80;
|
||||
const bbGoalDataset = {
|
||||
label: 'Goal (80)',
|
||||
data: batteryValues.map(() => bbGoal),
|
||||
borderColor: "#2ecc71",
|
||||
backgroundColor: "rgba(46, 204, 113, 0.08)",
|
||||
fill: false,
|
||||
borderDash: [2, 6],
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
order: 0
|
||||
};
|
||||
|
||||
// Compose datasets for Body Battery chart
|
||||
const bbDatasets = [
|
||||
{
|
||||
label: 'Body Battery (Average)',
|
||||
data: batteryValues,
|
||||
borderColor: this.colors.secondary,
|
||||
backgroundColor: 'rgba(111, 185, 143, 0.1)',
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Trend',
|
||||
data: bbTrendLine,
|
||||
borderColor: '#ff7e67',
|
||||
backgroundColor: 'rgba(255, 126, 103, 0.08)',
|
||||
fill: false,
|
||||
borderDash: [8, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0
|
||||
},
|
||||
bbGoalDataset
|
||||
];
|
||||
|
||||
// Create container for body battery chart
|
||||
const bbContainer = this.createChartContainer(wrapper);
|
||||
const bbCanvas = document.createElement('canvas');
|
||||
bbContainer.appendChild(bbCanvas);
|
||||
|
||||
|
||||
// Create chart
|
||||
const bbChart = new Chart(bbCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: batteryDates,
|
||||
datasets: [{
|
||||
label: 'Body Battery (Average)',
|
||||
data: batteryValues,
|
||||
borderColor: this.colors.secondary,
|
||||
backgroundColor: 'rgba(111, 185, 143, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
datasets: bbDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
@@ -654,6 +941,9 @@ class ChartsManager {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Body Battery Trend'
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@@ -668,7 +958,29 @@ class ChartsManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add trend and goal progress labels below the chart
|
||||
const bbTrendLabelDiv = document.createElement('div');
|
||||
bbTrendLabelDiv.className = 'trend-label';
|
||||
bbTrendLabelDiv.style.margin = '8px 0 0 0';
|
||||
bbTrendLabelDiv.style.fontWeight = 'bold';
|
||||
bbTrendLabelDiv.style.color = bbSlope > 0.1 ? '#2ecc71' : (bbSlope < -0.1 ? '#e74c3c' : '#3498db');
|
||||
bbTrendLabelDiv.textContent =
|
||||
bbSlope > 0.1
|
||||
? 'Improving Body Battery'
|
||||
: (bbSlope < -0.1 ? 'Declining Body Battery' : 'Stable Body Battery');
|
||||
bbContainer.appendChild(bbTrendLabelDiv);
|
||||
|
||||
const bbDaysMet = batteryValues.filter(v => v >= bbGoal).length;
|
||||
const bbPercentMet = Math.round((bbDaysMet / batteryValues.length) * 100);
|
||||
const bbGoalLabelDiv = document.createElement('div');
|
||||
bbGoalLabelDiv.className = 'goal-label';
|
||||
bbGoalLabelDiv.style.margin = '4px 0 16px 0';
|
||||
bbGoalLabelDiv.style.fontWeight = 'bold';
|
||||
bbGoalLabelDiv.style.color = '#2ecc71';
|
||||
bbGoalLabelDiv.textContent = `Goal met on ${bbDaysMet} of ${batteryValues.length} days (${bbPercentMet}%)`;
|
||||
bbContainer.appendChild(bbGoalLabelDiv);
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(bbChart);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user