Commit 1af0a999 authored by Almouhannad Hafez's avatar Almouhannad Hafez

Add pie chart

parent d7250a02
import * as d3 from 'd3';
import { ChartConfiguration } from "../chart-base/chart-configuration";
import { PieChart } from "../charts/pie-chart";
export class PieChartHelper {
private container: any;
private containerId: string = 'pie-chart-container';
private svgId: string = 'pie-chart';
private selector: any;
private config: ChartConfiguration = new ChartConfiguration(`#${this.svgId}`);
private chart: PieChart;
private data: any[] = [];
public setData(data: any) {
this.data = data;
}
public appendChart() {
// Add div container
this.container = d3.select('#medalDistributionByGenderChart')
.append('div')
.attr('class', 'container')
.attr('style', 'width: fit-content;')
.attr('id', `${this.containerId}`);
// Add year selector
this.selector = this.container.append('div')
.attr('id', 'pie-chart-selector')
.attr('class', 'text-center');
this.selector.append('label')
.attr('for', 'yearSelector')
.attr('class', 'form-label')
.text('Select year:');
this.selector.append('select')
.attr('class', 'form-select text-center')
.append('option').attr('value', 'all').text('All years');
const selectField = this.selector.select('select');
const years = Array.from(new Set(this.data.map(d => d.Year))); // Get unique years
years.forEach(year => {
selectField.append("option")
.attr("value", year)
.text(year);
});
// add select event handler
const visHelper = this;
selectField.on("change", function (event: any) {
const selectedYear = d3.select(event.target).property("value");
visHelper.updateChart(selectedYear);
});
// add svg for chart
this.container
.append('svg')
.attr('id', `${this.svgId}`);
// init chart
this.chart = new PieChart(this.config);
this.updateChart("all");
}
private updateChart(selectedYear: string) {
this.chart.data = this.processData(selectedYear);
this.chart.updateVis();
}
private processData(selectedYear: string) {
const filteredData = selectedYear !== 'all' ? this.data.filter(d => d.Year === selectedYear) : this.data;
const genderCountMap: { [key: string]: number } = {};
filteredData.forEach(d => {
const gender = d.Gender;
if (genderCountMap[gender]) {
genderCountMap[gender]++;
} else {
genderCountMap[gender] = 1;
}
});
const result: any[] = Object.keys(genderCountMap).map(key => ({
key: key,
value: genderCountMap[key]
}));
return result;
}
}
import * as d3 from 'd3';
import { Chart } from '../chart-base/chart';
import { ChartConfiguration } from '../chart-base/chart-configuration';
interface pieChartDatatype {
key: string;
value: number;
}
export class PieChart extends Chart {
private pie: any;
private arc: any;
private color: any;
tooltip: d3.Selection<HTMLDivElement, unknown, HTMLElement, any>;
constructor(_config: ChartConfiguration, _data?: pieChartDatatype[]) {
super(_config, _data);
this.initVis();
}
protected getDefaultMargins() {
return { top: 20, right: 80, bottom: 20, left: 20 };
}
protected getDefaultContainerSize() {
return { width: 400, height: 400 };
}
protected initVis() {
const vis = this;
// Define the pie layout
vis.pie = d3.pie<any>()
.value(d => d.value)
.sort(null);
// Define the arc
vis.arc = d3.arc()
.innerRadius(0)
.outerRadius(Math.min(vis.width, vis.height) / 2 - 1);
// Define color scale for gender
// Can use scaleOrdinal10 for generalization and usage of keys
// instead of hard coded keys
vis.color = d3.scaleOrdinal()
.domain(['Men', 'Women'])
.range(['#01a7c5', '#f284a5']);
// Add tooltip
this.tooltip = d3.select('body').append('div')
.attr('id', 'pie-chart-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('background', 'white')
.style('border', '1px solid black')
.style('padding', '5px')
.style('pointer-events', 'none');
}
public updateVis() {
const vis = this;
vis.renderVis();
}
protected renderVis() {
const vis = this;
// Clear previous
vis.chart.selectAll("*").remove();
// chart gropu
const g = vis.chart.append("g")
.attr("transform", `translate(${vis.width / 2}, ${vis.height / 2})`);
// Create args
const pieData = vis.pie(this.data);
// Total omong all values
const total = d3.sum(vis.data, (data: any) => data.value);
// Bind data to arcs
const arcs = g.selectAll(".arc")
.data(pieData, (d: any) => d.data.key);
// Enter section (new arcs)
const arcsEnter = arcs.enter().append("g")
.attr("class", "arc");
arcsEnter.append("path")
.attr("d", vis.arc)
.attr("fill", (d: any) => vis.color(d.data.key))
.attr("stroke", "#fff")
.attr("stroke-width", 0)
.attr("opacity", 0) // To be changed in transition
.transition()
.duration(500)
.attr("opacity", 1);
// Add percentage text to each arc
arcsEnter.append("text")
.attr("transform", (d: any) => `translate(${vis.arc.centroid(d)})`) // Position text at the centroid of the arc
.attr("dy", ".35em") // Adjust vertical alignment
.attr("text-anchor", "middle") // Center the text
.text((d: any) => {
const percentage = ((d.data.value / total) * 100).toFixed(2);
return `${percentage}%`; // Display percentage
})
.style("fill", "#fff")
.style("user-select", 'none')
.style('font-weight', 'bold');
// Add mouse event handlers for arcs
arcsEnter.select("path")
.on("mouseover", function (event: any, d: any) {
// Highlight the hovered arc
d3.select(event.currentTarget).attr("opacity", 1); // Set hovered arc to full opacity
// minimum opacity of other arcs
arcsEnter.selectAll("path").filter(function (arcData: any) {
return arcData.data.key !== d.data.key;
}).attr("opacity", 0.4);
// high stroke to distinguish between args when select
arcsEnter.selectAll("path").filter(function (arcData: any) {
return arcData.data.key === d.data.key;
}).attr("stroke-width", 6);
vis.tooltip
.style('display', 'block')
.style('opacity', .9);
vis.tooltip.html(`${d.data.key}<br>Total Medals: ${d.data.value}`)
.style('left', (event.pageX - 20) + 'px')
.style('top', (event.pageY - 85) + 'px');
})
.on("mousemove", function (event: any) {
vis.tooltip.style('left', (event.pageX - 20) + 'px')
.style('top', (event.pageY - 85) + 'px');
})
.on("mouseout", function () {
// Reset opacity of all arcs
arcsEnter.selectAll("path").attr("opacity", 1).attr('stroke-width', 0); // Reset all arcs to full opacity
vis.tooltip.style('display', 'none');
});
// Update section
arcs.select("path")
.transition() // smooth updates
.duration(500)
.attr("d", vis.arc)
.attr("fill", (d: any) => vis.color(d.data.key))
.attr("stroke", "#fff")
.attr("stroke-width", 2)
// Update text for existing arcs
arcs.select("text")
.transition() // Transition for text change
.duration(500)
.attr("transform", (d: any) => `translate(${vis.arc.centroid(d)})`) // Update position
.text((d: any) => {
const percentage = ((d.data.value / total) * 100).toFixed(2);
return `${percentage}%`; // Update percentage text
});
// Exit
arcs.exit().select("path")
.transition()
.duration(500)
.attr("opacity", 0) // Fade out effects
.remove();
// Legend
// this is in render for cases when keys changes when data changes (general case)
const legend = vis.chart.append("g")
.attr("transform", `translate(${vis.width}, ${vis.height - 60})`);
// Find all keys
const legendData = this.data.map(d => d.key);
// Bind data to legend items
const legendItems = legend.selectAll(".legend-item")
.data(legendData);
// Enter: create new legend items
const legendEnter = legendItems.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (_: any, i: any) => `translate(0, ${i * 20})`);
legendEnter.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.attr("fill", (d: any) => vis.color(d))
.attr("stroke", "#fff");
legendEnter.append("text")
.attr("x", 25)
.attr("y", 9)
.attr("dy", ".35em")
.text((d: any) => d)
.style("user-select", "none");
// Update
const legendUpdate = legendItems.merge(legendEnter);
legendUpdate.select("rect")
.transition() // Transition for color change
.duration(500)
.attr("fill", (d: any) => vis.color(d));
legendUpdate.select("text")
.transition() // Transition for text change
.duration(500)
.text((d: any) => d);
// Events handlers
legendUpdate.on("mouseover", function (_: any, d: any) {
// Highlight related arc
arcsEnter.selectAll("path").filter(function (arcData: any) {
return arcData.data.key === d;
}).attr("opacity", 1).attr("stroke-width", 6);
// Reduce opacity of other arcs
arcsEnter.selectAll("path").filter(function (arcData: any) {
return arcData.data.key !== d;
}).attr("opacity", 0.4);
})
.on("mouseout", function () {
// Reset opacity of all arcs
arcsEnter.selectAll("path").attr("opacity", 1).attr('stroke-width', 0); // Reset all arcs to full opacity
});
// Exit
legendItems.exit().transition()
.duration(500)
.attr("opacity", 0) // Fade out
.remove(); // Remove after fade out
}
}
\ No newline at end of file
...@@ -3,10 +3,12 @@ import '/src/css/style.css'; ...@@ -3,10 +3,12 @@ import '/src/css/style.css';
import 'bootstrap'; import 'bootstrap';
import { StackedBarChartHelper } from './charts-helpers/stacked-bar-chart-helper'; import { StackedBarChartHelper } from './charts-helpers/stacked-bar-chart-helper';
import { MultiLineChartHelper } from './charts-helpers/multi-line-chart-helper'; import { MultiLineChartHelper } from './charts-helpers/multi-line-chart-helper';
import { PieChartHelper } from './charts-helpers/pie-chart-helper';
const datasetPath = import.meta.env.VITE_DATASET_PATH; const datasetPath = import.meta.env.VITE_DATASET_PATH;
let rawData: any[]; let rawData: any[];
const stackerBarChartHelper = new StackedBarChartHelper(); const stackerBarChartHelper = new StackedBarChartHelper();
const multiLineChartHelper = new MultiLineChartHelper(); const multiLineChartHelper = new MultiLineChartHelper();
const pieChartHelper = new PieChartHelper();
d3.csv(datasetPath).then(data => { d3.csv(datasetPath).then(data => {
rawData = data.filter(d => d.Medal !== ''); rawData = data.filter(d => d.Medal !== '');
...@@ -17,5 +19,8 @@ d3.csv(datasetPath).then(data => { ...@@ -17,5 +19,8 @@ d3.csv(datasetPath).then(data => {
multiLineChartHelper.setData(rawData); multiLineChartHelper.setData(rawData);
multiLineChartHelper.appendChart(); multiLineChartHelper.appendChart();
});
pieChartHelper.setData(rawData);
pieChartHelper.appendChart();
});
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment