Py.Cafe

Feanor1992/

Holocaust Memorial Dashboard

Dash Color ChangerOn this Holocaust Remembrance Day, I'm sharing a project created specifically for this solemn occasion: the Holocaust Memorial Dashboard.

DocsPricing
  • Camps.csv
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import pandas as pd
import numpy as np
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.express as px

df = pd.read_csv('Camps.csv')

# Estimated Jewish death counts per main camp
murder_dict = {
    'Auschwitz': 1100000,
    'Bergen-Belsen': 70000,
    'Buchenwald': 11800,
    'Dachau': 41500,
    'Flossenbürg': 30000,
    'Herzogenbusch': 749,
    'Hinzert': 1000,
    'Krakau-Plaszów': 8000,
    'Lublin': 78000,
    'Neuengamme': 42900,
    'Natzweiler-Struthof': 22000
}

# Map death counts and compute marker sizes (log scale)
df['MurderedJews'] = df['MAIN'].map(murder_dict)
# Compute marker sizes (log scale, minimum for zero)
min_size = 5
scale = 10
def size_func(x): return min_size if x == 0 else np.log10(x + 1) * scale
df['MarkerSize'] = df['MurderedJews'].apply(size_func)

# Additional camp info for click details
camp_info = {
    'Auschwitz': 'Largest complex: Auschwitz I, II–Birkenau, III–Monowitz.',
    'Bergen-Belsen': 'Initially POW camp, later concentration camp.',
    'Buchenwald': 'One of first and biggest German camps.',
    'Dachau': 'First Nazi camp opened 1933.',
    'Flossenbürg': 'Forced labor in quarries.',
    'Herzogenbusch': 'Only SS camp in occupied Netherlands.',
    'Hinzert': 'Smaller camp in western Germany.',
    'Krakau-Plaszów': 'Camp in occupied Poland.',
    'Lublin': 'Majdanek camp and death site.',
    'Neuengamme': 'Labor camp near Hamburg.',
    'Natzweiler-Struthof': 'Only camp on current French territory.'
}
df['CampInfo'] = df['MAIN'].map(camp_info).fillna('No additional info')

# List of unique main camps and map styles
df_valid = df[df['MurderedJews'] > 0]
main_camps = sorted(df_valid['MAIN'].unique())
options_main = ['All'] + main_camps

map_styles = {
    'OpenStreetMap': 'open-street-map',
    'Carto Light': 'carto-positron',
    'Carto Dark': 'carto-darkmatter'
}

app = dash.Dash(__name__, meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1'}])
app.title = 'Holocaust Subcamp Dashboard'

app.layout = html.Div([
    html.H1(
        'Holocaust Subcamp Dashboard',
        style={'textAlign': 'center'}
    ),
    html.P(
        'Marker size = log-scale of total Jewish deaths in main camp.',
        style={'textAlign': 'center'}
    ),
    html.Div([
        html.Div([
            html.H4('Filters'),
            html.Label('Main Camps:'),
            dcc.Dropdown(
                id='main-filter',
                options=[{'label': c, 'value': c} for c in options_main],
                value=['All'],
                style={'color': 'black'},
                multi=True
            ),
            html.Br(),
            html.Label('Map Style:'),
            dcc.Dropdown(
                id='style-filter',
                options=[{'label': k, 'value': v} for k, v in map_styles.items()],
                value='open-street-map',
                style={'color': 'black'}
            ),
            html.Br(),
            html.Div(
                id='info-box',
                children='Click a marker for details.',
                style={'whiteSpace': 'pre-line'})
        ], style={'width': '25%', 'float': 'left', 'padding': '10px'}),
        html.Div([dcc.Graph(id='map', style={'height': '80vh'})], style={'width': '75%', 'float': 'right'})
    ], style={'display': 'flex'}),
    html.Footer(html.P(["Data sources: ",
        html.A('Auschwitz Museum', href='https://www.auschwitz.org/en/', target='_blank'), ', ',
        html.A('USHMM', href='https://www.ushmm.org/', target='_blank'), ', ',
        html.A('Holocaust Encyclopedia', href='https://encyclopedia.ushmm.org/', target='_blank'), ', ',
        html.A('Yad Vashem Collections', href='https://collections.yadvashem.org/', target='_blank')
    ]), style={'textAlign': 'center', 'fontSize': 'small', 'marginTop': '20px'})
])

@app.callback(
    Output('map', 'figure'),
    [Input('main-filter', 'value'), Input('style-filter', 'value')]
)
def update_map(selected, style):
    if selected and 'All' not in selected:
        d = df[df['MAIN'].isin(selected) & (df['MurderedJews'] > 0)]
    else:
        d = df[df['MurderedJews'] > 0]
    
    fig = px.scatter_mapbox(
        d,
        lat='LAT',
        lon='LONG',
        hover_name='SUBCAMP',
        hover_data={'MAIN': True, 'MurderedJews': True},
        custom_data=['MAIN','MurderedJews','CampInfo'],
        size='MarkerSize',
        color='MAIN',
        mapbox_style=style,
        zoom=4,
        center={'lat':50,'lon':15},
        title='Holocaust Subcamps: Main Camp Fatalities'
    )
    fig.update_layout(margin=dict(l=0,r=0,t=30,b=0), coloraxis_colorbar=dict(title='Main Camp'))
    
    return fig

@app.callback(
    Output('info-box', 'children'),
    Input('map', 'clickData')
)
def display_info(clickData):
    if not clickData:
        return 'Click a marker for details.'
    pt = clickData['points'][0]
    subcamp = pt['hovertext']
    cd = pt.get('customdata', [])
    main = cd[0] if len(cd)>0 else 'N/A'
    deaths = cd[1] if len(cd)>1 else 'N/A'
    info = cd[2] if len(cd)>2 else 'No additional info'
    return f"Subcamp: {subcamp}\nMain Camp: {main}\nDeaths in main Camp: {int(deaths):,}" if isinstance(deaths,(int,float)) else f"Subcamp: {subcamp}\nMain Camp: {main}\nDeaths: {deaths}\n{info}"

if __name__ == '__main__':
    app.run_server()