<template>
<q-dialog v-model="$store.chart.show" class="max-width" :persistent="persistent" full-width fullHeight
@hide="closeDialog" @keyup.esc="closeDialog" :maximized="true">
<q-card flat>
<q-card-section dense class="row items-center text-bold q-pa-sm">
<span v-html="title" />
<q-space />
<q-btn class="copy" dense flat size="sm" icon="content_copy" @click="copyToClipboard" />
<q-btn dense flat size="sm" icon="download" @click="download" />
<help-button v-if="help" :name="$t(help)" :titleToShow="titleToShow ? $t(titleToShow) : $t(help)" />
<q-btn dense size="sm" flat round icon="close" @click="closeDialog" />
</q-card-section>
<q-card-section class="q-pa-none">
<chart ref="chart" :chartData="chartData" :chartOptions="chartOptions" :type="chartType" />
</q-card-section>
</q-card>
</q-dialog>
</template>
<script>
/**
* Chart component
*
* @component
* @name Chart
* @example
* <Chart />
*/
import Chart from "./chart.vue";
import { StatisticsMixin } from "../mixins/statistics";
import HelpButton from "./help-button.vue";
import { copyToClipboard } from "quasar";
import { getTransitionRawChildren } from "vue";
export default {
name: "ChartPopup",
mixins: [StatisticsMixin],
components: {
Chart,
HelpButton,
},
data: () => ({
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.copyObject(this.$store.chart.props, this, true);
this.showChart();
},
methods: {
/**
* Downloads the chart as an image.
*/
download() {
this.$refs.chart.download();
},
/**
* Copies the chart as an image to clipboard.
*/
copyToClipboard() {
this.$refs.chart.copyToClipboard();
},
/**
* Closes the dialog box.
*/
closeDialog() {
this.$store.chart.show = false;
},
/**
* 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, avg, 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.
*/
async showChart() {
this.chartData = {};
this.chartOptions = {};
let unitSuffix = this.unitText(this.unit);
this.timeChart = this.xScale == "time";
if (this.numerical) {
this.title = this.title + unitSuffix;
}
let tl = null;
let series = {};
if (this.xyChart) {
this.chartData.labels = this.data.x;
if (this.data.y) { // single series
series["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 (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) : null;
}
this.chartData.datasets = [];
let i = 0;
for (let s of Object.keys(series).sort()) {
this.chartData.datasets.push({
label: s,
data: series[s],
pointRadius: series[s].length > 1 ? 1 : 5,
backgroundColor: this.chartColors[i % this.chartColors.length],
borderColor: this.chartColors[i % this.chartColors.length],
lineTension: 0
});
if (this.secondDerivative && !this.xyChart) {
let d = this.derivative(series[s]);
let sd = this.derivative(d);
this.chartData.datasets.push({
label: 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: {
type: this.xScale,
stacked: this.stacked,
time: {
unit: 'day',
displayFormats: {
day: 'YYYY-MM-DD'
}
},
},
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: this.annotationAxis,
label: {
text: this.$t('Average') + ': ' + this.stat.avg.toFixed(2) + unitSuffix,
color: 'red',
},
color: 'red',
value: this.stat.avg,
},
{
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.avg - this.stat.stdev).toFixed(2) + unitSuffix,
color: 'gray',
},
value: this.stat.avg - this.stat.stdev,
color: 'gray',
lineDash: [5, 5],
offset: this.annotationAxis == 'y' ? 0 : 80,
},
{
axis: this.annotationAxis,
label: {
text: "+σ" + ': ' + (this.stat.avg + this.stat.stdev).toFixed(2) + unitSuffix,
color: 'gray',
},
value: this.stat.avg + this.stat.stdev,
color: 'gray',
lineDash: [5, 5],
offset: this.annotationAxis == 'y' ? 0 : 80,
}
];
}
if (this.feature) {
let value = this.feature.get("value_avg");
if (!value) value = this.feature.get("value");
if (value) {
let name = this.feature.get("name") ?? this.feature.get("Name");
this.chartOptions.annotations.push({
axis: this.annotationAxis,
label: {
text: name + ': ' + value.toFixed(2) + unitSuffix,
color: 'blue',
},
value: value,
color: 'blue',
offset: 50,
});
}
};
},
},
}
</script>