arc-pie-chart
Version:
pie chart that can be divided into several steps
246 lines (222 loc) • 6.23 kB
text/typescript
import {
makeTextTagProps,
CoordinateType,
DataType,
GetCoordinateType,
HandleHoverEvent,
MakePathTagType,
IterDataType,
} from "./type";
// 참고 ; http://www.gisdeveloper.co.kr/?p=4705
function getCoordinate({ cx, cy, radius, degree }: GetCoordinateType) {
degree = (degree * Math.PI) / 180 - (90 * Math.PI) / 180;
return {
x: cx + radius * Math.cos(degree),
y: cy + radius * Math.sin(degree),
};
}
function toPieChartItemPath(
innerDistanceFromCenter: number,
outerDistanceFromCenter: number,
startDegree: number,
endDegree: number,
halfWidth: number
): { d: string; textCoordinate: CoordinateType } {
const cx = halfWidth;
const cy = halfWidth;
const startInner = getCoordinate({
cx,
cy,
radius: innerDistanceFromCenter,
degree: startDegree,
});
const endInner = getCoordinate({
cx,
cy,
radius: innerDistanceFromCenter,
degree: endDegree,
});
const startOuter = getCoordinate({
cx,
cy,
radius: outerDistanceFromCenter,
degree: startDegree,
});
const endOuter = getCoordinate({
cx,
cy,
radius: outerDistanceFromCenter,
degree: endDegree,
});
const arcSweep = endDegree - startDegree <= 180 ? "0" : "1";
const d = [
["M", startInner.x, startInner.y].join(" "),
["L", startOuter.x, startOuter.y].join(" "),
[
"A",
outerDistanceFromCenter,
outerDistanceFromCenter,
0,
arcSweep,
1,
endOuter.x,
endOuter.y,
].join(" "),
["L", endInner.x, endInner.y].join(" "),
[
"A",
innerDistanceFromCenter,
innerDistanceFromCenter,
0,
arcSweep,
0,
startInner.x,
startInner.y,
].join(" "),
"z",
].join(" ");
const textCoordinate = getCoordinate({
cx,
cy,
radius: (innerDistanceFromCenter + outerDistanceFromCenter) / 2,
degree: (startDegree + endDegree) / 2,
});
return { d, textCoordinate };
}
function makePathTag({ fill, d, className }: MakePathTagType) {
if (className) {
return `<path fill=${fill} d="${d}" class="${className}"></path>`;
}
return `<path fill=${fill} d="${d}"></path>`;
}
function makeTextTag({
text,
percentage,
coordinate,
color,
fontSize = 12,
className,
gap = 12,
}: makeTextTagProps) {
return `
<text ${className ? `class="${className}"` : ""} text-anchor="middle" x="${
coordinate.x
}" y="${coordinate.y}" font-size="${fontSize}" fill="${color}">${text}</text>
<text ${className ? `class="${className}"` : ""} text-anchor="middle" x="${
coordinate.x
}" y="${
coordinate.y + gap
}" font-size="${fontSize}" fill="${color}">${percentage}%</text>
`;
}
function iterData({
data,
startDegree,
parentDegree,
svg,
innerDistanceFromCenter,
outerDistanceFromCenter,
halfWidth,
totalDepth,
}: IterDataType) {
let totalDegree = 0;
data.forEach((datum) => {
if (datum.percentage === 0) {
return;
}
const degree = (parentDegree * datum.percentage) / 100;
const { d: pathD, textCoordinate } = toPieChartItemPath(
innerDistanceFromCenter,
outerDistanceFromCenter,
startDegree + totalDegree + 0.3,
startDegree + totalDegree + degree - 0.3,
halfWidth
);
svg.innerHTML +=
makePathTag({
fill: datum.color,
d: pathD,
className: datum.data ? "" : "pie_end",
}) +
makeTextTag({
text: datum.name,
percentage: datum.percentage,
coordinate: textCoordinate,
color: datum.textColor,
});
// halfWidth 250 : pieAreaWidth =
const pieAreaWidth = (13.5 * halfWidth) / 25;
const maxPieWidth = pieAreaWidth / totalDepth;
// halfWidth 250 : gapBetweenInnerOuterPie 10
const gapBetweenInnerOuterPie = Math.min(halfWidth / 25, maxPieWidth / 6.5);
// halfWidth 250 : pieWidth 55
const pieWidth = Math.min(
(5.5 * halfWidth) / 25,
(maxPieWidth * 5.5) / 6.5
);
if (datum.data) {
iterData({
data: datum.data,
startDegree: startDegree + totalDegree + 0.3,
parentDegree: degree,
svg,
innerDistanceFromCenter:
outerDistanceFromCenter + gapBetweenInnerOuterPie,
outerDistanceFromCenter: outerDistanceFromCenter + pieWidth,
halfWidth,
totalDepth,
});
}
totalDegree += degree;
});
}
function handleHoverEvent({ event, svg, halfWidth }: HandleHoverEvent) {
const path = (event.target as HTMLElement).closest("path");
if (path) {
const name = (path.nextSibling?.nextSibling as HTMLElement).innerHTML;
const percentage = Number(
(
path.nextSibling?.nextSibling?.nextSibling?.nextSibling as HTMLElement
).innerHTML.replace("%", "")
);
const oldChild = svg.querySelectorAll(".center_text");
oldChild.forEach((child: any) => svg.removeChild(child));
svg.innerHTML += makeTextTag({
text: name,
percentage,
coordinate: { x: halfWidth, y: halfWidth },
color: "black",
fontSize: (1.8 * halfWidth) / 25,
className: "center_text",
gap: 24,
});
}
}
function makePieChart(data: DataType[], depth: number, width: number = 500) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const halfWidth = width / 2;
svg.setAttribute("width", String(width));
svg.setAttribute("height", String(width));
const circleRadius = (7 * halfWidth) / 25;
const circle = `<circle cx="${halfWidth}" cy="${halfWidth}" r="${circleRadius}" fill="#ddd"></circle>`;
svg.innerHTML = circle;
// halfWidth 250 : innerDistanceFromCenter 85
const innerDistanceFromCenter = (8.5 * halfWidth) / 25; // first pie inner distance from center
// halfWidth 250 : outerDistanceFromCenter 115
const outerDistanceFromCenter = (11.5 * halfWidth) / 25; // first pie outer distance from center
iterData({
data,
startDegree: 0,
parentDegree: 360,
svg,
innerDistanceFromCenter,
outerDistanceFromCenter,
halfWidth,
totalDepth: depth - 1,
});
svg.addEventListener("mouseover", (event) =>
handleHoverEvent({ event, svg, halfWidth })
);
return svg;
}
export default makePieChart;