<template>
<q-card flat>
<q-card-section class="q-pa-none">
<div id="chart-container">
<chart v-if = "loaded" ref="chart" :chartData="chartData" :chartOptions="chartOptions" :type="chartType" />
</div>
</q-card-section>
</q-card>
</template>
<script>
/**
* Chart component
*
* @component
* @name Chart
* @example
* <Chart />
*/
import { StatisticsMixin } from "../mixins/statistics";
import { loadComponent } from "@/common/component-loader";
export default {
name: "ChartPopup",
mixins: [StatisticsMixin],
components: {
Chart: loadComponent('chart')
},
props: ['parentPopup'],
data: () => ({
loaded: false,
maximized: false,
help: "",
titleToShow: "",
persistent: false,
title: "",
chartType: "bar",
xScale: "linear",
annotationAxis: "y",
stat: null,
chartData: {},
chartOptions: {},
chartColors: [
'#1abc9c', // Turquoise
'#2ecc71', // Emerald
'#3498db', // Peter River
'#9b59b6', // Amethyst
'#34495e', // Wet Asphalt
'#16a085', // Green Sea
'#27ae60', // Nephritis
'#2980b9', // Belize Hole
'#8e44ad', // Wisteria
'#2c3e50', // Midnight Blue
'#f39c12', // Orange
'#e74c3c', // Pomegranate
'#ecf0f1', // Clouds
'#95a5a6', // Concrete
'#d35400', // Pumpkin
'#c0392b', // Alizarin
'#bdc3c7', // Silver
'#7f8c8d', // Asbestos
]
}),
async mounted() {
this.initializeComponent(this.parentPopup);
this.parentPopup.buttons = [{ icon: "content_copy", tooltip: "Copy to clipboard", action: this.copyToClipboard }, { icon: "download", tooltip: "Download", action: this.download }];
this.parentPopup.title = this.title;
this.showChart();
},
methods: {
/**
* Downloads the chart as an image.
*/
download() {
this.export(true, "chart-container", "chart.png");
//this.$refs.chart.download();
},
/**
* Copies the chart as an image to clipboard.
*/
copyToClipboard() {
this.export(false, "chart-container");
//this.$refs.chart.copyToClipboard();
},
/**
* Returns the x-axis label for the chart.
* If the chart is a time chart, it parses the time value using the parseTime method.
* Otherwise, it returns the original x value.
*
* @param {any} x - The x value for the chart.
* @returns {string} The x-axis label for the chart.
*/
xLabel(x) {
return this.timeChart ? this.parseTime(x, false) : x;
},
/**
* Displays a chart based on the provided parameters (as objects in "this").
*
* @param {string} chartType - The type of the chart. Valid values are 'line', 'bar', 'radar', 'doughnut', 'polarArea', 'bubble' and 'scatter'. Default is 'bar'.
* @param {object} data - The data source for the chart. Can be an array of objects or an object with x and y arrays.
* @param {object} stat - The statistics for the chart. Includes min, max, mean, median and stdev. If not provided, it will be calculated.
* @param {string} labelField - The field representing the label in the data source.
* @param {string} valueField - The field representing the value in the data source. Can be an array of fields, in which case the chart will display multiple series.
* @param {string} seriesField - The field representing the series in the data source.
* @param {string} title - The title of the chart.
* @param {string} unit - The unit of measurement for the chart.
* @param {string} xScale - The scale type of the x-axis. Valid values are 'linear','time' and 'category'. Default is 'linear'.
* @param {boolean} xyChart - Whether the chart is an XY chart (expects that data object contains x and y arrays). Default is false.
* @param {boolean} trendLine - Whether to display a trend line. Default is false.
* @param {boolean} secondDerivative - Whether to display the second derivative. Default is false.
* @param {boolean} numerical - Whether the values are numerical. Default is true.
* @param {boolean} feature - The feature to display on the chart. Default is null.
* @param {string} annotationAxis - The axis to display annotations on. Default is 'y'.
* @param {string} help - The help text to display.
* @param {string} titleToShow - The title to show on the help button.
* @param {boolean} persistent - Whether the dialog is persistent.
* @param {number} pointRadius - Point radius for the chart. Default is 1.
*/
async showChart() {
this.chartData = {};
this.chartOptions = {};
let unitSuffix = this.unitText(this.unit);
this.timeChart = this.xScale == "time";
let transparency = 1;
if (this.numerical) {
this.title = this.title + unitSuffix;
}
let tl = null;
let series = {};
console.log(this.data);
if (this.statInRow) {
let d = this.data.row;
this.stat = { min : d.min, max : d.max, mean : d.mean, median : d.median, stdev : d.stdev, n : d.n, q1 : d.q1, q3 : d.q3 };
}
if (this.xyChart) {
this.chartData.labels = this.data.x;
if (this.data.y) { // single series
series[this.seriesName ?? "none"] = this.data.y;
} else { // multiple series
series = this.data.series;
};
} else if (this.stacked) {
this.chartData.labels = this.data.map(f => f[this.labelField]);
for (let s of this.valueField) {
series[s] = this.data.map(f => f[s]);
}
} else if (this.seriesField) {
for (let item of this.data) {
let s = item[this.seriesField];
if (!series[s]) series[s] = [];
series[s].push({ x: this.xLabel(item[this.labelField]), y: item[this.valueField] });
}
} else if (this.chartType == "boxplot") {
transparency = 0.2;
let s = this.labelField;
this.chartData.labels = this.data.map(f => f[this.labelField]);
series[s] = this.data.map(f => ({
min: f.min, max: f.max, q1: f.q1, q3: f.q3, median: f.median, mean: f.mean
}));
} else if (Array.isArray(this.valueField)) {
for (let s of this.valueField) {
series[s] = this.data.map(f => ({ x: this.xLabel(f[this.labelField]), y: f[s] }));
}
} else if (this.valueField.startsWith(">")) {
let start = parseInt(this.valueField.substring(1));
for (let i = 1; i <= Object.keys(this.data[0]).length; i++) {
if (i >= start) {
let s = Object.keys(this.data[0])[i - 1];
series[s] = this.data.map(f => ({ x: this.xLabel(f[this.labelField]), y: f[s] }));
}
}
} else {
series[this.seriesName ?? this.valueField] = this.data.map(f => ({ x: this.xLabel(f[this.labelField], false), y: f[this.valueField] }));
if (!this.stat) {
let v = this.data.map(f => f[this.valueField]).sort();
this.stat = this.statistics(v);
}
tl = this.trendLine ? this.calcTrendLine(this.data, this.labelField, this.valueField, this.xScale) : null;
}
this.chartData.datasets = [];
let i = 0;
let pointRadius = this.pointRadius ?? 1;
for (let s of Object.keys(series).sort()) {
this.chartData.datasets.push({
label: this.snakeToSentence(s),
data: series[s],
pointRadius: series[s].length > 1 ? pointRadius : 5,
backgroundColor: this.hexToRgba(this.chartColors[i % this.chartColors.length], transparency),
borderColor: this.chartColors[i % this.chartColors.length],
lineTension: 0,
coef: 0
});
if (this.secondDerivative && !this.xyChart) {
let d = this.derivative(series[s]);
let sd = this.derivative(d);
this.chartData.datasets.push({
label: this.snakeToSentence(s) + '"',
data: sd,
pointRadius: series[s].length > 1 ? 1 : 5,
backgroundColor: this.chartColors[i % this.chartColors.length],
borderColor: this.chartColors[i % this.chartColors.length],
lineTension: 0,
borderDash: [5, 5],
yAxisID: 'y2',
});
}
i++;
}
if (tl != null) {
this.chartData.datasets.push({
label: this.$t('Trendline'),
data: tl.trendline,
type: 'line',
borderColor: 'red',
backgroundColor: 'red',
pointRadius: 0,
borderWidth: 1,
pointHitRadius: 5,
showLine: true,
lineTension: 0,
borderDash: [5, 5],
});
}
this.chartOptions = {
showDataLabels: this.showDataLabels,
dataLabelsAlign: this.dataLabelsAlign,
scales: {
x: {
title : {
display: true,
text: this.labelField ? this.snakeToSentence(this.labelField) : this.$t('Value')
},
type: this.xScale,
stacked: this.stacked,
time: {
unit: this.timeUnit ?? 'day',
displayFormats: {
day: 'YYYY-MM-DD',
hour: 'HH:mm'
}
},
ticks: {
major: {
enabled: true,
fontStyle: 'bold'
},
autoSkip: true, // Ensures that ticks are not skipped
maxTicksLimit: 100, // Maximum number of ticks to show
},
},
y: {
type: this.yScale,
stacked: this.stacked,
position: 'left',
// min: stat.min - span, //Math.min(stat.min, stat.min - stat.stdev),
// max: stat.max + span //Math.max(stat.max, stat.max + stat.stdev),
}
},
plugins: {
legend: {
position: 'top',
labels: {
filter: item => item.text != "None" && item.text != "null"
}
}
}
};
if (this.secondDerivative) {
this.chartOptions.scales.y2 = {
position: 'right',
title: {
display: true,
text: '2nd Derivative'
},
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
};
}
this.chartOptions.annotations = [];
if (this.stat != null && this.numerical) {
this.chartOptions.annotations = [
{
axis: null,
n: this.stat.n,
sigma: this.stat.stdev,
color: 'darkblue'
},
{
axis: this.annotationAxis,
label: {
text: this.$t('Mean') + ': ' + this.stat.mean.toFixed(2) + unitSuffix,
color: 'red',
},
color: 'red',
value: this.stat.mean
},
{
axis: this.annotationAxis,
label: {
text: this.$t('Median') + ': ' + this.stat.median.toFixed(2) + unitSuffix,
color: 'green',
},
value: this.stat.median,
color: 'green',
offset: this.annotationAxis == 'y' ? 200 : 30,
},
{
axis: this.annotationAxis,
label: {
text: "-σ" + ': ' + (this.stat.mean - this.stat.stdev).toFixed(2) + unitSuffix,
color: 'gray',
},
value: this.stat.mean - this.stat.stdev,
color: 'gray',
lineDash: [5, 5],
offset: this.annotationAxis == 'y' ? 0 : 50,
},
{
axis: this.annotationAxis,
label: {
text: "+σ" + ': ' + (this.stat.mean + this.stat.stdev).toFixed(2) + unitSuffix,
color: 'gray',
},
value: this.stat.mean + this.stat.stdev,
color: 'gray',
lineDash: [5, 5],
offset: this.annotationAxis == 'y' ? 0 : 70,
},
{
axis: this.annotationAxis,
label: {
text: "q1" + ': ' + this.stat.q1.toFixed(2) + unitSuffix,
color: 'gray',
},
value: this.stat.q1,
color: 'green',
lineDash: [15, 5],
offset: this.annotationAxis == 'y' ? 0 : 90,
},
{
axis: this.annotationAxis,
label: {
text: "q3" + ': ' + this.stat.q3.toFixed(2) + unitSuffix,
color: 'green',
},
value: this.stat.q3,
color: 'gray',
lineDash: [15, 5],
offset: this.annotationAxis == 'y' ? 0 : 110,
}
];
}
if (this.additionalAnnotations) {
this.additionalAnnotations = this.additionalAnnotations.map(a => ({ axis: this.annotationAxis, ...a }));
this.chartOptions.annotations = this.chartOptions.annotations.concat(this.additionalAnnotations);
// additionalAnnotations is an array of objects with the following properties e.g.:
// {
// label: {
// text: name + ': ' + value.toFixed(2) + unitSuffix,
// color: 'blue',
// },
// value: value,
// color: 'blue',
// offset: 50,
// };
}
this.loaded = true;
},
},
}
</script>