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
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 {
...
@@ -7,4 +7,8 @@ body {
box-sizing
:
border-box
;
box-sizing
:
border-box
;
overflow-x
:
hidden
;
overflow-x
:
hidden
;
background
:
#f7f7f7
;
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
*
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'
;
const
datasetPath
=
import
.
meta
.
env
.
VITE_DATASET_PATH
;
const
datasetPath
=
import
.
meta
.
env
.
VITE_DATASET_PATH
;
d3
.
csv
(
datasetPath
).
then
(
data
=>
{
console
.
log
(
data
[
0
]);
})
let
rawData
:
any
[];
\ No newline at end of file
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