Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
D
DV-Project
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
almohanad.hafez
DV-Project
Commits
33a14f23
Commit
33a14f23
authored
Feb 13, 2025
by
Almouhannad Hafez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add multi line chart
parent
b4b9b020
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
362 additions
and
3 deletions
+362
-3
index.html
index.html
+4
-3
multi-line-chart-helper.ts
src/ts/charts-helpers/multi-line-chart-helper.ts
+123
-0
multi-line-chart.ts
src/ts/charts/multi-line-chart.ts
+227
-0
main.ts
src/ts/main.ts
+8
-0
No files found.
index.html
View file @
33a14f23
...
@@ -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>
...
...
src/ts/charts-helpers/multi-line-chart-helper.ts
0 → 100644
View file @
33a14f23
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
src/ts/charts/multi-line-chart.ts
0 → 100644
View file @
33a14f23
// 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
src/ts/main.ts
View file @
33a14f23
...
@@ -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
();
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment