Skip to content

Commit ff6ac6c

Browse files
committed
got the horizontal bar visualization working
1 parent 320e10c commit ff6ac6c

1 file changed

Lines changed: 146 additions & 94 deletions

File tree

survey_dashboard/plots.py

Lines changed: 146 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
from functools import wraps
1616
from collections import Counter
1717
from bokeh.models import ColumnDataSource
18+
19+
from bokeh.models import HelpTool
20+
from bokeh.models import HoverTool
21+
from bokeh.models import Range1d
22+
from bokeh.transform import dodge
23+
1824
from bokeh.plotting import figure as bokeh_figure
1925
from bokeh.palettes import Category20c
2026
#from bokeh.transform import factor_cmap
@@ -123,6 +129,27 @@ def add_legend_at(fig, position='right'):
123129
"height": DEFAULT_FIGURE_HEIGHT}
124130
}
125131

132+
horizontal_theme = {
133+
"figure_kwargs" : {
134+
"background_fill_color" : '#00000000', #transparent
135+
"border_fill_color" : '#00000000',
136+
# Note: No x_range.range_padding for horizontal bars as x_range is numeric
137+
"xgrid.grid_line_color": None,
138+
"xaxis.major_label_orientation": 1,
139+
"title.text_font_size": '18px',
140+
"yaxis.axis_label_text_font_size": '18px',
141+
"xaxis.axis_label_text_font_size": '18px',
142+
"xaxis.major_label_text_font_size": '16px',
143+
"yaxis.major_label_text_font_size": '16px',
144+
"toolbar.logo": None,
145+
"toolbar_location": "right",
146+
"legend.location": "top_right",
147+
"legend.orientation": "vertical",
148+
"legend.click_policy": "hide",
149+
"width": DEFAULT_FIGURE_WIDTH,
150+
"height": DEFAULT_FIGURE_HEIGHT}
151+
}
152+
126153

127154

128155

@@ -173,122 +200,147 @@ def rek_set_attr(obj: object, key: str, val:object) -> None:
173200
key_new = ".".join(ke for ke in keys[1:])
174201
return rek_set_attr(obj2, key_new, val)
175202

203+
# DONE: Refactored this function heavily to get the plots to work
204+
def bokeh_barchart(df, x='x_value', y=['y_value'], factors=None, figure=None, data_visible=[True], title='',
205+
width=0.1, xlabel='', ylabel='Number of answers', palette=Category20c,
206+
fill_color=None, legend_labels=None, description='For more information about the HMC survey click here.',
207+
redirect='https://helmholtz-metadaten.de/en/', orientation='vertical', x_range=None, y_range=None,**kwargs):
208+
"""Create an interactive bar chart with bokeh"""
209+
210+
# Choose theme based on orientation
211+
if orientation == 'horizontal':
212+
return _bokeh_barchart_horizontal(df, x, y, factors, figure, data_visible, title,
213+
width, xlabel, ylabel, palette, fill_color, legend_labels,
214+
description, redirect, orientation, x_range, y_range, **kwargs)
215+
else:
216+
return _bokeh_barchart_vertical(df, x, y, factors, figure, data_visible, title,
217+
width, xlabel, ylabel, palette, fill_color, legend_labels,
218+
description, redirect, orientation, x_range, y_range, **kwargs)
176219

220+
@apply_theme(theme=horizontal_theme)
221+
def _bokeh_barchart_horizontal(df, x='x_value', y=['y_value'], factors=None, figure=None, data_visible=[True], title='',
222+
width=0.1, xlabel='', ylabel='Number of answers', palette=Category20c,
223+
fill_color=None, legend_labels=None, description='For more information about the HMC survey click here.',
224+
redirect='https://helmholtz-metadaten.de/en/', orientation='vertical', x_range=None, y_range=None,**kwargs):
225+
"""Internal function for horizontal bar charts with horizontal theme"""
226+
return _bokeh_barchart_impl(df, x, y, factors, figure, data_visible, title,
227+
width, xlabel, ylabel, palette, fill_color, legend_labels,
228+
description, redirect, orientation, x_range, y_range, **kwargs)
177229

230+
@apply_theme(theme=default_theme)
231+
def _bokeh_barchart_vertical(df, x='x_value', y=['y_value'], factors=None, figure=None, data_visible=[True], title='',
232+
width=0.1, xlabel='', ylabel='Number of answers', palette=Category20c,
233+
fill_color=None, legend_labels=None, description='For more information about the HMC survey click here.',
234+
redirect='https://helmholtz-metadaten.de/en/', orientation='vertical', x_range=None, y_range=None,**kwargs):
235+
"""Internal function for vertical bar charts with default theme"""
236+
return _bokeh_barchart_impl(df, x, y, factors, figure, data_visible, title,
237+
width, xlabel, ylabel, palette, fill_color, legend_labels,
238+
description, redirect, orientation, x_range, y_range, **kwargs)
178239

179-
@apply_theme()
180-
def bokeh_barchart(df, x='x_value', y=['y_value'], factors=None, figure=None, data_visible=[True], title='',
240+
def _bokeh_barchart_impl(df, x='x_value', y=['y_value'], factors=None, figure=None, data_visible=[True], title='',
181241
width=0.1, xlabel='', ylabel='Number of answers', palette=Category20c,
182242
fill_color=None, legend_labels=None, description='For more information about the HMC survey click here.',
183243
redirect='https://helmholtz-metadaten.de/en/', orientation='vertical', x_range=None, y_range=None,**kwargs):
184-
"""Create an interactive bar chart with bokeh
185-
186-
:param df: [description]
187-
:type df: bokeh.models.ColumnDataSource
188-
:param x: [description], defaults to 'x_value'
189-
:type x: str, optional
190-
:param y: [description], defaults to ['y_value']
191-
:type y: list, optional
192-
:param factors: [description], defaults to None
193-
:type factors: [type], optional
194-
:param figure: [description], defaults to None
195-
:type figure: [type], optional
196-
:param data_visible: [description], defaults to [True]
197-
:type data_visible: list, optional
198-
:param title: [description], defaults to ''
199-
:type title: str, optional
200-
:param width: [description], defaults to 0.1
201-
:type width: float, optional
202-
:param xlabel: [description], defaults to ''
203-
:type xlabel: str, optional
204-
:param ylabel: [description], defaults to 'Number of answers'
205-
:type ylabel: str, optional
206-
:param palette: [description], defaults to Category20c
207-
:type palette: [type], optional
208-
:param fill_color: [description], defaults to None
209-
:type fill_color: [type], optional
210-
:param legend_labels: [description], defaults to None
211-
:type legend_labels: [type], optional
212-
:param description: [description], defaults to 'For more information about the HMC survey click here.'
213-
:type description: str, optional
214-
:param redirect: [description], defaults to 'https://helmholtz-metadaten.de/en/pages/structure-governance'
215-
:type redirect: str, optional
216-
:return: [description]
217-
:rtype: [type]
218-
"""
244+
"""Internal implementation of bar chart creation"""
245+
219246
y_keys = y
220247
source = df
221-
#print(y, x)
222-
#print(df.column_names)
223248
help_t = HelpTool(description=description, redirect=redirect)
224249
tools = 'wheel_zoom,box_zoom,undo,reset,save'
225-
#if x_range is None:
226-
# x_range = source.data[x]
227-
# Handle None ranges to prevent Bokeh validation error
228-
if y_range is None:
229-
# Automatically calculate y_range based on data
230-
max_values = []
231-
for y_key in y_keys:
232-
if y_key in source.data:
233-
max_values.extend(source.data[y_key])
234-
if max_values:
235-
y_max = max(max_values)
236-
# Add 10% padding to the top
237-
y_range = (0, y_max * 1.1)
238-
else:
239-
y_range = (0, 10) # Fallback default range
250+
251+
# Use the provided ranges or calculate defaults
240252
if x_range is None:
241-
x_range = ['Category 1', 'Category 2', 'Category 3'] # Default categories
242-
fig = bokeh_figure(x_range=x_range, y_range=y_range, title=title, #y_range=(0, 280),
243-
height=DEFAULT_FIGURE_HEIGHT, width=DEFAULT_FIGURE_WIDTH, toolbar_location='above', tools=tools)
253+
if x in source.data:
254+
x_range = source.data[x]
255+
else:
256+
x_range = ['Category 1', 'Category 2', 'Category 3']
244257

258+
# Convert numerical lists to strings for categorical data
259+
if isinstance(x_range, list) and len(x_range) > 0 and isinstance(x_range[0], (int, float)):
260+
x_range = [str(val) for val in x_range]
261+
262+
#if y_range is None:
263+
# Calculate numerical range from actual data values
264+
max_values = []
265+
for y_key in y_keys:
266+
if y_key in source.data:
267+
max_values.extend(source.data[y_key])
268+
269+
if max_values:
270+
numerical_max = max(max_values)
271+
numerical_range = (0, numerical_max * 1.1)
272+
else:
273+
numerical_range = (0, 10) # Fallback default range
274+
275+
276+
# Set up ranges based on orientation
277+
if orientation == 'vertical':
278+
# Vertical: categorical on x-axis, numerical on y-axis
279+
fig_x_range = x_range
280+
fig_y_range = numerical_range
281+
fig_xlabel = xlabel
282+
fig_ylabel = ylabel
283+
else:
284+
# Horizontal: categorical on y-axis, numerical on x-axis
285+
fig_x_range = numerical_range
286+
fig_y_range = x_range
287+
fig_xlabel = ylabel
288+
fig_ylabel = xlabel
289+
290+
# Create figure
291+
fig = bokeh_figure(x_range=fig_x_range, y_range=fig_y_range, title=title,
292+
height=DEFAULT_FIGURE_HEIGHT, width=DEFAULT_FIGURE_WIDTH,
293+
toolbar_location='above', tools=tools)
294+
245295
fig.add_tools(help_t)
246296

297+
# Calculate bar positions for multiple series
247298
nvisible = len(y_keys)
248299
step = width + 0.05
249-
if nvisible%2 == 0:
250-
start = -step*nvisible/2 + step/2.0
251-
elif nvisible==1:
300+
if nvisible % 2 == 0:
301+
start = -step * nvisible / 2 + step / 2.0
302+
elif nvisible == 1:
252303
start = 0.0
253304
else:
254-
start = nvisible//2 * -step
305+
start = nvisible // 2 * -step
255306

256-
position = [start + i*step for i in range(len(y))]
257-
tooltips=[(f'{x}', f'@{x}')]
307+
position = [start + i * step for i in range(len(y_keys))]
308+
tooltips = [(f'{x}', f'@{x}')]
258309
bars = []
259-
260-
#for i, y in enumerate(y_keys):
261-
# bar = fig.vbar(x=dodge(x, position[i], range=fig.x_range), top=y, source=source,
262-
# width=width, color=fill_color[i], legend_label=y, **kwargs)
263-
for i, y in enumerate(y_keys):
264-
if orientation=='vertical':
265-
bar = fig.vbar(x=dodge(x, position[i], range=fig.x_range), top=y, source=source,
266-
width=width, color=fill_color[i], legend_label=y, selection_fill_color='black',
267-
selection_fill_alpha=0.8,
268-
nonselection_fill_alpha=0.2,
269-
nonselection_fill_color="blue",
270-
selection_line_color="black", fill_alpha=0.8,
271-
nonselection_line_alpha=0.5, hover_fill_alpha=1.0,
272-
hover_line_color="black", hover_line_width=5.0, **kwargs)
273-
fig.y_range.start = 0
274-
else: # orientation=='horizontal':
275-
bar = fig.hbar(y=dodge(x, position[i], range=fig.y_range), right=y, source=source,
276-
height=width, color=fill_color[i], legend_label=y, selection_fill_color='black', selection_fill_alpha=0.8,
277-
nonselection_fill_alpha=0.2,
278-
nonselection_fill_color="blue",
279-
selection_line_color="black", fill_alpha=0.8,
280-
nonselection_line_alpha=0.5, hover_fill_alpha=1.0,
281-
hover_line_color="black", hover_line_width=5.0, **kwargs)
282-
fig.x_range.start = 0
283-
284-
tooltips.append((f'{y}', '@{' + str(y) + '}'))
310+
311+
# Create bars based on orientation
312+
for i, y_key in enumerate(y_keys):
313+
if orientation == 'vertical':
314+
# Vertical bars: dodge along x-axis (categorical)
315+
bar = fig.vbar(x=dodge(x, position[i], range=fig.x_range),
316+
top=y_key, source=source,
317+
width=width, color=fill_color[i], legend_label=y_key,
318+
selection_fill_color='black', selection_fill_alpha=0.8,
319+
nonselection_fill_alpha=0.2, nonselection_fill_color="blue",
320+
selection_line_color="black", fill_alpha=0.8,
321+
nonselection_line_alpha=0.5, hover_fill_alpha=1.0,
322+
hover_line_color="black", hover_line_width=5.0, **kwargs)
323+
else:
324+
# Horizontal bars: dodge along y-axis (categorical)
325+
bar = fig.hbar(y=dodge(x, position[i], range=fig.y_range),
326+
right=y_key, source=source,
327+
height=width, color=fill_color[i], legend_label=y_key,
328+
selection_fill_color='black', selection_fill_alpha=0.8,
329+
nonselection_fill_alpha=0.2, nonselection_fill_color="blue",
330+
selection_line_color="black", fill_alpha=0.8,
331+
nonselection_line_alpha=0.5, hover_fill_alpha=1.0,
332+
hover_line_color="black", hover_line_width=5.0, **kwargs)
333+
334+
tooltips.append((f'{y_key}', f'@{y_key}'))
285335
bars.append(bar)
286-
287-
# How the data was given, there is not a way for the hover tool to display a single value
288-
hover = HoverTool(tooltips=tooltips,renderers=bars)
336+
337+
# Add hover tool
338+
hover = HoverTool(tooltips=tooltips, renderers=bars)
289339
fig.add_tools(hover)
290-
fig.yaxis.axis_label = ylabel
291-
fig.xaxis.axis_label = xlabel
340+
341+
# Set axis labels
342+
fig.yaxis.axis_label = fig_ylabel
343+
fig.xaxis.axis_label = fig_xlabel
292344

293345
return fig
294346

0 commit comments

Comments
 (0)