<template>
<div id="chart-container">
<canvas id="chart" class="chart" ref="chart"></canvas>
</div>
</template>
<script>
import Chart from 'chart.js/auto';
import 'chartjs-adapter-dayjs-4';
import Zoom from 'chartjs-plugin-zoom';
import Datalabels from 'chartjs-plugin-datalabels';
/**
* Vue component for rendering a chart using Chart.js library.
*
* @component chart
* @prop {Object} chartData - The data object for the chart.
* @prop {Object} chartOptions - The options object for the chart.
* @prop {String} type - The type of chart to render.
*
* @emits chartRendered - Event emitted when the chart is rendered.
* @emits chartDestroyed - Event emitted when the chart is destroyed.
*
* @example
<chart :chart-data="data" :chart-options="options" type="line"></chart>
*/
export default {
name: "Chart",
props: {
chartData: Object,
chartOptions: Object,
type: String
},
data() {
return {
chart: null,
title: "Chart",
annotationPlugin: {
id: 'annotation',
/**
* Calculates the index greater than or equal to the given value in the specified array.
*
* @param {Array} array - The array to search in.
* @param {number} value - The value to compare against.
* @returns {number} - The index of the first element in the array that is greater than or equal to the given value.
*/
calcIndexGE(array, value) {
let left = 0;
let right = array.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (array[mid] < value) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
},
/**
* Hook that is called after the chart is initialized.
* @param {Object} chart - The chart object.
*/
afterInit(chart) {
if (chart.config.options.annotations === undefined) {
chart.config.options.annotations = [];
return;
}
for (let a of chart.config.options.annotations) {
if (a.axis == 'x') {
a.indexGE = this.calcIndexGE(chart.data.labels, a.value);
a.indexLE = chart.data.labels[this.indexGE] == a.value ? a.indexGE : a.indexGE - 1;
}
}
},
/**
* Hook that is called after datasets are drawn. Draws annotations.
* @param {Object} chart - The chart object.
*/
afterDatasetsDraw(chart) {
const ctx = chart.ctx;
const xAxis = chart.scales['x'];
const yAxis = chart.scales['y'];
for (let a of chart.config.options.annotations) {
ctx.save();
let xPos, yPos;
if (a.axis == 'x') {
yPos = yAxis.top;
//xPos = (xAxis.getPixelForValue(a.indexLE) + xAxis.getPixelForValue(a.indexGE)) / 2;
let x1 = xAxis.getPixelForValue(a.indexLE);
let x2 = xAxis.getPixelForValue(a.indexGE);
let v1 = chart.data.labels[a.indexLE];
let v2 = chart.data.labels[a.indexGE];
xPos = x1 + (x2 - x1) * (a.value - v1) / (v2 - v1);
} else {
xPos = xAxis.left;
yPos = yAxis.getPixelForValue(a.value);
}
ctx.strokeStyle = a.color ?? 'red'; // Line color
ctx.setLineDash(a.lineDash ?? []);
ctx.lineWidth = 2; // Line width
ctx.beginPath();
ctx.moveTo(xPos, yPos);
if (a.axis == 'x') {
ctx.lineTo(xPos, yPos + yAxis.height);
} else {
ctx.lineTo(xPos + xAxis.width, yPos);
}
ctx.stroke();
ctx.restore();
// Draw the label
ctx.fillStyle = a.label.color ?? 'black'; // Label color
ctx.font = '14px Arial'; // Label font
if (a.axis == 'x') {
ctx.fillText(a.label.text, xPos + 3, yPos + (a.offset ?? 10)); // Adjust
} else {
ctx.fillText(a.label.text, xPos + (a.offset ?? 10), yPos + 9); // Adjust
}
}
},
}
};
},
mounted() {
this.renderChart();
},
unmounted() {
if (this.chart) {
this.chart.destroy();
}
},
methods: {
/**
* Downloads the chart image.
*/
download() {
const canvas = this.$refs.chart;
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = 'chart.png';
link.click();
},
/**
* Copies the chart image to the clipboard.
*/
copyToClipboard() {
const canvas = this.$refs.chart;
canvas.toBlob((blob) => {
const item = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([item])
.then(() => {
this.$q.notify({
message: this.$t("Image copied to clipboard"),
color: "positive",
timeout: 1000,
position: "bottom"
});
})
.catch((err) => {
this.showError("Copy failed: ", err);
});
}, 'image/png');
},
/**
* Renders the chart.
*/
renderChart() {
const ctx = this.$refs.chart.getContext('2d');
let options = this.chartOptions;
console.log("options", options);
options.maintainAspectRatio = false;
options.scales.x.adapters = {
date: this.dateAdapter
};
// options.plugins.title = {
// display: true,
// text: this.subSupToUnicode("H<sub>2</sub>O<sup>+</sup> Concentration vs. Time"),
// font: {
// size: 16
// }
// };
options.plugins.datalabels = {
display: options.showDataLabels ?? false,
formatter: (value, context) => {
return value.y;
},
color: '#000',
anchor: 'end',
align: options.dataLabelsAlign ?? 'top'
}
options.plugins.zoom = {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x'
}
};
this.title = options.title;
this.chart = new Chart(ctx, {
type: this.type,
data: this.chartData,
options: options,
plugins: [this.annotationPlugin, Zoom, Datalabels]
});
}
},
beforeDestroy() {
if (this.chart) {
this.chart.destroy();
}
}
};
</script>
<style scoped>
#chart-container {
width: 100%;
height: calc(100vh - 120px);
}
.center {
margin-left: auto;
margin-right: auto;
display: block;
text-align: center;
}
.copy {
position: absolute;
right: 0px;
top: 0px;
}
</style>