153 lines
3.4 KiB
HTML
153 lines
3.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>3-Axis Radar Chart with Ticks</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: white;
|
|
}
|
|
|
|
svg {
|
|
background: white;
|
|
display: block;
|
|
margin: auto;
|
|
}
|
|
|
|
.axis line {
|
|
stroke: black;
|
|
stroke-width: 1.5px;
|
|
}
|
|
|
|
line.tick {
|
|
stroke: black;
|
|
stroke-width: 1;
|
|
}
|
|
|
|
polygon.grid {
|
|
fill: none;
|
|
stroke: rgba(200, 200, 200, 0.5); /* light grey with 50% opacity */
|
|
stroke-dasharray: 2,2;
|
|
}
|
|
|
|
.area {
|
|
fill: rgba(70, 130, 180, 0.3); /* steelblue with transparency */
|
|
stroke: rgba(70, 130, 180, 0.6);
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.label {
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
text-anchor: middle;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<svg width="500" height="500"></svg>
|
|
|
|
<script>
|
|
const svg = d3.select("svg");
|
|
const width = +svg.attr("width");
|
|
const height = +svg.attr("height");
|
|
const radius = 200;
|
|
const center = { x: width / 2, y: height / 2 };
|
|
|
|
// 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.4, 0.6];
|
|
|
|
// 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 = 5;
|
|
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
|
|
const tickLength = 5;
|
|
axes.forEach(axis => {
|
|
for (let i = 1; i <= levels; i++) {
|
|
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(" "))
|
|
|
|
// 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);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|