|
15 | 15 | from functools import wraps |
16 | 16 | from collections import Counter |
17 | 17 | 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 | + |
18 | 24 | from bokeh.plotting import figure as bokeh_figure |
19 | 25 | from bokeh.palettes import Category20c |
20 | 26 | #from bokeh.transform import factor_cmap |
@@ -123,6 +129,27 @@ def add_legend_at(fig, position='right'): |
123 | 129 | "height": DEFAULT_FIGURE_HEIGHT} |
124 | 130 | } |
125 | 131 |
|
| 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 | + |
126 | 153 |
|
127 | 154 |
|
128 | 155 |
|
@@ -173,122 +200,147 @@ def rek_set_attr(obj: object, key: str, val:object) -> None: |
173 | 200 | key_new = ".".join(ke for ke in keys[1:]) |
174 | 201 | return rek_set_attr(obj2, key_new, val) |
175 | 202 |
|
| 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) |
176 | 219 |
|
| 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) |
177 | 229 |
|
| 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) |
178 | 239 |
|
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='', |
181 | 241 | width=0.1, xlabel='', ylabel='Number of answers', palette=Category20c, |
182 | 242 | fill_color=None, legend_labels=None, description='For more information about the HMC survey click here.', |
183 | 243 | 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 | + |
219 | 246 | y_keys = y |
220 | 247 | source = df |
221 | | - #print(y, x) |
222 | | - #print(df.column_names) |
223 | 248 | help_t = HelpTool(description=description, redirect=redirect) |
224 | 249 | 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 |
240 | 252 | 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'] |
244 | 257 |
|
| 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 | + |
245 | 295 | fig.add_tools(help_t) |
246 | 296 |
|
| 297 | + # Calculate bar positions for multiple series |
247 | 298 | nvisible = len(y_keys) |
248 | 299 | 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: |
252 | 303 | start = 0.0 |
253 | 304 | else: |
254 | | - start = nvisible//2 * -step |
| 305 | + start = nvisible // 2 * -step |
255 | 306 |
|
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}')] |
258 | 309 | 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}')) |
285 | 335 | 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) |
289 | 339 | 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 |
292 | 344 |
|
293 | 345 | return fig |
294 | 346 |
|
|
0 commit comments