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:
parent
abed6b82e5
commit
849d31bc8e
4 changed files with 240 additions and 11 deletions
19
src/frontend/package-lock.json
generated
19
src/frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue