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
c29464d5
You need to sign in or sign up before continuing.
Commit
c29464d5
authored
Feb 11, 2025
by
Almouhannad Hafez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add stacked bar chart
parent
47eab80b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
352 additions
and
1 deletion
+352
-1
style.css
src/css/style.css
+4
-0
stacked-bar-chart-helper.ts
src/ts/charts-helpers/stacked-bar-chart-helper.ts
+108
-0
stacked-bar-chart.ts
src/ts/charts/stacked-bar-chart.ts
+231
-0
main.ts
src/ts/main.ts
+9
-1
No files found.
src/css/style.css
View file @
c29464d5
...
...
@@ -7,4 +7,8 @@ body {
box-sizing
:
border-box
;
overflow-x
:
hidden
;
background
:
#f7f7f7
;
}
#stacked-bar-chart-legend
.legend-item
:hover
{
opacity
:
0.8
;
}
\ No newline at end of file
src/ts/charts-helpers/stacked-bar-chart-helper.ts
0 → 100644
View file @
c29464d5
import
*
as
d3
from
'd3'
;
import
{
ChartConfiguration
}
from
'../chart-base/chart-configuration'
;
import
{
StackedBarChart
}
from
'../charts/stacked-bar-chart'
;
export
class
StackedBarChartHelper
{
private
stacks
:
any
[]
=
[
{
stackName
:
'Bronze'
,
stackColor
:
'#A77044'
},
{
stackName
:
'Silver'
,
stackColor
:
'#A7A7AD'
},
{
stackName
:
'Gold'
,
stackColor
:
'#FEE101'
},
];
private
container
:
any
;
private
containerId
:
string
=
'stacked-bar-chart-container'
;
private
svgId
:
string
=
'stacked-bar-chart'
;
private
config
:
ChartConfiguration
=
new
ChartConfiguration
(
`#
${
this
.
svgId
}
`
);
private
chart
:
StackedBarChart
;
private
data
:
any
[]
=
[];
private
currentIndex
:
number
=
0
;
private
pageSize
:
number
=
10
;
private
prevBtnId
:
string
=
'stacked-bar-chart-prev-btn'
private
prevBtn
:
any
;
private
nextBtnId
:
string
=
'stacked-bar-chart-next-btn'
private
nextBtn
:
any
;
private
rankingTextId
:
string
=
'stacked-bar-chart-ranking-text'
;
private
rankingText
:
any
;
public
setData
(
data
:
any
[])
{
this
.
data
=
data
;
// Process data (aggregating by country)
const
medalCounts
=
d3
.
rollup
(
this
.
data
,
(
entries
)
=>
({
Bronze
:
entries
.
filter
(
e
=>
e
.
Medal
===
'Bronze'
).
length
,
Silver
:
entries
.
filter
(
e
=>
e
.
Medal
===
'Silver'
).
length
,
Gold
:
entries
.
filter
(
e
=>
e
.
Medal
===
'Gold'
).
length
,
total
:
entries
.
length
}),
d
=>
d
.
Country
);
this
.
data
=
Array
.
from
(
medalCounts
,
([
country
,
counts
])
=>
({
id
:
country
,
// We'll consider country name as id sice its unique
...
counts
}))
.
sort
((
a
,
b
)
=>
b
.
total
-
a
.
total
);
// Sort descening
}
public
appendChart
()
{
// Add div container
this
.
container
=
d3
.
select
(
'body'
)
.
append
(
'div'
)
.
attr
(
'class'
,
'container'
)
// bootstrap
.
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'
,
()
=>
{
this
.
currentIndex
=
Math
.
max
(
0
,
this
.
currentIndex
-
this
.
pageSize
);
this
.
updateChart
();
});
this
.
nextBtn
.
on
(
'click'
,
()
=>
{
this
.
currentIndex
=
Math
.
min
(
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
StackedBarChart
(
this
.
config
,
this
.
stacks
,
'Countries'
,
'Total Medals'
,
this
.
data
);
this
.
updateChart
();
}
private
updateChart
()
{
this
.
prevBtn
.
attr
(
'disabled'
,
this
.
currentIndex
===
0
?
true
:
null
);
// Disable at start
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
)}
`
)
const
pageData
=
this
.
data
.
slice
(
this
.
currentIndex
,
this
.
currentIndex
+
this
.
pageSize
);
this
.
chart
.
data
=
pageData
;
this
.
chart
.
updateVis
();
// Trigger vis update
}
}
\ No newline at end of file
src/ts/charts/stacked-bar-chart.ts
0 → 100644
View file @
c29464d5
// 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
// 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'
;
export
class
StackedBarChart
extends
Chart
{
public
stacks
:
any
[];
private
activeKeys
:
string
[];
private
xAxisTitle
:
string
;
private
yAxisTitle
:
string
;
private
legend
:
d3
.
Selection
<
SVGGElement
,
unknown
,
HTMLElement
,
any
>
;
private
tooltip
:
d3
.
Selection
<
HTMLDivElement
,
unknown
,
HTMLElement
,
any
>
;
constructor
(
_config
:
ChartConfiguration
,
_stacks
:
any
[],
xAxisTitle
:
string
,
yAxisTitle
:
string
,
_data
?:
any
[])
{
super
(
_config
,
_data
);
this
.
stacks
=
_stacks
;
this
.
activeKeys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
this
.
xAxisTitle
=
xAxisTitle
;
this
.
yAxisTitle
=
yAxisTitle
;
this
.
initVis
();
}
private
xScale
:
d3
.
ScaleBand
<
string
>
;
private
yScale
:
d3
.
ScaleLinear
<
number
,
number
,
never
>
;
private
colorScale
:
d3
.
ScaleOrdinal
<
string
,
unknown
,
never
>
;
private
stackGenerator
:
d3
.
Stack
<
any
,
{
[
key
:
string
]:
number
;
},
string
>
;
protected
getDefaultMargins
()
{
return
{
top
:
5
,
right
:
100
,
bottom
:
100
,
left
:
100
};
}
protected
getDefaultContainerSize
()
{
return
{
width
:
800
,
height
:
400
};
}
protected
initVis
()
{
const
vis
=
this
;
// keys are staks in stacked bar chart
const
keys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
// Axes, scales
vis
.
xScale
=
d3
.
scaleBand
().
padding
(
0.3
);
vis
.
xScale
.
range
([
0
,
vis
.
width
]);
vis
.
chart
.
append
(
'text'
)
.
attr
(
'class'
,
'axis-title'
)
.
attr
(
'x'
,
vis
.
width
+
40
)
.
attr
(
'y'
,
vis
.
height
+
6
)
.
attr
(
'text-anchor'
,
'middle'
)
.
attr
(
'fill'
,
'black'
)
.
style
(
'font-weight'
,
'bold'
)
.
text
(
this
.
xAxisTitle
);
vis
.
chart
.
append
(
'g'
).
attr
(
'id'
,
'stacked-bar-chart-x-axis'
);
vis
.
yScale
=
d3
.
scaleLinear
();
vis
.
yScale
.
range
([
vis
.
height
,
0
]);
vis
.
chart
.
append
(
'text'
)
.
attr
(
'class'
,
'axis-title'
)
.
attr
(
'transform'
,
'rotate(-90)'
)
.
attr
(
'x'
,
-
vis
.
height
/
2
)
.
attr
(
'y'
,
-
50
)
.
attr
(
'text-anchor'
,
'middle'
)
.
style
(
'font-weight'
,
'bold'
)
.
text
(
this
.
yAxisTitle
);
vis
.
chart
.
append
(
'g'
).
attr
(
'id'
,
'stacked-bar-chart-y-axis'
);
vis
.
colorScale
=
d3
.
scaleOrdinal
()
.
domain
(
keys
)
.
range
(
this
.
stacks
.
map
(
d
=>
d
.
stackColor
));
vis
.
stackGenerator
=
d3
.
stack
()
.
keys
(
keys
)
.
order
(
d3
.
stackOrderNone
)
// keep original stack order
.
offset
(
d3
.
stackOffsetNone
);
// legend (passed stacks names)
vis
.
legend
=
vis
.
chart
.
append
(
'g'
)
.
attr
(
'id'
,
'stacked-bar-chart-legend'
)
.
attr
(
'transform'
,
`translate(0,
${
vis
.
height
+
80
}
)`
);
// Position legend below chart
vis
.
legend
.
append
(
'text'
)
.
attr
(
'x'
,
25
)
.
attr
(
'y'
,
15
)
.
attr
(
'text-anchor'
,
'middle'
)
.
style
(
'font-weight'
,
'bold'
)
.
text
(
'Filter by:'
);
keys
.
forEach
((
key
,
i
)
=>
{
const
legendItem
=
vis
.
legend
.
append
(
'g'
)
.
attr
(
'class'
,
'legend-item'
)
.
attr
(
'transform'
,
`translate(
${(
i
+
1
)
*
80
}
,0)`
)
.
style
(
'cursor'
,
'pointer'
)
.
on
(
'click'
,
_
=>
vis
.
handleKeyFiltering
(
key
));
legendItem
.
append
(
'rect'
)
.
attr
(
'width'
,
20
).
attr
(
'height'
,
20
)
.
attr
(
'fill'
,
String
(
vis
.
colorScale
(
key
)));
legendItem
.
append
(
'text'
)
.
attr
(
'x'
,
25
).
attr
(
'y'
,
15
)
.
text
(
key
);
});
// Tooltip
this
.
tooltip
=
d3
.
select
(
'body'
).
append
(
'div'
)
.
attr
(
'class'
,
'tooltip'
)
.
style
(
'opacity'
,
0
)
.
style
(
'position'
,
'absolute'
)
.
style
(
'background'
,
'white'
)
.
style
(
'border'
,
'1px solid black'
)
.
style
(
'padding'
,
'5px'
)
.
style
(
'pointer-events'
,
'none'
);
}
// Selecting key from legend menu handler
private
handleKeyFiltering
(
medalType
:
string
)
{
const
keys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
const
index
=
this
.
activeKeys
.
indexOf
(
medalType
);
if
(
index
!=
-
1
)
this
.
activeKeys
.
splice
(
index
,
1
);
else
{
this
.
activeKeys
.
push
(
medalType
);
}
if
(
this
.
activeKeys
.
length
===
0
)
this
.
activeKeys
=
keys
;
// Reset if none selected
this
.
updateVis
();
}
protected
renderVis
()
{
const
vis
=
this
;
const
keys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
const
orderedKeys
=
keys
.
filter
(
k
=>
vis
.
activeKeys
.
includes
(
k
));
vis
.
stackGenerator
.
keys
(
orderedKeys
);
const
stackedData
=
vis
.
stackGenerator
(
vis
.
data
);
// Enter-Update(Merge)-Exit for layers(each key stack) and rects
const
layers
=
vis
.
chart
.
selectAll
(
'.key-layer'
)
.
data
(
stackedData
,
(
d
:
any
)
=>
d
.
key
);
layers
.
exit
()
.
transition
().
duration
(
1000
)
.
style
(
'opacity'
,
0
)
.
remove
();
const
enterLayers
=
layers
.
enter
()
.
append
(
'g'
)
.
attr
(
'class'
,
'key-layer'
)
.
attr
(
'fill'
,
(
d
:
any
)
=>
vis
.
colorScale
(
d
.
key
));
const
allLayers
=
enterLayers
.
merge
(
layers
);
allLayers
.
each
((
layerData
:
any
,
index
:
number
,
groups
:
any
)
=>
{
const
layer
=
d3
.
select
(
groups
[
index
]);
const
rects
=
layer
.
selectAll
<
SVGRectElement
,
any
>
(
'rect'
).
data
(
layerData
,
(
d
:
any
)
=>
d
.
data
.
id
);
rects
.
exit
().
transition
().
duration
(
1000
)
.
attr
(
'y'
,
vis
.
height
).
attr
(
'height'
,
0
)
.
remove
();
const
enterRects
=
rects
.
enter
()
.
append
(
'rect'
)
.
attr
(
'x'
,
(
d
:
any
)
=>
vis
.
xScale
(
d
.
data
.
id
)
!
)
.
attr
(
'width'
,
vis
.
xScale
.
bandwidth
())
.
attr
(
'y'
,
vis
.
height
)
.
attr
(
'height'
,
0
)
.
on
(
'mouseover'
,
function
(
event
:
any
,
d
:
any
)
{
vis
.
tooltip
.
transition
().
duration
(
200
).
style
(
'opacity'
,
.
9
);
vis
.
tooltip
.
html
(
`
${
d
.
data
.
id
}
<br>
${
d
[
1
]
-
d
[
0
]}
`
)
.
style
(
'left'
,
(
event
.
pageX
-
20
)
+
'px'
)
.
style
(
'top'
,
(
event
.
pageY
-
60
)
+
'px'
);
})
.
on
(
'mousemove'
,
function
(
event
:
any
)
{
vis
.
tooltip
.
style
(
'left'
,
(
event
.
pageX
-
20
)
+
'px'
)
.
style
(
'top'
,
(
event
.
pageY
-
60
)
+
'px'
);
})
.
on
(
'mouseout'
,
function
()
{
vis
.
tooltip
.
transition
().
duration
(
200
).
style
(
'opacity'
,
0
);
});
enterRects
.
merge
(
rects
)
.
transition
().
duration
(
1000
)
.
attr
(
'y'
,
(
d
:
any
)
=>
vis
.
yScale
(
d
[
1
]))
.
attr
(
'height'
,
(
d
:
any
)
=>
vis
.
yScale
(
d
[
0
])
-
vis
.
yScale
(
d
[
1
]));
});
vis
.
legend
.
selectAll
(
'.legend-item'
)
.
style
(
'opacity'
,
(
_
,
index
)
=>
this
.
getLegendItemOpacity
(
index
));
vis
.
updateAxes
();
}
// Selected or not
private
getLegendItemOpacity
(
index
:
any
)
{
const
vis
=
this
;
const
orderedKeys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
return
vis
.
activeKeys
.
includes
(
orderedKeys
[
index
])
?
1
:
0.3
;
// Fade inactive items
}
private
updateAxes
()
{
const
vis
=
this
;
// Ensure axis changes are correct after changing data
vis
.
chart
.
select
(
'#stacked-bar-chart-x-axis'
)
.
attr
(
'transform'
,
`translate(0,
${
vis
.
height
}
)`
)
.
call
(
d3
.
axisBottom
(
vis
.
xScale
))
.
selectAll
(
'text'
)
.
attr
(
'transform'
,
'rotate(-30)'
)
.
style
(
'text-anchor'
,
'end'
)
.
style
(
'font-size'
,
'13px'
)
.
style
(
'fill'
,
'black'
);
vis
.
chart
.
select
(
'#stacked-bar-chart-y-axis'
)
.
call
(
d3
.
axisLeft
(
vis
.
yScale
).
ticks
(
5
))
.
selectAll
(
'text'
)
.
style
(
'font-size'
,
'13px'
)
.
style
(
'fill'
,
'black'
);
}
public
updateVis
()
{
const
vis
=
this
;
// Update domains and render
const
keys
=
this
.
stacks
.
map
(
d
=>
d
.
stackName
);
const
orderedKeys
=
keys
.
filter
(
k
=>
vis
.
activeKeys
.
includes
(
k
));
vis
.
xScale
.
domain
(
vis
.
data
.
map
((
d
:
any
)
=>
d
.
id
));
vis
.
yScale
.
domain
([
0
,
d3
.
max
(
vis
.
data
,
(
d
:
any
)
=>
orderedKeys
.
reduce
((
sum
,
key
)
=>
sum
+
d
[
key
],
0
)
)
!
]);
vis
.
renderVis
();
}
}
\ No newline at end of file
src/ts/main.ts
View file @
c29464d5
import
*
as
d3
from
'd3'
;
import
'/src/css/style.css'
;
import
'bootstrap'
;
import
{
StackedBarChartHelper
}
from
'./charts-helpers/stacked-bar-chart-helper'
;
const
datasetPath
=
import
.
meta
.
env
.
VITE_DATASET_PATH
;
d3
.
csv
(
datasetPath
).
then
(
data
=>
{
console
.
log
(
data
[
0
]);
})
\ No newline at end of file
let
rawData
:
any
[];
const
stackerBarChartHelper
=
new
StackedBarChartHelper
();
d3
.
csv
(
datasetPath
).
then
(
data
=>
{
rawData
=
data
.
filter
(
d
=>
d
.
Medal
!==
''
);
stackerBarChartHelper
.
setData
(
rawData
);
stackerBarChartHelper
.
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