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()