206 lines
5.8 KiB
JavaScript
206 lines
5.8 KiB
JavaScript
/* This code is copywrite Telos Digital 2025 working for Telos Partners:
|
|
|
|
https://www.telospartners.com/
|
|
|
|
It is released under the MIT licence https://opensource.org/license/mit
|
|
|
|
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the “Software”), to
|
|
deal in the Software without restriction, including without limitation the
|
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
sell copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
*/
|
|
|
|
function drawtriangle(id,text,colour,values,rag,offset,mean)
|
|
{
|
|
|
|
const svg = d3.select(`${id}`);
|
|
const width = 500; // Fixed size. Was +svg.attr("width");
|
|
const height = 500; // Fixed size. Was +svg.attr("height");
|
|
const radius = 200;
|
|
const center = { x: (width / 2) + offset.x, y: (height / 2) + offset.y};
|
|
|
|
let textcolour = colour;
|
|
let polycolour = colour;
|
|
let opacity = 0.3;
|
|
|
|
if (rag !== "")
|
|
{
|
|
polycolour = rag;
|
|
textcolour = "black";
|
|
//console.log("rag: "+rag);
|
|
opacity = 0.6;
|
|
}
|
|
|
|
|
|
// Define axes with A at the top (rotate -90 degrees)
|
|
const axes = [
|
|
{ name: "Feature A", angle: 0 },
|
|
{ name: "Feature B", angle: 120 },
|
|
{ name: "Feature C", angle: 240 }
|
|
];
|
|
axes.forEach(d => d.angle = (d.angle - 90) * Math.PI / 180);
|
|
|
|
// Sample data: [0-1] scale
|
|
// const values = [0.8, 0.1, 0.76]; Now passed in function args
|
|
|
|
// Don't draw if class "no-axis"
|
|
if (!svg.classed("no-axis")) {
|
|
// Draw axis lines
|
|
svg.append("g")
|
|
.attr("class", "axis")
|
|
.selectAll("line")
|
|
.data(axes)
|
|
.join("line")
|
|
.attr("x1", center.x)
|
|
.attr("y1", center.y)
|
|
.attr("x2", d => center.x + radius * Math.cos(d.angle))
|
|
.attr("y2", d => center.y + radius * Math.sin(d.angle));
|
|
|
|
}
|
|
|
|
|
|
// Draw grid rings (polygons)
|
|
const levels = 7;
|
|
for (let i = 1; i <= levels; i++) {
|
|
const r = (radius / levels) * i;
|
|
const points = axes.map(d => {
|
|
return [
|
|
center.x + r * Math.cos(d.angle),
|
|
center.y + r * Math.sin(d.angle)
|
|
];
|
|
});
|
|
svg.append("polygon")
|
|
.attr("class", "grid")
|
|
.attr("points", points.map(p => p.join(",")).join(" "));
|
|
}
|
|
|
|
|
|
// Draw axis ticks
|
|
let tickLength = 5;
|
|
if (svg.classed("no-axis")) {
|
|
tickLength = 0;
|
|
}
|
|
axes.forEach(axis => {
|
|
for (let i = 1; i <= levels -1 ; i++) { // -1 to remove tick outside outline
|
|
const r = (radius / levels) * i;
|
|
const angle = axis.angle;
|
|
|
|
const x1 = center.x + r * Math.cos(angle);
|
|
const y1 = center.y + r * Math.sin(angle);
|
|
|
|
// Perpendicular tick
|
|
const perpAngle = angle + Math.PI / 2;
|
|
const x2 = x1 + tickLength * Math.cos(perpAngle);
|
|
const y2 = y1 + tickLength * Math.sin(perpAngle);
|
|
|
|
svg.append("line")
|
|
.attr("x1", x1)
|
|
.attr("y1", y1)
|
|
.attr("x2", x2)
|
|
.attr("y2", y2)
|
|
.attr("class", "tick");
|
|
}
|
|
});
|
|
|
|
// Convert values to coordinates
|
|
const valuePoints = axes.map((d, i) => {
|
|
const r = radius * values[i];
|
|
return [
|
|
center.x + r * Math.cos(d.angle),
|
|
center.y + r * Math.sin(d.angle)
|
|
];
|
|
});
|
|
|
|
// Draw data area
|
|
svg.append("polygon")
|
|
.attr("class", "area")
|
|
.attr("points", valuePoints.map(p => p.join(",")).join(" "))
|
|
.attr("fill", polycolour) // #d76a23 with 30% opacity (translucent)
|
|
.attr("fill-opacity", opacity)
|
|
.attr("stroke", polycolour) // Outer line color
|
|
.attr("stroke-width", 1); // Thin outer line
|
|
|
|
// Draw outer triangle border (goes to edge of chart)
|
|
const outerPoints = axes.map(d => [
|
|
center.x + radius * Math.cos(d.angle),
|
|
center.y + radius * Math.sin(d.angle)
|
|
]);
|
|
|
|
// Draw border separately (on top)
|
|
svg.append("polygon")
|
|
.attr("points", outerPoints.map(p => p.join(",")).join(" "))
|
|
.attr("fill","none")
|
|
.attr("stroke", colour) // Outer line color
|
|
.attr("stroke-width", 5); // Thicker outer line
|
|
|
|
// Put label in bottom centre of triangle
|
|
const bottomLeft = outerPoints[1];
|
|
const bottomRight = outerPoints[2];
|
|
|
|
const midX = (bottomLeft[0] + bottomRight[0]) / 2;
|
|
const midY = (bottomLeft[1] + bottomRight[1]) / 2;
|
|
|
|
//Don't draw the label for 'no-text' class
|
|
if (!svg.classed("no-text")){
|
|
svg.append("text")
|
|
.attr("x", midX)
|
|
.attr("y", midY - 10) // Alter for correct height
|
|
.attr("text-anchor", "middle")
|
|
.attr("fill", textcolour) /// My Colour
|
|
.attr("font-size", "22px")
|
|
.attr("font-weight", "bold")
|
|
.text(`${text}`);
|
|
}
|
|
|
|
// And now a central number
|
|
let bigy = midY-45;
|
|
let bigfont = "36px";
|
|
if (svg.classed("big-number")){
|
|
bigy = midY-85;
|
|
bigfont = "70px";
|
|
}
|
|
svg.append("text")
|
|
.attr("x", midX)
|
|
.attr("y", bigy)
|
|
.attr("text-anchor", "middle")
|
|
.attr("fill", "black") // 🎨 customize color
|
|
.attr("font-size", bigfont)
|
|
.attr("font-weight", "bold")
|
|
.attr("font-family", 'sans-serif, Consolas, "Lucida Console", Menlo, "DejaVu Sans Mono", monospace')
|
|
.text(`${mean}`);
|
|
|
|
// Add axis labels
|
|
/*
|
|
svg.selectAll(".label")
|
|
.data(axes)
|
|
.join("text")
|
|
.attr("class", "label")
|
|
.attr("x", d => center.x + (radius + 20) * Math.cos(d.angle))
|
|
.attr("y", (d,i) => {
|
|
if (i === 0) { //adjust for 1st access, rather than using d.name
|
|
return center.y + (radius + 20) * Math.sin(d.angle)
|
|
} else
|
|
return center.y + (radius + 20 + 30) * Math.sin(d.angle)
|
|
})
|
|
.text(d => d.name);
|
|
*/
|
|
|
|
|
|
}
|