Commit 967de113 authored by Almouhannad Hafez's avatar Almouhannad Hafez

Add polar area chart

parent e5e133c4
import * as d3 from 'd3';
import { ChartConfiguration } from "../chart-base/chart-configuration";
import { PolarAreaChart } from '../charts/polar-area-chart';
export class PloarAreaChartHelper {
private container: any;
private containerId: string = 'polar-area-chart-container';
private svgId: string = 'polar-area-chart';
private selector: any;
private config: ChartConfiguration = new ChartConfiguration(`#${this.svgId}`, { width: 500, height: 408 });
private chart: PolarAreaChart;
private data: any[] = [];
public setData(data: any) {
this.data = data;
}
public appendChart() {
// Add div container
this.container = d3.select('#medalDistributionBySportChart')
.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', 'polar-area-chart-selector')
.attr('class', 'text-center mt-4');
this.selector.append('label')
.attr('for', 'yearSelector')
.attr('class', 'form-label')
.style('font-weight', 'bold')
.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 PolarAreaChart(this.config);
this.updateChart("all");
}
private updateChart(selectedYear: string) {
// this.chart.data = this.processData(selectedYear);
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 sportCountMap: { [key: string]: number } = {};
filteredData.forEach(d => {
const sport = d.Sport;
if (sportCountMap[sport]) {
sportCountMap[sport]++;
} else {
sportCountMap[sport] = 1;
}
});
const result: any[] = Object.keys(sportCountMap).map(key => ({
key: key,
value: sportCountMap[key]
}));
return result.sort((a, b) => b.value - a.value).slice(0, 10);
}
}
import * as d3 from 'd3';
import { Chart } from '../chart-base/chart';
import { ChartConfiguration } from '../chart-base/chart-configuration';
interface PolarAreaChartDataType {
key: string;
value: number;
}
export class PolarAreaChart extends Chart {
private pie: any;
private arc: any;
private outerArc: any;
private color: any;
private innerRadius: number;
tooltip: d3.Selection<HTMLDivElement, unknown, HTMLElement, any>;
outerRadius: number;
constructor(_config: ChartConfiguration, _data?: PolarAreaChartDataType[]) {
super(_config, _data);
this.initVis();
}
protected getDefaultMargins() {
return { top: 40, right: 130, bottom: 20, left: 130 };
}
protected getDefaultContainerSize() {
return { width: 500, height: 500 };
}
protected initVis() {
const vis = this;
// Define the pie layout
vis.pie = d3.pie<any>()
.value(d => d.value)
.sort(null);
// Compute radiius
vis.outerRadius = Math.min(vis.width, vis.height) / 2 + 45;
vis.innerRadius = vis.outerRadius * 0.05;
// Arc for the polar area chart slices
vis.arc = d3.arc()
.innerRadius(vis.innerRadius)
.outerRadius((d: any) => {
// Scale the outer radius based on the value
return (d.data.value / d3.max(vis.data, (d: any) => d.value)) * vis.outerRadius;
});
vis.outerArc = d3.arc()
.innerRadius(vis.outerRadius * 1.1) // bigger than outerRadius
.outerRadius(vis.outerRadius * 1.1); // to make labels sit outside
// color scale
vis.color = d3.scaleOrdinal(d3.schemeCategory10);
// tooltip
this.tooltip = d3.select('body').append('div')
.attr('id', 'polar-area-chart-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'white')
.style('border', '1px solid black')
.style('padding', '5px')
.style('pointer-events', 'none');
}
public updateVis() {
this.renderVis();
}
protected renderVis() {
const vis = this;
const totalValues = d3.sum(vis.data, (d: any) => d.value);
//helper function to find the midpoint angle
function midAngle(d: any) {
return d.startAngle + (d.endAngle - d.startAngle) / 2;
}
vis.chart.selectAll("*").remove();
// Create group for the chart
const g = vis.chart.append("g")
.attr("transform", `translate(${vis.width / 2}, ${vis.height / 2})`);
// Create pie data
const pieData = vis.pie(vis.data);
// Bind data to group elements
const arcs = g.selectAll(".arc")
.data(pieData, (d: any) => d.data.key);
// ENTER
const arcsEnter = arcs.enter().append("g")
.attr("class", "arc");
// The polar area chart slices
arcsEnter.append("path")
.attr("fill", (d: any) => vis.color(d.data.key))
.attr("stroke", "#fff")
.attr("stroke-width", 4)
.attr("d", vis.arc)
.attr("opacity", 0)
.transition()
.duration(500)
.attr("opacity", 1);
// Paths to connect slices to labels
arcsEnter.append("path")
.attr("class", "label-line")
.attr("stroke", "#222")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("d", (d: any) => {
const pos = vis.outerArc.centroid(d);
const mid = vis.arc.centroid(d);
// change position based on quarter
pos[0] = (d.data.value / d3.max(vis.data, (d: any) => d.value)) * vis.outerRadius * (midAngle(d) < Math.PI ? 1.2 : -1.2);
return `M${mid[0]},${mid[1]} L${pos[0]},${pos[1]}`;
})
.transition()
.duration(500)
//labels outside the polar area chart
arcsEnter.append("text")
.attr("dy", "0.35em")
.style("font-size", "12px")
.style("font-weight", 'bold')
.attr("text-anchor", (d: any) =>
midAngle(d) < Math.PI ? "start" : "end"
)
.attr("transform", (d: any) => {
const [x, y] = vis.arc.centroid(d);
return `translate(${x},${y})`;
})
.transition()
.duration(500)
.attr("transform", (d: any) => {
const pos = vis.outerArc.centroid(d);
pos[0] = (d.data.value / d3.max(vis.data, (d: any) => d.value)) * vis.outerRadius * (midAngle(d) < Math.PI ? 1.2 : -1.2) + (midAngle(d) < Math.PI ? 3 : -3);
return `translate(${pos})`;
})
.text((d: any) => d.data.key);
// mouse events (hover) for arcs
arcsEnter.select("path")
.on("mouseover", function (event: any, d: any) {
d3.select(event.currentTarget).attr("opacity", 1);
arcsEnter.selectAll("path").filter((arcData: any) =>
arcData.data.key !== d.data.key
).attr("opacity", 0.4);
const percentage = ((d.data.value / totalValues) * 100).toFixed(2);
vis.tooltip.style('opacity', 0.9).style('display', 'block');
vis.tooltip.html(`${d.data.key}<br>Total Medals: ${d.data.value}<br>Percentage: ${percentage}%`)
.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 () {
arcsEnter.selectAll("path").attr("opacity", 1);
vis.tooltip.style('opacity', 0).style('display', 'none');
});
// To make hover effect on lines also
arcsEnter.select("path.label-line")
.on("mouseover", function (event: any, d: any) {
d3.select(event.currentTarget).attr("opacity", 1);
arcsEnter.selectAll("path").filter((arcData: any) =>
arcData.data.key !== d.data.key
).attr("opacity", 0.4);
const percentage = ((d.data.value / totalValues) * 100).toFixed(2);
vis.tooltip.style('opacity', 0.9).style('display', 'block');
vis.tooltip.html(`${d.data.key}<br>Total Medals: ${d.data.value}<br>Percentage: ${percentage}%`)
.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 () {
arcsEnter.selectAll("path").attr("opacity", 1);
vis.tooltip.style('opacity', 0).style('display', 'none');
});
// UPDATE
arcs.select("path")
.transition()
.duration(500)
.attr("d", vis.arc)
.attr("fill", (d: any) => vis.color(d.data.key))
.attr("stroke", "#fff")
.attr("stroke-width", 1);
arcs.select("path.label-line")
.transition()
.duration(500)
.attr("d", (d: any) => {
const pos = vis.outerArc.centroid(d);
const mid = vis.arc.centroid(d);
pos[0] = (d.data.value / d3.max(vis.data, (d: any) => d.value)) * vis.outerRadius * (midAngle(d) < Math.PI ? 1.2 : -1.2);
return `M${mid[0]},${mid[1]} L${pos[0]},${pos[1]}`;
});
arcs.select("text")
.transition()
.duration(500)
.attr("text-anchor", (d: any) =>
midAngle(d) < Math.PI ? "start" : "end"
)
.attr("transform", (d: any) => {
const pos = vis.outerArc.centroid(d);
pos[0] = (d.data.value / d3.max(vis.data, (d: any) => d.value)) * vis.outerRadius * (midAngle(d) < Math.PI ? 1.2 : -1.2);
return `translate(${pos})`;
})
.text((d: any) => d.data.key);
// EXIT
arcs.exit().remove();
}
}
\ No newline at end of file
......@@ -4,11 +4,14 @@ import 'bootstrap';
import { StackedBarChartHelper } from './charts-helpers/stacked-bar-chart-helper';
import { MultiLineChartHelper } from './charts-helpers/multi-line-chart-helper';
import { PieChartHelper } from './charts-helpers/pie-chart-helper';
import { PloarAreaChartHelper } from './charts-helpers/polar-area-chart-helper';
const datasetPath = import.meta.env.VITE_DATASET_PATH;
let rawData: any[];
const stackerBarChartHelper = new StackedBarChartHelper();
const multiLineChartHelper = new MultiLineChartHelper();
const pieChartHelper = new PieChartHelper();
const polarChartHelper = new PloarAreaChartHelper();
d3.csv(datasetPath).then(data => {
rawData = data.filter(d => d.Medal !== '');
......@@ -23,6 +26,9 @@ d3.csv(datasetPath).then(data => {
pieChartHelper.setData(rawData);
pieChartHelper.appendChart();
polarChartHelper.setData(rawData);
polarChartHelper.appendChart();
});
// Attach event listeners to buttons
......
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