Commit 33a14f23 authored by Almouhannad Hafez's avatar Almouhannad Hafez

Add multi line chart

parent b4b9b020
...@@ -39,16 +39,17 @@ ...@@ -39,16 +39,17 @@
<h2 class="accordion-header" id="headingTwo"> <h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Medal Distribution by Country Medal Trend by Country over time
</button> </button>
</h2> </h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" <div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo"
data-bs-parent="#olympicStatsAccordion"> data-bs-parent="#olympicStatsAccordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="chart-description"> <div class="chart-description">
This chart shows the distribution of medals won by different countries in the Olympic Games. This chart illustrates the trend of total medals won over time, highlighting the changes in performance
across various Olympic Games.
</div> </div>
<canvas id="topAthletesChart"></canvas> <div id="medalsTrendByTimeChart"></div>
</div> </div>
</div> </div>
</div> </div>
......
import * as d3 from 'd3';
import { ChartConfiguration } from "../chart-base/chart-configuration";
import { MultiLineChart } from "../charts/multi-line-chart";
export class MultiLineChartHelper {
private container: any;
private containerId: string = 'multi-line-chart-container';
private svgId: string = 'multi-line-chart';
private config: ChartConfiguration = new ChartConfiguration(`#${this.svgId}`);
private chart: MultiLineChart;
private data: any[] = [];
private currentIndex: number = 0;
private pageSize: number = 5;
private prevBtnId: string = 'multi-line-chart-prev-btn'
private prevBtn: any;
private nextBtnId: string = 'multi-line-chart-next-btn'
private nextBtn: any;
private rankingTextId: string = 'multi-line-chart-ranking-text';
private rankingText: any;
public setData(data: any[]) {
this.data = data;
const medalCounts = d3.rollup(
this.data.filter(d => d.Medal && d.Medal !== ""),
v => v.length,
d => d.Country,
d => d.Year
);
let result: any[] = [];
medalCounts.forEach((yearMap, country) => {
const entries: Array<{ key: number; value: number }> = [];
yearMap.forEach((count, year) => {
entries.push({ key: year, value: count });
});
result.push({ id: country, entries });
});
result.sort((a, b) => {
const totalA = d3.sum(a.entries, (d: any) => d.value);
const totalB = d3.sum(b.entries, (d: any) => d.value);
return totalB - totalA;
});
this.data = result.filter(d => d.entries.length >= 8);
}
public appendChart() {
// Add div container
this.container = d3.select('#medalsTrendByTimeChart')
.append('div')
.attr('class', 'container')
.attr('style', 'width: fit-content;')
.attr('id', `${this.containerId}`);
// Add ranking selection
this.container
.append('div')
.attr('class', 'text-center mt-4 mx-auto navigation-controls')
.html(`
<p style='text-align:center; font-weight:700;'>Select ranking:</p>
<button id="${this.prevBtnId}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i>
</button>
<span id="${this.rankingTextId}"></span>
<button id="${this.nextBtnId}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-right"></i>
</button>
`);
this.prevBtn = d3.select(`#${this.prevBtnId}`);
this.nextBtn = d3.select(`#${this.nextBtnId}`);
this.rankingText = d3.select(`#${this.rankingTextId}`)
this.prevBtn.on('click', () => {
if (this.currentIndex > 0) {
this.currentIndex -= this.pageSize;
}
this.updateChart();
});
this.nextBtn.on('click', () => {
if (this.currentIndex + this.pageSize < this.data.length) {
this.currentIndex += this.pageSize;
}
this.updateChart();
});
// add svg for chart
this.container
.append('svg')
.attr('id', `${this.svgId}`);
// initilize new chart
this.chart = new MultiLineChart(this.config, 'Year', 'Total Medals', this.data);
// set tooltip placeholders
d3.select('#multi-line-chart-tooltip').select('#id-title').text("Country: ");
d3.select('#multi-line-chart-tooltip').select('#key-title').text("Year: ");
d3.select('#multi-line-chart-tooltip').select('#value-title').text("Medals: ");
this.updateChart();
}
private updateChart() {
// Update navigation
this.prevBtn.attr('disabled', this.currentIndex === 0 ? true : null);
this.nextBtn.attr('disabled', this.currentIndex >= this.data.length - this.pageSize ? true : null);
this.rankingText.text(`${this.currentIndex + 1} - ${Math.min(this.currentIndex + this.pageSize, this.data.length)}`)
this.chart.data = this.data.slice(this.currentIndex, this.currentIndex + this.pageSize);
this.chart.updateVis();
}
}
\ No newline at end of file
// No dataset-related code here, just pass prepared data correctly using helper
// and you'll get vis with stacks-based filtering and tooltip printing id value
// Note that passed data must match interface multiLineChartDatatype
// Modify some hard-coded margins, paddings values to enhance vis appearance
import * as d3 from 'd3';
import { Chart } from '../chart-base/chart';
import { ChartConfiguration } from '../chart-base/chart-configuration';
interface multiLineChartDatatype {
key: any;
entries: {
key: any;
value: any;
}[];
}
export class MultiLineChart extends Chart {
private xScale: any;
private yScale: any;
private xAxis: any;
private yAxis: any;
private colorScale: any;
private lineGenerator: any;
private tooltip: any;
private xAxisLabel: string;
private yAxisLabel: string;
constructor(_config: ChartConfiguration, _xAxisLabel: string, _yAxisLabel: string, _data: multiLineChartDatatype[]) {
super(_config, _data);
this.xAxisLabel = _xAxisLabel;
this.yAxisLabel = _yAxisLabel;
this.initVis();
}
protected getDefaultMargins() {
return { top: 20, right: 150, bottom: 50, left: 70 };
}
protected getDefaultContainerSize() {
return { width: 800, height: 500 };
}
protected initVis() {
const vis = this;
vis.xScale = d3.scaleLinear()
.range([0, vis.width]);
vis.xAxis = vis.chart.append('g')
.attr('transform', `translate(0,${vis.height})`)
.call(d3.axisBottom(vis.xScale));
vis.chart.append("text")
.attr("class", "axis-label")
.attr("text-anchor", "middle")
.attr("x", vis.width / 2)
.attr("y", vis.height + 40)
.style('font-weight', 'bold')
.text(this.xAxisLabel);
vis.yScale = d3.scaleLinear()
.nice()
.range([vis.height, 0]);
vis.yAxis = vis.chart.append('g')
.call(d3.axisLeft(vis.yScale));
vis.chart.append("text")
.attr("class", "axis-label")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("x", -vis.height / 2)
.style('font-weight', 'bold')
.text(this.yAxisLabel);
vis.colorScale = d3.scaleOrdinal(d3.schemeCategory10);
vis.lineGenerator = d3.line()
.x((d: any) => vis.xScale(d.key))
.y((d: any) => vis.yScale(d.value));
// Add tooltip
this.tooltip = d3.select('body').append('div')
.attr('id', 'multi-line-chart-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('background', 'white')
.style('border', '1px solid black')
.style('padding', '5px')
.style('pointer-events', 'none')
.html(
`<span id="id-title"></span><span id="id-placeholder"></span><br>
<span id="key-title"></span><span id="key-placeholder"></span><br>
<span id="value-title"></span><span id="value-placeholder"></span>`);
}
public updateVis() {
const vis = this;
// Update scales
vis.xScale.domain(d3.extent(vis.data.flatMap(d => d.entries), (d: any) => d.key));
vis.yScale.domain([0, d3.max(vis.data.flatMap(d => d.entries), (d: any) => d.value)]).nice();
vis.colorScale.domain(this.data.map(d => d.id));
// Update Axis
vis.xAxis.transition().duration(1000).call(d3.axisBottom(vis.xScale));
vis.yAxis.transition().duration(1000).call(d3.axisLeft(vis.yScale));
vis.renderVis();
}
protected renderVis() {
const vis = this;
const lines = vis.chart.selectAll('.line').data(vis.data, (d: any) => d.id);
// Enter selection for new lines
const lineEnter = lines.enter()
.append('path')
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', (d: any) => vis.colorScale(d.id))
.attr('stroke-width', 4)
.attr('d', (d: any) => vis.lineGenerator(d.entries))
.style("opacity", 0);
// Merge and update existing lines with transition
lineEnter.merge(lines)
.transition()
.duration(500)
.attr('d', (d: any) => vis.lineGenerator(d.entries))
.style("opacity", 1); // Fade in
// Exit selection for removed lines
lines.exit()
.transition()
.duration(500)
.style("opacity", 0)
.remove();
// Line hover event
vis.chart.selectAll('.line')
.on("mouseover", (event: any) => {
d3.selectAll(".line").style("opacity", 0.2);
d3.select(event.currentTarget).style("opacity", 1).attr("stroke-width", 6);
})
.on("mouseout", function () {
d3.selectAll(".line").style("opacity", 1).attr("stroke-width", 4);
vis.tooltip.style("opacity", 0);
})
.on("mousemove", function (event: any, d: any) {
const mouseX = d3.pointer(event)[0];
const key = vis.xScale.invert(mouseX);
// Find the closest data point to the mouse position
const closestDataPoint = d.entries.reduce((prev: any, curr: any) => {
return (Math.abs(curr.key - key) < Math.abs(prev.key - key) ? curr : prev);
});
// Update tooltip content and position
d3.select('#multi-line-chart-tooltip')
.select('#id-placeholder')
.text(d.id);
d3.select('#multi-line-chart-tooltip')
.select('#key-placeholder')
.text(closestDataPoint.key);
d3.select('#multi-line-chart-tooltip')
.select('#value-placeholder')
.text(closestDataPoint.value);
vis.tooltip
.style("left", (event.pageX - 20) + "px")
.style("top", (event.pageY - 85) + "px")
.style("opacity", 1);
});
// Render legend //
vis.chart.append("text")
.attr("x", vis.width + 10)
.attr("y", 0)
.text("Filter by:")
.style("font-weight", "bold")
.style("font-size", "14px")
.style("user-select", "none");
// Bind the data to the legend items
const legend = vis.chart.selectAll('.legend').data(vis.data, (d: any) => d.id);
// Enter selection for new legend items
const legendEnter = legend.enter()
.append("g")
.attr("class", "legend")
.attr("transform", (_: any, i: number) => `translate(${vis.width + 10},${i * 20 + 20})`)
.on("mouseover", (event: any, d: any) => {
d3.selectAll(".line").style("opacity", 0.2);
d3.select(`.line[stroke='${vis.colorScale(d.id)}']`).style("opacity", 1).attr("stroke-width", 6);
d3.selectAll(".legend").select("rect").style("opacity", 0.2);
d3.selectAll(".legend").select("text").style("opacity", 0.2);
d3.select(event.currentTarget).select("rect").style("opacity", 1);
d3.select(event.currentTarget).select("text").style("opacity", 1);
})
.on("mouseout", function () {
d3.selectAll(".legend").select("rect").style("opacity", 1);
d3.selectAll(".legend").select("text").style("opacity", 1);
d3.selectAll(".line").style("opacity", 1).attr("stroke-width", 4);
});
// Append rectangles and text for the legend
legendEnter.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", (d: any) => vis.colorScale(d.id));
legendEnter.append("text")
.attr("x", 15)
.attr("y", 10)
.text((d: any) => d.id)
.style("font-size", "12px")
.style("user-select", "none");
// Update selection for existing legend items
const legendUpdate = legend.merge(legendEnter);
legendUpdate.attr("transform", (_: any, i: number) => `translate(${vis.width + 10},${i * 20 + 20})`);
// Exit selection for removed legend items
legend.exit().remove();
}
}
\ No newline at end of file
...@@ -2,12 +2,20 @@ import * as d3 from 'd3'; ...@@ -2,12 +2,20 @@ import * as d3 from 'd3';
import '/src/css/style.css'; 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';
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();
d3.csv(datasetPath).then(data => { d3.csv(datasetPath).then(data => {
rawData = data.filter(d => d.Medal !== '');
rawData = data.filter(d => d.Year !== '');
rawData = data.filter(d => d.Medal !== ''); rawData = data.filter(d => d.Medal !== '');
stackerBarChartHelper.setData(rawData); stackerBarChartHelper.setData(rawData);
stackerBarChartHelper.appendChart(); stackerBarChartHelper.appendChart();
multiLineChartHelper.setData(rawData);
multiLineChartHelper.appendChart();
}); });
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