FRONTEND: Add stacked bar chart integration with error bars using Chart.js in ReportChart; include Chart.js dependency and enhance ReportRoute with updated icons

This commit is contained in:
Jan 2025-09-09 21:52:39 +02:00
parent abed6b82e5
commit 849d31bc8e
4 changed files with 240 additions and 11 deletions

View file

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"chart.js": "^4.5.0",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
@ -973,6 +974,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@phosphor-icons/vue": { "node_modules/@phosphor-icons/vue": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz",
@ -1719,6 +1726,18 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chart.js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",

View file

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"chart.js": "^4.5.0",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",

View file

@ -1,15 +1,216 @@
<script lang="ts"> <template>
import {defineComponent} from 'vue' <div class="chart-container">
<canvas ref="stackedBarChart"></canvas>
</div>
</template>
export default defineComponent({ <script>
name: "ReportChart"
}) import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
BarController,
ScatterController,
Title,
Tooltip,
Legend,
PointElement
} from 'chart.js';
// Register the required components
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
BarController,
ScatterController,
Title,
Tooltip,
Legend,
PointElement
);
export default {
name: "ReportChart",
computed: {
},
data() {
return {
data: {
labels: ['Sample A'],
datasets: [
{
label: 'MEK A',
data: [35],
backgroundColor: '#6B869C', // Blue-gray like in your image
borderColor: '#6B869C',
borderWidth: 1,
stack: 'stack1'
},
{
label: 'Logistics costs',
data: [15],
backgroundColor: '#5AF0B4', // Mint green like in your image
borderColor: '#5AF0B4',
borderWidth: 1,
stack: 'stack1'
},
{
label: 'Error Bars',
// data: [50, 65, 58, 75], // Total height (used for error bar positioning)
type: 'scatter',
backgroundColor: 'transparent',
borderColor: 'transparent',
pointRadius: 0,
showLine: false,
stack: 'errorBars',
errorBars: {
plus: [5], // Upper error
minus: [4] // Lower error
}
}
]
}
}
},
mounted() {
const ctx = this.$refs.stackedBarChart.getContext('2d');
// Sample data
const data = this.data;
const config = {
type: 'bar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: {
display: false
},
ticks: {
display: false //this will remove only the label
},
border: {
display: false
}
},
y: {
stacked: true, // Don't stack for error bars
beginAtZero: true,
max: 90,
grid: {
color: '#E3EDFF'
},
ticks: {
display: false //this will remove only the label
},
border: {
display: false
}
},
},
plugins: {
legend: {
display: false,
labels: {
filter: function (item, chart) {
// Hide the error bars dataset from legend
return item.text !== 'Error Bars';
}
}
},
tooltip: {
filter: function (tooltipItem) {
// Don't show tooltip for error bars dataset
return tooltipItem.datasetIndex !== 2;
}
}
},
animation: {
onComplete: function () {
// Draw error bars after animation completes
drawErrorBars(this);
}
}
},
plugins: [{
afterDraw: function (chart) {
drawErrorBars(chart);
}
}]
};
function drawErrorBars(chart) {
const ctx = chart.ctx;
const meta = chart.getDatasetMeta(0); // Get metadata for first dataset
const errorData = chart.data.datasets[2].errorBars;
if (!errorData) return;
ctx.strokeStyle = '#002F54';
ctx.lineWidth = 1;
meta.data.forEach((bar, index) => {
const x = bar.x;
// Calculate the top of the stacked bar (this is the center point for error bars)
const topValue = chart.data.datasets[0].data[index] + chart.data.datasets[1].data[index];
const yCenterPixel = chart.scales.y.getPixelForValue(topValue);
// Error bar measurements
const errorUp = errorData.plus[index];
const errorDown = errorData.minus[index];
// Convert error values to pixel distances
const errorUpPixels = chart.scales.y.getPixelForValue(topValue) - chart.scales.y.getPixelForValue(topValue + errorUp);
const errorDownPixels = chart.scales.y.getPixelForValue(topValue - errorDown) - chart.scales.y.getPixelForValue(topValue);
const yErrorTop = yCenterPixel - errorUpPixels;
const yErrorBottom = yCenterPixel + errorDownPixels;
// Draw vertical line
ctx.beginPath();
ctx.moveTo(x, yErrorTop);
ctx.lineTo(x, yErrorBottom);
ctx.stroke();
// Draw top cap
ctx.beginPath();
ctx.moveTo(x - 8, yErrorTop);
ctx.lineTo(x + 8, yErrorTop);
ctx.stroke();
// Draw bottom cap
ctx.beginPath();
ctx.moveTo(x - 8, yErrorBottom);
ctx.lineTo(x + 8, yErrorBottom);
ctx.stroke();
});
}
// Create the chart
const stackedBarChart = new ChartJS(ctx, config);
}
}
</script> </script>
<template>
</template>
<style scoped> <style scoped>
.chart-container {
position: relative;
height: 300px;
}
</style> </style>

View file

@ -16,7 +16,7 @@
v-if="section.transport_type === 'ROAD' || section.transport_type === 'POST_RUN'"></ph-truck> v-if="section.transport_type === 'ROAD' || section.transport_type === 'POST_RUN'"></ph-truck>
<div> <div>
{{ section.from_node.external_mapping_id ?? section.from_node.name }} > {{ section.from_node.external_mapping_id ?? section.from_node.name }} <PhArrowRight :size="12" weight="bold" />
{{ section.to_node.external_mapping_id ?? section.to_node.name }} {{ section.to_node.external_mapping_id ?? section.to_node.name }}
</div> </div>
</div> </div>
@ -40,11 +40,19 @@
</template> </template>
<script> <script>
import {PhBoat, PhTrain, PhTruck, PhTruckTrailer} from "@phosphor-icons/vue"; import {
PhArrowRight,
PhBoat,
PhCaretDoubleRight,
PhCaretRight,
PhTrain,
PhTruck,
PhTruckTrailer
} from "@phosphor-icons/vue";
export default { export default {
name: 'ReportRoute', name: 'ReportRoute',
components: {PhTruck, PhTruckTrailer, PhTrain, PhBoat}, components: {PhArrowRight, PhCaretDoubleRight, PhCaretRight, PhTruck, PhTruckTrailer, PhTrain, PhBoat},
props: { props: {
sections: { sections: {
type: Array, type: Array,