|
|
|
|
@ -5,8 +5,10 @@ import * as _ from "lodash";
|
|
|
|
|
import * as log from "loglevel";
|
|
|
|
|
import { computed, onMounted } from "vue";
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import colors from "@/colors.json";
|
|
|
|
|
import type { LearningSequence } from "@/types";
|
|
|
|
|
import type { DefaultArcObject } from "d3";
|
|
|
|
|
|
|
|
|
|
const circleStore = useCircleStore();
|
|
|
|
|
|
|
|
|
|
@ -33,6 +35,17 @@ onMounted(async () => {
|
|
|
|
|
render();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
interface CirclePie extends d3.PieArcDatum<number> {
|
|
|
|
|
title: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
translation_key: string;
|
|
|
|
|
arrowStartAngle: number;
|
|
|
|
|
arrowEndAngle: number;
|
|
|
|
|
someFinished: boolean;
|
|
|
|
|
allFinished: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pieData = computed(() => {
|
|
|
|
|
const circle = circleStore.circle;
|
|
|
|
|
console.log("initial of compute pie data ", circle);
|
|
|
|
|
@ -42,193 +55,203 @@ const pieData = computed(() => {
|
|
|
|
|
|
|
|
|
|
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
|
|
|
|
|
const pieGenerator = d3.pie();
|
|
|
|
|
let angles = pieGenerator(pieWeights);
|
|
|
|
|
_.forEach(angles, (pie) => {
|
|
|
|
|
pie.startAngle = pie.startAngle + Math.PI;
|
|
|
|
|
pie.endAngle = pie.endAngle + Math.PI;
|
|
|
|
|
const thisLearningSequence = circle.learningSequences[pie.index];
|
|
|
|
|
pie.title = thisLearningSequence.title;
|
|
|
|
|
pie.icon = thisLearningSequence.icon;
|
|
|
|
|
pie.arrowStartAngle = pie.endAngle + (pie.startAngle - pie.endAngle) / 2;
|
|
|
|
|
pie.arrowEndAngle = pie.startAngle + (pie.startAngle - pie.endAngle) / 2;
|
|
|
|
|
pie.translation_key = thisLearningSequence.translation_key;
|
|
|
|
|
pie.slug = thisLearningSequence.slug;
|
|
|
|
|
pie.someFinished = someFinished(thisLearningSequence);
|
|
|
|
|
pie.allFinished = allFinished(thisLearningSequence);
|
|
|
|
|
const angles = pieGenerator(pieWeights);
|
|
|
|
|
let result = angles.map((angle) => {
|
|
|
|
|
const thisLearningSequence = circle.learningSequences[angle.index];
|
|
|
|
|
|
|
|
|
|
return Object.assign(
|
|
|
|
|
{
|
|
|
|
|
startAngle: angle.startAngle + Math.PI,
|
|
|
|
|
endAngle: angle.endAngle + Math.PI,
|
|
|
|
|
..._.pick(thisLearningSequence, ["title", "icon", "translation_key", "slug"]),
|
|
|
|
|
arrowStartAngle: angle.endAngle + (angle.startAngle - angle.endAngle) / 2,
|
|
|
|
|
arrowEndAngle: angle.startAngle + (angle.startAngle - angle.endAngle) / 2,
|
|
|
|
|
someFinished: someFinished(thisLearningSequence),
|
|
|
|
|
allFinished: allFinished(thisLearningSequence),
|
|
|
|
|
},
|
|
|
|
|
angle
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
angles = angles.reverse();
|
|
|
|
|
return angles;
|
|
|
|
|
result = result.reverse();
|
|
|
|
|
return result as CirclePie[];
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const width = 450;
|
|
|
|
|
const height = 450;
|
|
|
|
|
const radius = Math.min(width, height) / 2.4;
|
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
const arrowStrokeWidth = 2;
|
|
|
|
|
function getColor(d: CirclePie) {
|
|
|
|
|
let color = colors.gray[300];
|
|
|
|
|
if (d.someFinished) {
|
|
|
|
|
color = colors.sky[500];
|
|
|
|
|
}
|
|
|
|
|
if (d.allFinished) {
|
|
|
|
|
color = colors.green[500];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getHoverColor(d: CirclePie) {
|
|
|
|
|
let color = colors.gray[200];
|
|
|
|
|
if (d.someFinished) {
|
|
|
|
|
color = colors.sky[400];
|
|
|
|
|
}
|
|
|
|
|
if (d.allFinished) {
|
|
|
|
|
color = colors.green[400];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
const svg = d3.select(".circle-visualization");
|
|
|
|
|
// Clean svg before adding new stuff.
|
|
|
|
|
svg.selectAll("*").remove();
|
|
|
|
|
|
|
|
|
|
// 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]);
|
|
|
|
|
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 + ")");
|
|
|
|
|
const g = svg
|
|
|
|
|
.append("g")
|
|
|
|
|
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
|
|
|
|
|
|
|
|
|
|
function getColor(d) {
|
|
|
|
|
let color = colors.gray[300];
|
|
|
|
|
if (d.someFinished) {
|
|
|
|
|
color = colors.sky[500];
|
|
|
|
|
}
|
|
|
|
|
if (d.allFinished) {
|
|
|
|
|
color = colors.green[500];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
// Generate the pie diagram wede
|
|
|
|
|
const wedgeGenerator = d3
|
|
|
|
|
.arc()
|
|
|
|
|
.innerRadius(radius / 2.5)
|
|
|
|
|
.padAngle(12 / 360)
|
|
|
|
|
.outerRadius(radius);
|
|
|
|
|
|
|
|
|
|
// Generate the arrows
|
|
|
|
|
const arrowRadius = radius * 1.1;
|
|
|
|
|
|
|
|
|
|
const learningSequences = g
|
|
|
|
|
.selectAll(".learningSegmentArc")
|
|
|
|
|
.data(pieData.value)
|
|
|
|
|
.enter()
|
|
|
|
|
.append("g")
|
|
|
|
|
.attr("class", "learningSegmentArc")
|
|
|
|
|
.attr("role", "button")
|
|
|
|
|
.attr("fill", colors.gray[300]);
|
|
|
|
|
|
|
|
|
|
learningSequences
|
|
|
|
|
.on("mouseover", function (d, i) {
|
|
|
|
|
d3.select(this)
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(200)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return getHoverColor(d);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.on("mouseout", function (d, i) {
|
|
|
|
|
d3.select(this)
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(200)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
return getColor(d);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.on("click", function (d, elm) {
|
|
|
|
|
console.log("clicked on ", d, elm);
|
|
|
|
|
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
learningSequences
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(1)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
return getColor(d);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
learningSequences.append("path").attr("d", wedgeGenerator);
|
|
|
|
|
|
|
|
|
|
const learningSequenceText = learningSequences
|
|
|
|
|
.append("text")
|
|
|
|
|
.attr("fill", colors.blue[900])
|
|
|
|
|
.style("font-size", "15px")
|
|
|
|
|
.text((d) => {
|
|
|
|
|
return d.title;
|
|
|
|
|
})
|
|
|
|
|
.attr("transform", function (d) {
|
|
|
|
|
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
|
|
|
|
|
translate = [translate[0], translate[1] + 20];
|
|
|
|
|
return "translate(" + translate + ")";
|
|
|
|
|
})
|
|
|
|
|
.attr("class", "circlesText text-large font-bold")
|
|
|
|
|
.style("text-anchor", "middle");
|
|
|
|
|
|
|
|
|
|
const iconWidth = 25;
|
|
|
|
|
|
|
|
|
|
const learningSequenceIcon = learningSequences
|
|
|
|
|
.append("svg:image")
|
|
|
|
|
.attr("xlink:href", (d) => {
|
|
|
|
|
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
|
|
|
|
|
})
|
|
|
|
|
.attr("width", iconWidth)
|
|
|
|
|
.attr("height", iconWidth)
|
|
|
|
|
.attr("transform", function (d) {
|
|
|
|
|
let translate = wedgeGenerator.centroid(d as unknown as DefaultArcObject);
|
|
|
|
|
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
|
|
|
|
|
return "translate(" + translate + ")";
|
|
|
|
|
})
|
|
|
|
|
.attr("class", "filter-blue-900");
|
|
|
|
|
|
|
|
|
|
// Create Arrows
|
|
|
|
|
const arrow = d3
|
|
|
|
|
.arc()
|
|
|
|
|
.innerRadius(arrowRadius)
|
|
|
|
|
.outerRadius(arrowRadius + arrowStrokeWidth)
|
|
|
|
|
.padAngle(20 / 360)
|
|
|
|
|
.startAngle((d) => {
|
|
|
|
|
return (d as unknown as CirclePie).arrowStartAngle;
|
|
|
|
|
})
|
|
|
|
|
.endAngle((d) => {
|
|
|
|
|
return (d as unknown as CirclePie).arrowEndAngle;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const arrows = g
|
|
|
|
|
.selectAll(".arrow")
|
|
|
|
|
.data(pieData.value)
|
|
|
|
|
.join("g")
|
|
|
|
|
.attr("class", "arrow")
|
|
|
|
|
.attr("marker-end", "url(#triangle)");
|
|
|
|
|
|
|
|
|
|
// remove last arrow
|
|
|
|
|
d3.selection.prototype.last = function () {
|
|
|
|
|
const last = this.size() - 1;
|
|
|
|
|
return d3.select(this.nodes()[last]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const all_arows = g.selectAll(".arrow");
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
all_arows.last().remove();
|
|
|
|
|
|
|
|
|
|
//Draw arrow paths
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getHoverColor(d) {
|
|
|
|
|
let color = colors.gray[200];
|
|
|
|
|
if (d.someFinished) {
|
|
|
|
|
color = colors.sky[400];
|
|
|
|
|
}
|
|
|
|
|
if (d.allFinished) {
|
|
|
|
|
color = colors.green[400];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate the pie diagram wede
|
|
|
|
|
const wedgeGenerator = d3
|
|
|
|
|
.arc()
|
|
|
|
|
.innerRadius(radius / 2.5)
|
|
|
|
|
.padAngle(12 / 360)
|
|
|
|
|
.outerRadius(radius);
|
|
|
|
|
|
|
|
|
|
// Generate the arrows
|
|
|
|
|
const arrowRadius = radius * 1.1;
|
|
|
|
|
|
|
|
|
|
const learningSequences = g
|
|
|
|
|
.selectAll(".learningSegmentArc")
|
|
|
|
|
.data(pieData.value)
|
|
|
|
|
.enter()
|
|
|
|
|
.append("g")
|
|
|
|
|
.attr("class", "learningSegmentArc")
|
|
|
|
|
.attr("role", "button")
|
|
|
|
|
.attr("fill", colors.gray[300]);
|
|
|
|
|
|
|
|
|
|
learningSequences
|
|
|
|
|
.on("mouseover", function (d, i) {
|
|
|
|
|
d3.select(this)
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(200)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
return getHoverColor(d);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.on("mouseout", function (d, i) {
|
|
|
|
|
d3.select(this)
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(200)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
return getColor(d);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.on("click", function (d, elm) {
|
|
|
|
|
console.log("clicked on ", d, elm);
|
|
|
|
|
document.getElementById(elm.slug)?.scrollIntoView({ behavior: "smooth" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
learningSequences
|
|
|
|
|
.transition()
|
|
|
|
|
.duration(1)
|
|
|
|
|
.attr("fill", (d) => {
|
|
|
|
|
return getColor(d);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
learningSequences.append("path").attr("d", wedgeGenerator);
|
|
|
|
|
|
|
|
|
|
const learningSequenceText = learningSequences
|
|
|
|
|
.append("text")
|
|
|
|
|
.attr("fill", colors.blue[900])
|
|
|
|
|
.style("font-size", "15px")
|
|
|
|
|
.text((d) => {
|
|
|
|
|
return d.title;
|
|
|
|
|
})
|
|
|
|
|
.attr("transform", function (d) {
|
|
|
|
|
let translate = wedgeGenerator.centroid(d);
|
|
|
|
|
translate = [translate[0], translate[1] + 20];
|
|
|
|
|
return "translate(" + translate + ")";
|
|
|
|
|
})
|
|
|
|
|
.attr("class", "circlesText text-large font-bold")
|
|
|
|
|
.style("text-anchor", "middle");
|
|
|
|
|
|
|
|
|
|
const iconWidth = 25;
|
|
|
|
|
|
|
|
|
|
const learningSequenceIcon = learningSequences
|
|
|
|
|
.append("svg:image")
|
|
|
|
|
.attr("xlink:href", (d) => {
|
|
|
|
|
return "/static/icons/" + d.icon.replace("it-", "") + ".svg";
|
|
|
|
|
})
|
|
|
|
|
.attr("width", iconWidth)
|
|
|
|
|
.attr("height", iconWidth)
|
|
|
|
|
.attr("transform", function (d) {
|
|
|
|
|
let translate = wedgeGenerator.centroid(d);
|
|
|
|
|
translate = [translate[0] - iconWidth / 2, translate[1] - iconWidth];
|
|
|
|
|
return "translate(" + translate + ")";
|
|
|
|
|
})
|
|
|
|
|
.attr("class", "filter-blue-900");
|
|
|
|
|
|
|
|
|
|
// Create Arrows
|
|
|
|
|
const arrow = d3
|
|
|
|
|
.arc()
|
|
|
|
|
.innerRadius(arrowRadius)
|
|
|
|
|
.outerRadius(arrowRadius + arrowStrokeWidth)
|
|
|
|
|
.padAngle(20 / 360)
|
|
|
|
|
.startAngle((d) => {
|
|
|
|
|
return d.arrowStartAngle;
|
|
|
|
|
})
|
|
|
|
|
.endAngle((d) => {
|
|
|
|
|
return d.arrowEndAngle;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const arrows = g
|
|
|
|
|
.selectAll(".arrow")
|
|
|
|
|
.data(pieData.value)
|
|
|
|
|
.join("g")
|
|
|
|
|
.attr("class", "arrow")
|
|
|
|
|
.attr("marker-end", "url(#triangle)");
|
|
|
|
|
|
|
|
|
|
// remove last arrow
|
|
|
|
|
d3.selection.prototype.last = function () {
|
|
|
|
|
const last = this.size() - 1;
|
|
|
|
|
return d3.select(this.nodes()[last]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const all_arows = g.selectAll(".arrow");
|
|
|
|
|
all_arows.last().remove();
|
|
|
|
|
|
|
|
|
|
//Draw arrow paths
|
|
|
|
|
arrows.append("path").attr("fill", colors.gray[500]).attr("d", arrow);
|
|
|
|
|
return svg;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|