142 lines
3.7 KiB
Vue
142 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import * as d3 from "d3";
|
|
import { computed, onMounted } from "vue";
|
|
|
|
// @ts-ignore
|
|
import colors from "@/colors.json";
|
|
|
|
export type CircleSectorProgress = "none" | "in_progress" | "finished";
|
|
|
|
export interface CircleSectorData {
|
|
progress: CircleSectorProgress;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
sectors: CircleSectorData[];
|
|
}>();
|
|
|
|
onMounted(async () => {
|
|
render();
|
|
});
|
|
|
|
interface CircleSector extends d3.PieArcDatum<number> {
|
|
arrowStartAngle: number;
|
|
arrowEndAngle: number;
|
|
progress: CircleSectorProgress;
|
|
}
|
|
|
|
const pieData = computed(() => {
|
|
const pieWeights = new Array(Math.max(props.sectors.length, 1)).fill(1);
|
|
const pieGenerator = d3.pie();
|
|
const angles = pieGenerator(pieWeights);
|
|
return angles
|
|
.map((angle) => {
|
|
// Rotate the circle by PI (180 degrees) normally 0 = 12'o clock, now start at 6 o clock
|
|
angle.startAngle += Math.PI;
|
|
angle.endAngle += Math.PI;
|
|
|
|
return Object.assign(
|
|
{
|
|
startAngle: angle.startAngle,
|
|
endAngle: angle.endAngle,
|
|
arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2,
|
|
arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
|
|
progress: props.sectors[angle.index].progress,
|
|
},
|
|
angle
|
|
);
|
|
})
|
|
.reverse() as CircleSector[];
|
|
});
|
|
|
|
const width = 100;
|
|
const height = 100;
|
|
const radius = 50;
|
|
|
|
const svgId = computed(() => {
|
|
// Generate a random id for the svg element like 'circle-visualization-ziurxxmp'
|
|
return `circle-visualization-${Math.random()
|
|
.toString(36)
|
|
.replace(/[^a-z]+/g, "")
|
|
.substring(2, 10)}`;
|
|
});
|
|
|
|
function getColor(sector: CircleSector) {
|
|
let color = colors.gray[300];
|
|
if ("in_progress" === sector.progress) {
|
|
color = colors.sky[500];
|
|
}
|
|
if ("finished" === sector.progress) {
|
|
color = colors.green[500];
|
|
}
|
|
return color;
|
|
}
|
|
|
|
function render() {
|
|
const svg = d3.select("#" + svgId.value);
|
|
// Clean svg before adding new stuff.
|
|
svg.selectAll("*").remove();
|
|
|
|
if (pieData.value) {
|
|
const arrowStrokeWidth = 2;
|
|
// Append marker as definition to the svg
|
|
svg
|
|
.attr("viewBox", `0 0 ${width} ${height}`)
|
|
.append("svg:defs")
|
|
.append("svg:marker")
|
|
.attr("id", "triangle")
|
|
.attr("refX", 11)
|
|
.attr("refY", 11)
|
|
.attr("markerWidth", 20)
|
|
.attr("markerHeight", 20)
|
|
.attr("markerUnits", "userSpaceOnUse")
|
|
.attr("orient", "auto")
|
|
.append("path")
|
|
.attr("d", "M -1 0 l 10 0 M 0 -1 l 0 10")
|
|
.attr("transform", "rotate(-90, 10, 0)")
|
|
.attr("stroke-width", arrowStrokeWidth)
|
|
.attr("stroke", colors.gray[500]);
|
|
|
|
const g = svg
|
|
.append("g")
|
|
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
|
|
|
|
// Generate the pie diagram wedge
|
|
const wedgeGenerator = d3
|
|
.arc()
|
|
.innerRadius(radius / 2.5)
|
|
.padAngle(12 / 360)
|
|
.outerRadius(radius);
|
|
|
|
const learningSequences = g
|
|
.selectAll(".learningSegmentArc")
|
|
.data(pieData.value)
|
|
.enter()
|
|
.append("g")
|
|
.attr("class", "learningSegmentArc");
|
|
|
|
learningSequences
|
|
.transition()
|
|
.duration(1)
|
|
.attr("fill", (d) => {
|
|
return getColor(d);
|
|
});
|
|
|
|
// @ts-ignore
|
|
learningSequences.append("path").attr("d", wedgeGenerator);
|
|
}
|
|
return svg;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="svg-container content-center">
|
|
<pre hidden>{{ pieData }}</pre>
|
|
<pre hidden>{{ render() }}</pre>
|
|
<svg :id="svgId" class="h-full min-w-[20px]">
|
|
<circle :cx="width / 2" :cy="height / 2" :r="radius" :color="colors.gray[300]" />
|
|
<circle :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white" />
|
|
</svg>
|
|
</div>
|
|
</template>
|