In this article, we will learn how we can create our custom piechart using SVG and javascript, which is totally vector, it allows high-quality graphics that scale without losing resolutions, we have to enter the values in pie charts and on page load the library will create a beautiful pie chart on the page with these different values and angles. which is very similar to the amchart , the amchart is a paid version.
Demo Javascript Pie Charts
Structure of a Pie Chart
A pie chart is composed of slices that represent different proportions of a whole. Each slice’s angle corresponds to its percentage of the total data set. The basic components of a pie chart include:
- Data Values: The numerical values that will be represented as slices.
- Angles: Each slice’s angle is determined by its value relative to the total sum of all values.
- Colors: Each slice can have a distinct color for better visual differentiation.
Calculating Angles
To create a pie chart, you need to calculate the angle for each slice based on its value. The formula for calculating the angle is:
Angle=(ValueTotal)×360Angle=(TotalValue​)×360
Where:
- Value is the data point you want to represent.
- Total is the sum of all data points.
Implementing a Pie Chart with SVG
Here’s a step-by-step guide to creating a simple pie chart using SVG and JavaScript.
Step 1: Set Up Your HTML
<div class="svg-container">
<!--<svg id="pie-chart" viewBox="0 0 300 150">
<defs>
<marker id="arrowhead" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<circle cx="3" cy="3" r="3" fill="#000"/>
</marker>
</defs>
</svg>-->
</div>
Step 2: Create Your JavaScript File
Next, create a pieChart.js
file where you will write the JavaScript code to generate the pie chart.
document.addEventListener("DOMContentLoaded", () => {
const svg = document.getElementById("pie-chart");
const radius = 100; // Radius of the semi-circle
const cx = 150; // Center x of the semi-circle
const cy = 130; // Center y of the semi-circle
const startAngle = -180; // Start angle in degrees
const endAngle = 0; // End angle in degrees
// Combined array for colors, percentages, labels, and font sizes
const data = [
{
percentage: 20,
color: "#4be3c5",
internalLabel: "20%",
internalFontSize: "7px",
externalLabel: "Ecosystem Incentives (Locked) 20,000,000",
externalFontSize: "3.5px",
},
{
percentage: 10,
color: "#4dd0b6",
internalLabel: "10%",
internalFontSize: "6px",
externalLabel: "Foundation Reserve (Locked) 10,000,000",
externalFontSize: "3.5px",
},
{
percentage: 5,
color: "#dcfbf5",
internalLabel: "5%",
internalFontSize: "5px",
externalLabel: "Ecosystem Growth(Locked) 5,000,000",
externalFontSize: "3.5px",
},
{
percentage: 55,
color: "#1fedc4",
internalLabel: "55%",
internalFontSize: "16px",
externalLabel: "VELY Circulating Supply 55,000,000",
externalFontSize: "3.5px",
},
{
percentage: 10,
color: "#c0fff2",
internalLabel: "10%",
internalFontSize: "7px",
externalLabel: "Community Pool (Locked) 10,000,000",
externalFontSize: "3.5px",
},
];
const total = data.reduce((a, b) => a + b.percentage, 0);
let currentAngle = startAngle;
// Create paths, lines, and text elements
const paths = [];
const lines = [];
const internalTexts = [];
const externalTexts = [];
function splitText(text, maxWidth) {
const words = text.split(' ');
let line = '';
let lines = [];
words.forEach(word => {
const testLine = line + word + ' ';
const testText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
testText.setAttribute('class', 'label-text');
testText.style.fontSize = '14px';
testText.textContent = testLine;
svg.appendChild(testText);
const { width } = testText.getBBox();
svg.removeChild(testText);
if (width > maxWidth) {
lines.push(line.trim());
line = word + ' ';
} else {
line = testLine;
}
});
lines.push(line.trim());
return lines;
}
const maxWidth = 100; // Max width for line wrapping
data.forEach((item, index) => {
const angle = (item.percentage / total) * (endAngle - startAngle);
const endAngleRad = (currentAngle + angle) * (Math.PI / 180);
const startAngleRad = currentAngle * (Math.PI / 180);
const x1 = cx + radius * Math.cos(startAngleRad);
const y1 = cy + radius * Math.sin(startAngleRad);
const x2 = cx + radius * Math.cos(endAngleRad);
const y2 = cy + radius * Math.sin(endAngleRad);
const largeArcFlag = angle > 180 ? 1 : 0;
const pathData = `
M ${cx} ${cy}
L ${x1} ${y1}
A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}
Z
`;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData.trim());
path.setAttribute('fill', item.color);
path.setAttribute('class', 'slice'); // Add class for animation
svg.appendChild(path);
// Create internal text element for label inside the slice
const internalText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
internalText.setAttribute('class', 'label-text');
internalText.style.fontSize = item.internalFontSize;
internalText.textContent = item.internalLabel;
svg.appendChild(internalText);
// Create line element for external label
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('class', 'label-line');
svg.appendChild(line);
// Create external label text elements
const externalLines = splitText(item.externalLabel, maxWidth);
const externalTextGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
svg.appendChild(externalTextGroup);
externalLines.forEach((textLine, i) => {
const externalText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
externalText.setAttribute('class', 'label-text');
externalText.style.fontSize = item.externalFontSize;
externalText.textContent = textLine;
externalTextGroup.appendChild(externalText);
});
paths.push({
path,
startAngle: currentAngle,
endAngle: currentAngle + angle,
index
});
lines.push({
line,
startAngle: currentAngle,
endAngle: currentAngle + angle,
index
});
internalTexts.push({
internalText,
percentage: item.percentage,
startAngle: currentAngle,
endAngle: currentAngle + angle
});
externalTexts.push({
externalTextGroup,
percentage: item.percentage,
startAngle: currentAngle,
endAngle: currentAngle + angle,
label: item.externalLabel
});
currentAngle += angle;
});
const animationDuration = 500; // Duration of animation in milliseconds
const animationDelay = 200; // Delay between each slice animation in milliseconds
let startTime = performance.now();
function animate(time) {
const elapsed = time - startTime;
const progress = Math.min(elapsed / animationDuration, 1);
paths.forEach(({ path, startAngle, endAngle, index }) => {
const currentProgress = progress * (endAngle - startAngle);
const currentEndAngle = startAngle + currentProgress;
const endAngleRad = currentEndAngle * (Math.PI / 180);
const startAngleRad = startAngle * (Math.PI / 180);
const x1 = cx + radius * Math.cos(startAngleRad);
const y1 = cy + radius * Math.sin(startAngleRad);
const x2 = cx + radius * Math.cos(endAngleRad);
const y2 = cy + radius * Math.sin(endAngleRad);
const largeArcFlag = currentProgress > 180 ? 1 : 0;
const pathData = `
M ${cx} ${cy}
L ${x1} ${y1}
A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}
Z
`;
path.setAttribute('d', pathData.trim());
path.style.opacity = 1;
// Update internal text inside the slice
const midAngle = startAngle + currentProgress / 2;
const midAngleRad = midAngle * (Math.PI / 180);
internalTexts[index].internalText.setAttribute('x', cx + (radius / 2) * Math.cos(midAngleRad));
internalTexts[index].internalText.setAttribute('y', cy + (radius / 2) * Math.sin(midAngleRad));
// Update line element
const midAngleForLine = startAngle + (currentProgress / 2);
const midAngleRadForLine = midAngleForLine * (Math.PI / 180);
const lineStartX = cx + (radius) * Math.cos(midAngleRadForLine);
const lineStartY = cy + (radius) * Math.sin(midAngleRadForLine);
// Shorten the line to bring the label closer
const lineEndX = cx + (radius * 1.25) * Math.cos(midAngleRadForLine);
const lineEndY = cy + (radius * 1.25) * Math.sin(midAngleRadForLine);
lines[index].line.setAttribute('x1', lineStartX);
lines[index].line.setAttribute('y1', lineStartY);
lines[index].line.setAttribute('x2', lineEndX);
lines[index].line.setAttribute('y2', lineEndY);
// Update external label
externalTexts[index].externalTextGroup.querySelectorAll('text').forEach((textElement, i) => {
const labelX = lineEndX + (lineEndX > lineStartX ? 10 : -10);
const labelY = lineEndY + (i * 5); // Vertical spacing for multi-line labels
textElement.setAttribute('x', labelX);
textElement.setAttribute('y', labelY - 5); // Small vertical offset to move text closer to line head
});
});
// Request animation frame for the next frame
if (progress < 1) {
requestAnimationFrame(animate);
}
}
// Start animation for each slice with a delay
let delay = 1000;
data.forEach((item, index) => {
setTimeout(() => {
startTime = performance.now();
requestAnimationFrame(animate);
}, 0);
delay += animationDelay;
});
});
Step 3: Set Up Your Style
<style>
svg {
width: 100%; /* SVG scales to the width of the container */
height: auto; /* Height scales proportionally */
display: block; /* Removes extra space below SVG */
background: none; /* Optional background color */
}
.label-line {
stroke: #545454;
stroke-width: .2; /* Thin line */
marker-end: url(#arrowhead); /* Circle marker */
}
.label-text {
fill: #000;
text-anchor: middle;
}/* Responsive Styles */
</style>
Settings
const data = [
{
percentage: 20,
color: "#4be3c5",
internalLabel: "20%",
internalFontSize: "7px",
externalLabel: "Ecosystem Incentives (Locked) 20,000,000",
externalFontSize: "3.5px",
},
{
percentage: 10,
color: "#4dd0b6",
internalLabel: "10%",
internalFontSize: "6px",
externalLabel: "Foundation Reserve (Locked) 10,000,000",
externalFontSize: "3.5px",
},
{
percentage: 5,
color: "#dcfbf5",
internalLabel: "5%",
internalFontSize: "5px",
externalLabel: "Ecosystem Growth(Locked) 5,000,000",
externalFontSize: "3.5px",
},
{
percentage: 55,
color: "#1fedc4",
internalLabel: "55%",
internalFontSize: "16px",
externalLabel: "VELY Circulating Supply 55,000,000",
externalFontSize: "3.5px",
},
{
percentage: 10,
color: "#c0fff2",
internalLabel: "10%",
internalFontSize: "7px",
externalLabel: "Community Pool (Locked) 10,000,000",
externalFontSize: "3.5px",
},
];
To set font size of each label and the percentage of each slice you can edit the above settings , this settings are already available in the library above
percentage: 5 : slice percentage
color: “#dcfbf5” : slice color
internalLabel: “5%” : text inside the slice
internalFontSize: “5px” : Font size inside the slice
externalLabel: “Text outside the slice”,
externalFontSize: “3.5px” : font size out side the slice
Conclusion
Creating a pie chart with SVG in JavaScript provides flexibility and control over your visualizations. By understanding how to manipulate SVG elements and calculate angles based on data values, you can create interactive and visually appealing charts tailored to your needs. This approach not only enhances user experience but also makes your web applications more engaging and informative.