Reputation: 1641
I'm unable to figure out how to evenly space each slice on this donut chart.
import { storyblokEditable } from "@storyblok/react";
import { type BlokType } from "../../../types/storyblok";
interface AnimatedPieBlokProps {
blok: BlokType;
}
function AnimatedPieBlok({ blok }: AnimatedPieBlokProps) {
const { pieSlices } = blok;
const totalSlices = pieSlices.length;
const calculatedSlices = pieSlices.map((slice: BlokType) => ({
...slice,
value: 360 / totalSlices
}));
const total = calculatedSlices.reduce((sum: number, item: BlokType) => sum + item.value, 0);
const spacing = 1; // Set the spacing between slices
const sliceAngle = (360 - (spacing * totalSlices)) / totalSlices;
let cumulativeValue = 0;
return (
<div {...storyblokEditable(blok)} key={blok._uid} className="flex flex-row justify-center">
<svg width="450" height="450" viewBox="0 0 200 200" className="flex-1">
{calculatedSlices.map((item: BlokType, index: number) => {
const words = item.title.split(' ');
const firstWord = words[0];
const secondWord = words[1];
const startAngle = cumulativeValue * (Math.PI / 180);
const endAngle = (cumulativeValue + sliceAngle) * (Math.PI / 180);
// Adjust the angles to create spacing
const adjustedStartAngle = startAngle + (spacing / 100); // Adjust for spacing
const adjustedEndAngle = endAngle - (spacing / 100); // Adjust for spacing
const x1 = 100 + 100 * Math.cos(startAngle);
const y1 = 100 + 100 * Math.sin(startAngle);
const x2 = 100 + 100 * Math.cos(endAngle);
const y2 = 100 + 100 * Math.sin(endAngle);
// Calculate the midpoint for the label and image
const midAngle = (adjustedStartAngle + adjustedEndAngle) / 2;
const labelX = 100 + 65 * Math.cos(midAngle); // Adjust the radius for label position
const labelY = 100 + 65 * Math.sin(midAngle); // Adjust the radius for label position
const imageX = 100 + 40 * Math.cos(midAngle); // Adjust the radius for image position
const imageY = 100 + 40 * Math.sin(midAngle); // Adjust the radius for image position
cumulativeValue += sliceAngle + spacing;
return (
<g
key={index}
className="transition-transform duration-300 ease-in-out hover:scale-105 relative hover:z-10"
>
<path
d={`M 100,100 L ${x1},${y1} A 100,100 0 ${item.value / total > 0.5 ? 1 : 0} 1 ${x2},${y2} Z`}
fill={item.backgroundColor}
/>
<image
href={item.image.filename} // Use the imageUrl from the dataset
x={imageX - 15} // Center the image (adjust as needed)
y={imageY - 15} // Center the image (adjust as needed)
width="30" // Set the width of the image
height="30" // Set the height of the image
/>
<text
x={labelX}
y={labelY}
fill="white"
fontSize="7px"
textAnchor="middle"
alignmentBaseline="middle"
>
<tspan>{firstWord}</tspan>
{secondWord ? <tspan x={labelX} dy="1.2em">{secondWord}</tspan> : null}
</text>
</g>
);
})}
<circle cx="100" cy="100" r="30" fill="white" />
<circle cx="100" cy="100" r="29" fill="none" stroke="#140965" strokeWidth="3" />
</svg>
</div>
);
}
export default AnimatedPieBlok;
I've tried using other AI tools along with other stackoverflow posts to try and resolve the issue but haven't been able to get it to work properly.
Upvotes: 0
Views: 55
Reputation: 21173
pathLength
, and then you do not need any Math
Wrapped in a native JavaScript Web Component (JSWC) for ease of use
with shadowDOM so <style>
is scoped and does not leak out to the rest of the DOM page:
<pie-chart gap="11"></pie-chart>
<pie-chart slices="8" gap="8"></pie-chart>
<pie-chart stroke="green"></pie-chart>
<script>
customElements.define( "pie-chart", class extends HTMLElement {
connectedCallback() {
const slices = ~~(this.getAttribute("slices") || 6);
const stroke = this.getAttribute("stroke")||"blue";
const strokeWidth = 20;
const gap = ~~(this.getAttribute("gap") || 12);
const sliceAngle = 360 / slices - gap;
this.attachShadow({ mode: "open" }).innerHTML = `
<style>
:host { display: inline-block; width: 160px; background:grey }
path { fill: none; stroke: ${stroke}; stroke-width: ${strokeWidth};
transform:rotate(var(--rotation)); transform-origin: 50% 50%;
stroke-dashoffset:var(--offset); stroke-dasharray:var(--dash) 360 }
</style>
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" stroke-width="${strokeWidth}" stroke="white" fill="none"></circle>
</svg>`;
const paths = Array.from({ length: slices }).map((_, idx) => {
const slice = document.createElementNS("http://www.w3.org/2000/svg", "path");
slice.setAttribute("pathLength", 360);
slice.setAttribute("d", "M50,10 A40,40 0 1,1 10,50 A40,40 0 1,1 50,10");
slice.style.setProperty("--dash", sliceAngle);
slice.style.setProperty("--offset", gap);
slice.style.setProperty("--rotation", `${idx * (sliceAngle + gap)}deg`);
return slice;
});
this.shadowRoot.querySelector("svg").append(...paths)
}});
</script>
<pie-chart gap="11"></pie-chart>
<pie-chart slices="8" gap="8"></pie-chart>
<pie-chart stroke="green"></pie-chart>
Mix of CSS properties and attributes just to show what is possible, you could do everything with <path>
attributes
Use https://yqnn.github.io/svg-path-editor if you need another d-path
Still a Web Component with shadowDOM so multiple instances can be used
<script>
customElements.define(
"pie-chart",
class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // wait till innerHTML is parsed
const slices = this.innerHTML.split(",");
const angle = 360 / slices.length;
this.attachShadow({ mode: "open" }).innerHTML =
`<style>:host { display:inline-block; width:160px; background:grey }</style>
<svg viewBox="0 0 100 100">
<defs>
<mask id="m">
<circle r="50" cx="50" cy="50" fill="white" />
<circle r="15" cx="50" cy="50" fill="black" />
</mask>
</defs>
<g mask="url(#m)" font-family="sans-serif" font-size="7" dominant-baseline="middle" text-anchor="middle">
<g transform="translate(50 50)" fill="none" stroke-width="100">` +
slices.map((slice,idx) => {
let [color,label] = slice.split(":");
return `<g transform="rotate(${idx * angle}) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="${color}" stroke-dasharray="${angle} 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-${idx*angle})"
fill="black">${label}</text>
</g>`}).join("") + `</g></g></svg>`;
})
}})
</script>
<pie-chart>orange:one,green:two,tomato:three,lightblue:four,red:five,purple:six,yellow:seven,cyan:eight</pie-chart>
Upvotes: 0
Reputation: 13090
You can use the stroke-dasharray combined with pathLength to create slices, and then move the slices a bit (for the two examples, two different values: translate(.5 0)
and translate(1 0)
). This will make the gap between the slices even. To make the inner and outer edge of the slices look nice, mask off all the slices with a mask that has circles in white and black.
My examples are static -- you can probably figure out the dynamic version yourself.
<svg viewBox="0 0 100 100" width="300">
<defs>
<mask id="m1">
<circle r="50" cx="50" cy="50" fill="white" />
<circle r="15" cx="50" cy="50" fill="black" />
</mask>
</defs>
<g mask="url(#m1)" font-family="sans-serif" font-size="6"
dominant-baseline="middle" text-anchor="middle">
<g transform="translate(50 50)" fill="none" stroke-width="100">
<g transform="rotate(0) translate(.5 0) rotate(-60)">
<circle r="50" stroke="red" stroke-dasharray="120 360" pathLength="360" />
<text transform="rotate(60) translate(30 0)"
fill="black">Lorem</text>
</g>
<g transform="rotate(120) translate(.5 0) rotate(-60)">
<circle r="50" stroke="orange" stroke-dasharray="120 360" pathLength="360" />
<text transform="rotate(60) translate(30 0) rotate(-120)"
fill="black">Lorem</text>
</g>
<g transform="rotate(240) translate(.5 0) rotate(-60)">
<circle r="50" stroke="green" stroke-dasharray="120 360" pathLength="360" />
<text transform="rotate(60) translate(30 0) rotate(-240)"
fill="black">Lorem</text>
</g>
</g>
</g>
</svg>
<svg viewBox="0 0 100 100" width="300">
<defs>
<mask id="m2">
<circle r="50" cx="50" cy="50" fill="white" />
<circle r="15" cx="50" cy="50" fill="black" />
</mask>
</defs>
<g mask="url(#m2)" font-family="sans-serif" font-size="5"
dominant-baseline="middle" text-anchor="middle">
<g transform="translate(50 50)" fill="none" stroke-width="100">
<g transform="rotate(0) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="red" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0)"
fill="black">Lorem</text>
</g>
<g transform="rotate(45) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="orange" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-45)"
fill="black">Lorem</text>
</g>
<g transform="rotate(90) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="green" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-90)"
fill="black">Lorem</text>
</g>
<g transform="rotate(135) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="tomato" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-135)"
fill="black">Lorem</text>
</g>
<g transform="rotate(180) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="lightblue" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-180)"
fill="black">Lorem</text>
</g>
<g transform="rotate(225) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="red" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-225)"
fill="black">Lorem</text>
</g>
<g transform="rotate(270) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="orange" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-270)"
fill="black">Lorem</text>
</g>
<g transform="rotate(315) translate(1 0) rotate(-22.5)">
<circle r="50" stroke="green" stroke-dasharray="45 360" pathLength="360" />
<text transform="rotate(22.5) translate(30 0) rotate(-315)"
fill="black">Lorem</text>
</g>
</g>
</g>
</svg>
Upvotes: 0