iqplot 0.3.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iqplot/cat.py ADDED
@@ -0,0 +1,1838 @@
1
+ import copy
2
+ import warnings
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ import colorcet
8
+
9
+ import bokeh.models
10
+ import bokeh.plotting
11
+
12
+ from . import utils
13
+ from .dist import histogram
14
+
15
+
16
+ def strip(
17
+ data=None,
18
+ q=None,
19
+ cats=None,
20
+ q_axis="x",
21
+ palette=None,
22
+ order=None,
23
+ p=None,
24
+ show_legend=None,
25
+ legend_location="right",
26
+ legend_orientation="vertical",
27
+ legend_click_policy="hide",
28
+ color_column=None,
29
+ parcoord_column=None,
30
+ tooltips=None,
31
+ marker="circle",
32
+ spread=None,
33
+ cat_grid=False,
34
+ marker_kwargs=None,
35
+ jitter_kwargs=None,
36
+ swarm_kwargs=None,
37
+ parcoord_kwargs=None,
38
+ jitter=None,
39
+ horizontal=None,
40
+ val=None,
41
+ click_policy=None,
42
+ **kwargs,
43
+ ):
44
+ """
45
+ Make a strip plot.
46
+
47
+ Parameters
48
+ ----------
49
+ data : Pandas DataFrame, 1D Numpy array, or xarray
50
+ DataFrame containing tidy data for plotting. If a Numpy array,
51
+ a single category is assumed and a strip plot generated from
52
+ data.
53
+ q : hashable
54
+ Name of column to use as quantitative variable if `data` is a
55
+ Pandas DataFrame. Otherwise, `q` is used as the quantitative
56
+ axis label.
57
+ cats : hashable or list of hashables
58
+ Name of column(s) to use as categorical variable(s).
59
+ q_axis : str, either 'x' or 'y', default 'x'
60
+ Axis along which the quantitative value varies.
61
+ palette : list of strings of hex colors, or single hex string
62
+ If a list, color palette to use. If a single string representing
63
+ a hex color, all glyphs are colored with that color. Default is
64
+ colorcet.b_glasbey_category10 from the colorcet package.
65
+ order : list or None
66
+ If not None, must be a list of unique group names when the input
67
+ data frame is grouped by `cats`. The order of the list specifies
68
+ the ordering of the categorical variables on the categorical
69
+ axis and legend. If None, the categories appear in the order in
70
+ which they appeared in the inputted data frame.
71
+ p : bokeh.plotting.Figure instance, or None (default)
72
+ If None, create a new figure. Otherwise, populate the existing
73
+ figure `p`.
74
+ show_legend : bool, default False
75
+ If True, display legend.
76
+ legend_location : str, default 'right'
77
+ Location of legend. If one of "right", "left", "above", or
78
+ "below", the legend is placed outside of the plot area. If one
79
+ of "top_left", "top_center", "top_right", "center_right",
80
+ "bottom_right", "bottom_center", "bottom_left", "center_left",
81
+ or "center", the legend is placed within the plot area. If a
82
+ 2-tuple, legend is placed according to the coordinates in the
83
+ tuple.
84
+ legend_orientation : str, default 'vertical'
85
+ Either 'horizontal' or 'vertical'.
86
+ legend_click_policy : str, default 'hide'
87
+ Either 'hide', 'mute', or None; how the glyphs respond when the
88
+ corresponding category is clicked in the legend.
89
+ color_column : hashable, default None
90
+ Column of `data` to use in determining color of glyphs. If None,
91
+ then `cats` is used.
92
+ parcoord_column : hashable, default None
93
+ Column of `data` to use to construct a parallel coordinate plot.
94
+ Data points with like entries in the parcoord_column are
95
+ connected with lines.
96
+ tooltips : list of 2-tuples
97
+ Specification for tooltips as per Bokeh specifications. For
98
+ example, if we want `col1` and `col2` tooltips, we can use
99
+ `tooltips=[('label 1', '@col1'), ('label 2', '@col2')]`.
100
+ marker : str, default 'circle'
101
+ Name of marker to be used in the plot. Must be one of
102
+ ['asterisk', 'circle', 'circle_cross', 'circle_x', 'cross',
103
+ 'dash', 'diamond', 'diamond_cross', 'hex', 'inverted_triangle',
104
+ 'square', 'square_cross', 'square_x', 'triangle', 'x'].
105
+ spread : str or None, default None
106
+ If 'jitter', spread points out using a jitter transform. If
107
+ 'swarm', spread points in beeswarm style. If None or 'none', do
108
+ not spread.
109
+ cat_grid : bool, default False
110
+ If True, show grid lines for categorical axis.
111
+ marker_kwargs : dict
112
+ Keyword arguments to pass when adding markers to the plot.
113
+ ["x", "y", "source", "marker", "cat", "legend_label"] are note
114
+ allowed because they are determined by other inputs.
115
+ jitter_kwargs : dict
116
+ Keyword arguments to be passed to `bokeh.transform.jitter()`. If
117
+ not specified, default is
118
+ `{'distribution': 'normal', 'width': 0.1}`. If the user
119
+ specifies `{'distribution': 'uniform'}`, the `'width'` entry is
120
+ adjusted to 0.4. Only active if `spread` is `'jitter'`.
121
+ swarm_kwargs : dict
122
+ Keyword arguments for use in generating swarm. Only active if
123
+ `spread` is `'swarm'`. Keys with allowed values are:
124
+
125
+ - 'corral': Either 'gutter' (default) or 'wrap'. This
126
+ specifies how points that are moved too far out are dealt
127
+ with. Using 'gutter', points are overlaid at the maximum
128
+ allowed distance. Using 'wrap', points are reflected inwards
129
+ from the maximal extent and possibly overlaid with other
130
+ points.
131
+
132
+ - 'priority': Either 'ascending' (default) or 'descending'.
133
+ Sort order when determining which points get moved in the
134
+ y-direction first.
135
+
136
+ - marker_pad_px : Gap between markers in units of pixels,
137
+ default 0.
138
+ parcoord_kwargs : dict
139
+ Keyword arguments to be passed to `p.line()` when making lines
140
+ for the parallel coordinate plot. Default is to have one-pixel
141
+ gray lines.
142
+ jitter : bool, default False
143
+ Deprecated, use `spread`.
144
+ horizontal : bool or None, default None
145
+ Deprecated. Use `q_axis`.
146
+ val : hashable
147
+ Deprecated, use `q`.
148
+ click_policy : str, default 'hide'
149
+ Deprecated. Use `legend_click_policy`.
150
+ kwargs
151
+ Any kwargs to be passed to `bokeh.plotting.figure()` when
152
+ instantiating the figure.
153
+
154
+ Returns
155
+ -------
156
+ output : bokeh.plotting.Figure instance
157
+ Plot populated with a strip plot.
158
+ """
159
+ # Protect against mutability of dicts
160
+ jitter_kwargs = copy.copy(jitter_kwargs)
161
+ swarm_kwargs = copy.copy(swarm_kwargs)
162
+ marker_kwargs = copy.copy(marker_kwargs)
163
+
164
+ q, legend_click_policy, _ = utils._parse_deprecations(
165
+ q, q_axis, val, horizontal, "x", click_policy, legend_click_policy, None, None
166
+ )
167
+
168
+ # Hand check jitter deprecation
169
+ if jitter is not None:
170
+ if jitter:
171
+ if spread is None:
172
+ spread = "jitter"
173
+ warnings.warn("`jitter` is deprecated. Use spread='jitter'.")
174
+ if spread == "jitter":
175
+ warnings.warn("`jitter` is deprecated. Use spread='jitter'.")
176
+ else:
177
+ raise RuntimeError(
178
+ "`jitter` is deprecated. Use spread='jitter'. `jitter` and `spread` are in conflict."
179
+ )
180
+ else:
181
+ if spread == "jitter":
182
+ raise RuntimeError(
183
+ "`jitter` is deprecated. Use `spread`. `jitter` and `spread` are in conflict."
184
+ )
185
+ else:
186
+ warnings.warn("`jitter` is deprecated. Use spread='jitter'.")
187
+
188
+ # Check spread
189
+ if spread is not None:
190
+ spread = spread.lower()
191
+ if spread == "beeswarm":
192
+ raise RuntimeError("Did you mean `spread='swarm'`?")
193
+ if spread not in ("swarm", "jitter", "none", None):
194
+ raise RuntimeError(
195
+ "Invalid `spread`. Valid choices are 'swarm', 'jitter', and None."
196
+ )
197
+
198
+ if spread is not None and spread != "none" and parcoord_column is not None:
199
+ raise NotImplementedError(
200
+ "Parallel coordinate plots are not implemented with jitter or swarm spreading."
201
+ )
202
+
203
+ if palette is None:
204
+ palette = colorcet.b_glasbey_category10
205
+
206
+ if show_legend is None:
207
+ if color_column is None:
208
+ show_legend = False
209
+ else:
210
+ show_legend = not _color_column_hexcodes(data, color_column)
211
+
212
+ # The legend for a strip plot comes from `color_column`, not `cats`,
213
+ # so only `order` is checked here.
214
+ utils._check_cats_none(cats, order)
215
+
216
+ data, q, cats, show_legend = utils._data_cats(data, q, cats, show_legend, None)
217
+ order = utils._order_to_str(order)
218
+
219
+ cats, cols = utils._check_cat_input(
220
+ data, cats, q, color_column, parcoord_column, tooltips, palette, order, kwargs
221
+ )
222
+
223
+ grouped = data.groupby(cats, sort=False)
224
+
225
+ if p is None:
226
+ p, factors, color_factors = _cat_figure(
227
+ data, grouped, q, order, color_column, q_axis, kwargs
228
+ )
229
+ else:
230
+ if isinstance(p.x_range, bokeh.models.ranges.FactorRange) and q_axis == "x":
231
+ raise RuntimeError("`q_axis` is 'x', but `p` has a categorical x-axis.")
232
+ elif isinstance(p.y_range, bokeh.models.ranges.FactorRange) and q_axis == "y":
233
+ raise RuntimeError("`q_axis` is 'y', but `p` has a categorical y-axis.")
234
+
235
+ _, factors, color_factors = _get_cat_range(
236
+ data, grouped, order, color_column, q_axis
237
+ )
238
+
239
+ if tooltips is not None:
240
+ p.add_tools(bokeh.models.HoverTool(tooltips=tooltips, name="hover_glyphs"))
241
+
242
+ if jitter_kwargs is None:
243
+ jitter_kwargs = dict(width=0.1, mean=0, distribution="normal")
244
+ elif not isinstance(jitter_kwargs, dict):
245
+ raise RuntimeError("`jitter_kwargs` must be a dict.")
246
+ elif "width" not in jitter_kwargs:
247
+ if (
248
+ "distribution" not in jitter_kwargs
249
+ or jitter_kwargs["distribution"] == "uniform"
250
+ ):
251
+ jitter_kwargs["width"] = 0.4
252
+ else:
253
+ jitter_kwargs["width"] = 0.1
254
+
255
+ if swarm_kwargs is None:
256
+ swarm_kwargs = dict(corral="gutter", priority="ascending", marker_pad_px=0)
257
+ elif not isinstance(swarm_kwargs, dict):
258
+ raise RuntimeError("`swarm_kwargs` must be a dict.")
259
+ if "corral" not in swarm_kwargs:
260
+ swarm_kwargs["corral"] = "gutter"
261
+ if "priority" not in swarm_kwargs:
262
+ swarm_kwargs["priority"] = "ascending"
263
+ if "marker_pad_px" not in swarm_kwargs:
264
+ swarm_kwargs["marker_pad_px"] = 0
265
+
266
+ marker_kwargs = utils._check_marker_kwargs(marker_kwargs)
267
+
268
+ if "name" not in marker_kwargs:
269
+ marker_kwargs["name"] = "hover_glyphs"
270
+ if (
271
+ "color" not in marker_kwargs
272
+ and "fill_color" not in marker_kwargs
273
+ and "line_color" not in marker_kwargs
274
+ ):
275
+ if color_column is None:
276
+ color_column = "cat"
277
+ if show_legend:
278
+ warnings.warn(
279
+ "`color_column` is not specified. No legend will be generated."
280
+ )
281
+ show_legend = False
282
+ if color_factors == "hex":
283
+ marker_kwargs["line_color"] = color_column
284
+ marker_kwargs["fill_color"] = color_column
285
+ if show_legend:
286
+ warnings.warn(
287
+ "`color_column` consists of hex colors. No legend will be generated."
288
+ )
289
+ show_legend = False
290
+ elif not show_legend:
291
+ marker_kwargs["fill_color"] = bokeh.transform.factor_cmap(
292
+ color_column, palette=palette, factors=color_factors
293
+ )
294
+ marker_kwargs["line_color"] = bokeh.transform.factor_cmap(
295
+ color_column, palette=palette, factors=color_factors
296
+ )
297
+
298
+ if marker == "tick":
299
+ marker = "dash"
300
+ marker = utils._check_marker(marker)
301
+
302
+ if marker == "dash":
303
+ if spread == "swarm":
304
+ raise RuntimeError(
305
+ "Cannot have 'swarm' spreading with dash or tick markers."
306
+ )
307
+ if "angle" not in marker_kwargs and q_axis == "x":
308
+ marker_kwargs["angle"] = np.pi / 2
309
+ if "size" not in marker_kwargs:
310
+ if q_axis == "x":
311
+ marker_kwargs["size"] = p.frame_height * 0.25 / len(grouped)
312
+ else:
313
+ marker_kwargs["size"] = p.frame_width * 0.25 / len(grouped)
314
+ else:
315
+ if "size" not in marker_kwargs:
316
+ marker_kwargs["size"] = 4
317
+ if "line_width" not in marker_kwargs:
318
+ marker_kwargs["line_width"] = 1
319
+
320
+ source_dict = _cat_source_dict(data, cats, cols, color_column)
321
+
322
+ if spread == "swarm":
323
+ r = (marker_kwargs["size"] + marker_kwargs["line_width"]) / 2
324
+
325
+ if (
326
+ q_axis == "x" and "x_axis_type" in kwargs and kwargs["x_axis_type"] == "log"
327
+ ) or (
328
+ q_axis == "y" and "y_axis_type" in kwargs and kwargs["y_axis_type"] == "log"
329
+ ):
330
+ log_q = True
331
+ else:
332
+ log_q = False
333
+
334
+ if q_axis == "x" and np.all(utils._range_specified(p.x_range)):
335
+ if log_q:
336
+ q_range = [np.log10(p.x_range.start), np.log10(p.x_range.end)]
337
+ else:
338
+ q_range = [p.x_range.start, p.x_range.end]
339
+ elif q_axis == "y" and np.all(utils._range_specified(p.y_range)):
340
+ if log_q:
341
+ q_range = [np.log10(p.y_range.start), np.log10(p.y_range.end)]
342
+ else:
343
+ q_range = [p.y_range.start, p.y_range.end]
344
+ else:
345
+ if log_q:
346
+ q_range_width = np.log10(data[q]).max() - np.log10(data[q]).min()
347
+ q_range = [
348
+ np.log10(data[q]).min() - 0.05 * q_range_width,
349
+ np.log10(data[q]).max() + 0.05 * q_range_width,
350
+ ]
351
+ else:
352
+ q_range_width = data[q].max() - data[q].min()
353
+ q_range = [
354
+ data[q].min() - 0.05 * q_range_width,
355
+ data[q].max() + 0.05 * q_range_width,
356
+ ]
357
+
358
+ swarm_transform = (
359
+ grouped[q]
360
+ .transform(_swarm, p, r, q_range, q_axis, log_q, **swarm_kwargs)
361
+ .values
362
+ )
363
+ source_dict["__swarm"] = [
364
+ (*cat, y_val) if isinstance(cat, tuple) else (cat, y_val)
365
+ for cat, y_val in zip(source_dict["cat"], swarm_transform)
366
+ ]
367
+
368
+ if q_axis == "x":
369
+ x = q
370
+ if spread == "jitter":
371
+ jitter_kwargs["range"] = p.y_range
372
+ y = bokeh.transform.jitter("cat", **jitter_kwargs)
373
+ elif spread == "swarm":
374
+ y = "__swarm"
375
+ else:
376
+ y = "cat"
377
+ if not cat_grid:
378
+ p.ygrid.grid_line_color = None
379
+ else:
380
+ y = q
381
+ if spread == "jitter":
382
+ jitter_kwargs["range"] = p.x_range
383
+ x = bokeh.transform.jitter("cat", **jitter_kwargs)
384
+ elif spread == "swarm":
385
+ x = "__swarm"
386
+ else:
387
+ x = "cat"
388
+ if not cat_grid:
389
+ p.xgrid.grid_line_color = None
390
+
391
+ if parcoord_column is not None:
392
+ source_pc = _parcoord_source(data, q, cats, q_axis, parcoord_column, factors)
393
+
394
+ if parcoord_kwargs is None:
395
+ line_color = "gray"
396
+ parcoord_kwargs = {}
397
+ elif not isinstance(parcoord_kwargs, dict):
398
+ raise RuntimeError("`parcoord_kwargs` must be a dict.")
399
+
400
+ if "color" in parcoord_kwargs and "line_color" not in parcoord_kwargs:
401
+ line_color = parcoord_kwargs.pop("color")
402
+ else:
403
+ line_color = parcoord_kwargs.pop("line_color", "gray")
404
+
405
+ p.multi_line(
406
+ source=source_pc, xs="xs", ys="ys", line_color=line_color, **parcoord_kwargs
407
+ )
408
+
409
+ if color_factors == "hex" or color_column == "cat" or not show_legend:
410
+ p.scatter(
411
+ source=bokeh.models.ColumnDataSource(source_dict),
412
+ x=x,
413
+ y=y,
414
+ marker=marker,
415
+ **marker_kwargs,
416
+ )
417
+ else:
418
+ items = []
419
+ df = pd.DataFrame(source_dict)
420
+ for i, (name, g) in enumerate(df.groupby(color_column)):
421
+ marker_kwargs["color"] = palette[i % len(palette)]
422
+ mark = p.scatter(source=g, x=x, y=y, marker=marker, **marker_kwargs)
423
+ items.append((g["__label"].iloc[0], [mark]))
424
+
425
+ if len(p.legend) == 1:
426
+ for item in items:
427
+ p.legend.items.append(
428
+ bokeh.models.LegendItem(label=item[0], renderers=item[1])
429
+ )
430
+ else:
431
+ if len(p.legend) > 1:
432
+ warnings.warn(
433
+ "Ambiguous which legend to add glyphs to. Creating new legend."
434
+ )
435
+ if legend_location in ["right", "left", "above", "below"]:
436
+ legend = bokeh.models.Legend(
437
+ items=items,
438
+ location="center",
439
+ orientation=legend_orientation,
440
+ title=color_column,
441
+ )
442
+ p.add_layout(legend, legend_location)
443
+ elif legend_location in [
444
+ "top_left",
445
+ "top_center",
446
+ "top_right",
447
+ "center_right",
448
+ "bottom_right",
449
+ "bottom_center",
450
+ "bottom_left",
451
+ "center_left",
452
+ "center",
453
+ ] or isinstance(legend_location, tuple):
454
+ legend = bokeh.models.Legend(
455
+ items=items,
456
+ location=legend_location,
457
+ orientation=legend_orientation,
458
+ title=color_column,
459
+ )
460
+ p.add_layout(legend, "center")
461
+ else:
462
+ raise RuntimeError(
463
+ 'Invalid `legend_location`. Must be a 2-tuple specifying location or one of ["right", "left", "above", "below", "top_left", "top_center", "top_right", "center_right", "bottom_right", "bottom_center", "bottom_left", "center_left", "center"]'
464
+ )
465
+
466
+ p.legend.click_policy = legend_click_policy
467
+
468
+ return p
469
+
470
+
471
+ def box(
472
+ data=None,
473
+ q=None,
474
+ cats=None,
475
+ q_axis="x",
476
+ palette=None,
477
+ order=None,
478
+ p=None,
479
+ whisker_caps=False,
480
+ display_points=True,
481
+ outlier_marker="circle",
482
+ min_data=5,
483
+ cat_grid=False,
484
+ box_kwargs=None,
485
+ median_kwargs=None,
486
+ whisker_kwargs=None,
487
+ outlier_kwargs=None,
488
+ display_outliers=None,
489
+ horizontal=None,
490
+ val=None,
491
+ **kwargs,
492
+ ):
493
+ """
494
+ Make a box-and-whisker plot.
495
+
496
+ Parameters
497
+ ----------
498
+ data : Pandas DataFrame, 1D Numpy array, or xarray
499
+ DataFrame containing tidy data for plotting. If a Numpy array,
500
+ a single category is assumed and a box plot with a single box is
501
+ generated from data.
502
+ q : hashable
503
+ Name of column to use as quantitative variable if `data` is a
504
+ Pandas DataFrame. Otherwise, `q` is used as the quantitative
505
+ axis label.
506
+ cats : hashable or list of hashables
507
+ Name of column(s) to use as categorical variable(s).
508
+ q_axis : str, either 'x' or 'y', default 'x'
509
+ Axis along which the quantitative value varies.
510
+ palette : list of strings of hex colors, or single hex string
511
+ If a list, color palette to use. If a single string representing
512
+ a hex color, all glyphs are colored with that color. Default is
513
+ colorcet.b_glasbey_category10 from the colorcet package.
514
+ order : list or None
515
+ If not None, must be a list of unique group names when the input
516
+ data frame is grouped by `cats`. The order of the list specifies
517
+ the ordering of the categorical variables on the categorical
518
+ axis and legend. If None, the categories appear in the order in
519
+ which they appeared in the inputted data frame.
520
+ p : bokeh.plotting.Figure instance, or None (default)
521
+ If None, create a new figure. Otherwise, populate the existing
522
+ figure `p`.
523
+ whisker_caps : bool, default False
524
+ If True, put caps on whiskers. If False, omit caps.
525
+ display_points : bool, default True
526
+ If True, display outliers and any other points that arise from
527
+ categories with fewer than `min_data` data points; otherwise
528
+ suppress them. This should only be False when using the boxes
529
+ as annotation on another plot.
530
+ outlier_marker : str, default 'circle'
531
+ Name of marker to be used in the plot. Must be one of
532
+ ['asterisk', 'circle', 'circle_cross', 'circle_x', 'cross',
533
+ 'dash', 'diamond', 'diamond_cross', 'hex', 'inverted_triangle',
534
+ 'square', 'square_cross', 'square_x', 'triangle', 'x'].
535
+ min_data : int, default 5
536
+ Minimum number of data points in a given category in order to
537
+ make a box and whisker. Otherwise, individual data points are
538
+ plotted as in a strip plot.
539
+ cat_grid : bool, default False
540
+ If True, display grid line for categorical axis.
541
+ box_kwargs : dict, default None
542
+ A dictionary of kwargs to be passed into `p.hbar()` or
543
+ `p.vbar()` when constructing the boxes for the box plot.
544
+ median_kwargs : dict, default None
545
+ A dictionary of kwargs to be passed into `p.hbar()` or
546
+ `p.vbar()` when constructing the median line for the box plot.
547
+ whisker_kwargs : dict, default None
548
+ A dictionary of kwargs to be passed into `p.segment()`
549
+ when constructing the whiskers for the box plot.
550
+ outlier_kwargs : dict, default None
551
+ A dictionary of kwargs to be passed into `p.scatter()`
552
+ when constructing the outliers for the box plot.
553
+ display_outliers : bool, default None
554
+ Deprecated. Use `display_points`.
555
+ horizontal : bool or None, default None
556
+ Deprecated. Use `q_axis`.
557
+ val : hashable
558
+ Deprecated, use `q`.
559
+ kwargs
560
+ Kwargs that are passed to bokeh.plotting.figure() in constructing
561
+ the figure.
562
+
563
+ Returns
564
+ -------
565
+ output : bokeh.plotting.Figure instance
566
+ Plot populated with box-and-whisker plot.
567
+
568
+ Notes
569
+ -----
570
+ Uses the Tukey convention for box plots. The top and bottom of
571
+ the box are respectively the 75th and 25th percentiles of the
572
+ data. The line in the middle of the box is the median. The top
573
+ whisker extends to the maximum of the set of data points that are
574
+ less than 1.5 times the IQR beyond the top of the box, with an
575
+ analogous definition for the lower whisker. Data points not
576
+ between the ends of the whiskers are considered outliers and are
577
+ plotted as individual points.
578
+ """
579
+ # Protect against mutability of dicts
580
+ box_kwargs = copy.copy(box_kwargs)
581
+ median_kwargs = copy.copy(median_kwargs)
582
+ whisker_kwargs = copy.copy(whisker_kwargs)
583
+ outlier_kwargs = copy.copy(outlier_kwargs)
584
+
585
+ q, _, _ = utils._parse_deprecations(
586
+ q, q_axis, val, horizontal, "x", None, None, None, None
587
+ )
588
+
589
+ if display_outliers is not None:
590
+ warnings.warn(
591
+ f"`display_outliers` is deprecated. Use `display_points`. Using `display_points={display_outliers}.",
592
+ DeprecationWarning,
593
+ )
594
+ display_points = display_outliers
595
+
596
+ if palette is None:
597
+ palette = colorcet.b_glasbey_category10
598
+
599
+ # A box plot has no legend, so only `order` is checked here.
600
+ utils._check_cats_none(cats, order)
601
+
602
+ data, q, cats, _ = utils._data_cats(data, q, cats, False, None)
603
+ order = utils._order_to_str(order)
604
+
605
+ cats, cols = utils._check_cat_input(
606
+ data, cats, q, None, None, None, palette, order, box_kwargs
607
+ )
608
+
609
+ if outlier_kwargs is None:
610
+ outlier_kwargs = dict()
611
+ elif not isinstance(outlier_kwargs, dict):
612
+ raise RuntimeError("`outlier_kwargs` must be a dict.")
613
+
614
+ if box_kwargs is None:
615
+ box_kwargs = {"line_color": None}
616
+ box_width = 0.4
617
+ elif not isinstance(box_kwargs, dict):
618
+ raise RuntimeError("`box_kwargs` must be a dict.")
619
+ else:
620
+ box_width = box_kwargs.pop("width", 0.4)
621
+ if "line_color" not in box_kwargs:
622
+ box_kwargs["line_color"] = None
623
+
624
+ if whisker_kwargs is None:
625
+ whisker_kwargs = {"line_color": "black"}
626
+ elif not isinstance(whisker_kwargs, dict):
627
+ raise RuntimeError("`whisker_kwargs` must be a dict.")
628
+ elif "line_color" not in whisker_kwargs and "color" not in whisker_kwargs:
629
+ whisker_kwargs["line_color"] = "black"
630
+
631
+ if median_kwargs is None:
632
+ median_kwargs = {"line_color": "white"}
633
+ elif not isinstance(median_kwargs, dict):
634
+ raise RuntimeError("`median_kwargs` must be a dict.")
635
+ elif "color" not in median_kwargs and "line_color" not in median_kwargs:
636
+ median_kwargs["line_color"] = "white"
637
+
638
+ if q_axis == "x":
639
+ if "height" in box_kwargs:
640
+ warnings.warn("'height' entry in `box_kwargs` ignored; using `box_width`.")
641
+ del box_kwargs["height"]
642
+ else:
643
+ if "width" in box_kwargs:
644
+ warnings.warn("'width' entry in `box_kwargs` ignored; using `box_width`.")
645
+ del box_kwargs["width"]
646
+
647
+ grouped = data.groupby(cats, sort=False)
648
+
649
+ if p is None:
650
+ p, factors, color_factors = _cat_figure(
651
+ data, grouped, q, order, None, q_axis, kwargs
652
+ )
653
+ else:
654
+ _, factors, color_factors = _get_cat_range(data, grouped, order, None, q_axis)
655
+
656
+ marker = utils._check_marker(outlier_marker)
657
+
658
+ source_box, source_outliers = _box_source(data, cats, q, cols, min_data)
659
+
660
+ if "color" in outlier_kwargs:
661
+ if "line_color" in outlier_kwargs or "fill_color" in outlier_kwargs:
662
+ raise RuntimeError(
663
+ "If `color` is in `outlier_kwargs`, `line_color` and `fill_color` cannot be."
664
+ )
665
+ else:
666
+ if "fill_color" in box_kwargs:
667
+ if "fill_color" not in outlier_kwargs:
668
+ outlier_kwargs["fill_color"] = box_kwargs["fill_color"]
669
+ if "line_color" not in outlier_kwargs:
670
+ outlier_kwargs["line_color"] = box_kwargs["fill_color"]
671
+ else:
672
+ if "fill_color" not in outlier_kwargs:
673
+ outlier_kwargs["fill_color"] = bokeh.transform.factor_cmap(
674
+ "cat", palette=palette, factors=factors
675
+ )
676
+ if "line_color" not in outlier_kwargs:
677
+ outlier_kwargs["line_color"] = bokeh.transform.factor_cmap(
678
+ "cat", palette=palette, factors=factors
679
+ )
680
+
681
+ if "fill_color" not in box_kwargs:
682
+ box_kwargs["fill_color"] = bokeh.transform.factor_cmap(
683
+ "cat", palette=palette, factors=factors
684
+ )
685
+
686
+ if q_axis == "x":
687
+ p.segment(
688
+ source=source_box,
689
+ y0="cat",
690
+ y1="cat",
691
+ x0="top",
692
+ x1="top_whisker",
693
+ **whisker_kwargs,
694
+ )
695
+ p.segment(
696
+ source=source_box,
697
+ y0="cat",
698
+ y1="cat",
699
+ x0="bottom",
700
+ x1="bottom_whisker",
701
+ **whisker_kwargs,
702
+ )
703
+ if whisker_caps:
704
+ p.hbar(
705
+ source=source_box,
706
+ y="cat",
707
+ left="top_whisker",
708
+ right="top_whisker",
709
+ height=box_width / 4,
710
+ **whisker_kwargs,
711
+ )
712
+ p.hbar(
713
+ source=source_box,
714
+ y="cat",
715
+ left="bottom_whisker",
716
+ right="bottom_whisker",
717
+ height=box_width / 4,
718
+ **whisker_kwargs,
719
+ )
720
+ p.hbar(
721
+ source=source_box,
722
+ y="cat",
723
+ left="bottom",
724
+ right="top",
725
+ height=box_width,
726
+ **box_kwargs,
727
+ )
728
+ p.hbar(
729
+ source=source_box,
730
+ y="cat",
731
+ left="middle",
732
+ right="middle",
733
+ height=box_width,
734
+ **median_kwargs,
735
+ )
736
+ if display_points:
737
+ p.scatter(
738
+ source=source_outliers, y="cat", x=q, marker=marker, **outlier_kwargs
739
+ )
740
+ if not cat_grid:
741
+ p.ygrid.grid_line_color = None
742
+ else:
743
+ p.segment(
744
+ source=source_box,
745
+ x0="cat",
746
+ x1="cat",
747
+ y0="top",
748
+ y1="top_whisker",
749
+ **whisker_kwargs,
750
+ )
751
+ p.segment(
752
+ source=source_box,
753
+ x0="cat",
754
+ x1="cat",
755
+ y0="bottom",
756
+ y1="bottom_whisker",
757
+ **whisker_kwargs,
758
+ )
759
+ if whisker_caps:
760
+ p.vbar(
761
+ source=source_box,
762
+ x="cat",
763
+ bottom="top_whisker",
764
+ top="top_whisker",
765
+ width=box_width / 4,
766
+ **whisker_kwargs,
767
+ )
768
+ p.vbar(
769
+ source=source_box,
770
+ x="cat",
771
+ bottom="bottom_whisker",
772
+ top="bottom_whisker",
773
+ width=box_width / 4,
774
+ **whisker_kwargs,
775
+ )
776
+ p.vbar(
777
+ source=source_box,
778
+ x="cat",
779
+ bottom="bottom",
780
+ top="top",
781
+ width=box_width,
782
+ **box_kwargs,
783
+ )
784
+ p.vbar(
785
+ source=source_box,
786
+ x="cat",
787
+ bottom="middle",
788
+ top="middle",
789
+ width=box_width,
790
+ **median_kwargs,
791
+ )
792
+ if display_points:
793
+ p.scatter(
794
+ source=source_outliers, x="cat", y=q, marker=marker, **outlier_kwargs
795
+ )
796
+ if not cat_grid:
797
+ p.xgrid.grid_line_color = None
798
+
799
+ return p
800
+
801
+
802
+ def stripbox(
803
+ data=None,
804
+ q=None,
805
+ cats=None,
806
+ q_axis="x",
807
+ palette=None,
808
+ order=None,
809
+ p=None,
810
+ show_legend=False,
811
+ legend_location="right",
812
+ legend_orientation="vertical",
813
+ legend_click_policy="hide",
814
+ top_level="strip",
815
+ color_column=None,
816
+ parcoord_column=None,
817
+ tooltips=None,
818
+ marker="circle",
819
+ spread=None,
820
+ cat_grid=False,
821
+ marker_kwargs=None,
822
+ jitter_kwargs=None,
823
+ swarm_kwargs=None,
824
+ parcoord_kwargs=None,
825
+ whisker_caps=True,
826
+ min_data=5,
827
+ box_kwargs=None,
828
+ median_kwargs=None,
829
+ whisker_kwargs=None,
830
+ jitter=None,
831
+ horizontal=None,
832
+ val=None,
833
+ click_policy=None,
834
+ **kwargs,
835
+ ):
836
+ """
837
+ Make a strip plot with a box plot as annotation.
838
+
839
+ Parameters
840
+ ----------
841
+ data : Pandas DataFrame, 1D Numpy array, or xarray
842
+ DataFrame containing tidy data for plotting. If a Numpy array,
843
+ a single category is assumed and a strip plot generated from
844
+ data.
845
+ q : hashable
846
+ Name of column to use as quantitative variable if `data` is a
847
+ Pandas DataFrame. Otherwise, `q` is used as the quantitative
848
+ axis label.
849
+ cats : hashable or list of hashables
850
+ Name of column(s) to use as categorical variable(s).
851
+ q_axis : str, either 'x' or 'y', default 'x'
852
+ Axis along which the quantitative value varies.
853
+ palette : list of strings of hex colors, or single hex string
854
+ If a list, color palette to use. If a single string representing
855
+ a hex color, all glyphs are colored with that color. Default is
856
+ colorcet.b_glasbey_category10 from the colorcet package.
857
+ order : list or None
858
+ If not None, must be a list of unique group names when the input
859
+ data frame is grouped by `cats`. The order of the list specifies
860
+ the ordering of the categorical variables on the categorical
861
+ axis and legend. If None, the categories appear in the order in
862
+ which they appeared in the inputted data frame.
863
+ p : bokeh.plotting.Figure instance, or None (default)
864
+ If None, create a new figure. Otherwise, populate the existing
865
+ figure `p`.
866
+ top_level : str, default 'strip'
867
+ If 'box', the box plot is overlaid. If 'strip', the strip plot
868
+ is overlaid.
869
+ show_legend : bool, default False
870
+ If True, display legend.
871
+ legend_location : str, default 'right'
872
+ Location of legend. If one of "right", "left", "above", or
873
+ "below", the legend is placed outside of the plot area. If one
874
+ of "top_left", "top_center", "top_right", "center_right",
875
+ "bottom_right", "bottom_center", "bottom_left", "center_left",
876
+ or "center", the legend is placed within the plot area. If a
877
+ 2-tuple, legend is placed according to the coordinates in the
878
+ tuple.
879
+ legend_orientation : str, default 'vertical'
880
+ Either 'horizontal' or 'vertical'.
881
+ legend_click_policy : str, default 'hide'
882
+ Either 'hide', 'mute', or None; how the glyphs respond when the
883
+ corresponding category is clicked in the legend.
884
+ color_column : hashable, default None
885
+ Column of `data` to use in determining color of glyphs. The data
886
+ in the color_column are assumed to be categorical. If the data
887
+ in color_column consist entirely of hex colors, then those
888
+ colors are directly used to color the glyphs. If None,
889
+ then `cats` is used.
890
+ parcoord_column : hashable, default None
891
+ Column of `data` to use to construct a parallel coordinate plot.
892
+ Data points with like entries in the parcoord_column are
893
+ connected with lines in the strip plot.
894
+ tooltips : list of 2-tuples
895
+ Specification for tooltips as per Bokeh specifications. For
896
+ example, if we want `col1` and `col2` tooltips, we can use
897
+ `tooltips=[('label 1', '@col1'), ('label 2', '@col2')]`.
898
+ marker : str, default 'circle'
899
+ Name of marker to be used in the plot. Must be one of
900
+ ['asterisk', 'circle', 'circle_cross', 'circle_x', 'cross',
901
+ 'dash', 'diamond', 'diamond_cross', 'hex', 'inverted_triangle',
902
+ 'square', 'square_cross', 'square_x', 'triangle', 'x'].
903
+ spread : str or None, default None
904
+ If 'jitter', spread points out using a jitter transform. If
905
+ 'swarm', spread points in beeswarm style. If None or 'none', do
906
+ not spread.
907
+ cat_grid : bool, default False
908
+ If True, display grid line for categorical axis.
909
+ marker_kwargs : dict
910
+ Keyword arguments to pass when adding markers to the plot.
911
+ ["x", "y", "marker", "source", "cat", "legend"] are not allowed
912
+ because they are determined by other inputs.
913
+ jitter_kwargs : dict
914
+ Keyword arguments to be passed to `bokeh.transform.jitter()`. If
915
+ not specified, default is
916
+ `{'distribution': 'normal', 'width': 0.1}`. If the user
917
+ specifies `{'distribution': 'uniform'}`, the `'width'` entry is
918
+ adjusted to 0.4. Only active if `spread` is `'jitter'`.
919
+ swarm_kwargs : dict
920
+ Keyword arguments for use in generating swarm. Only active if
921
+ `spread` is `'swarm'`. Keys with allowed values are:
922
+
923
+ - 'corral': Either 'gutter' (default) or 'wrap'. This
924
+ specifies how points that are moved too far out are dealt
925
+ with. Using 'gutter', points are overlaid at the maximum
926
+ allowed distance. Using 'wrap', points are reflected inwards
927
+ from the maximal extent and possibly overlaid with other
928
+ points.
929
+
930
+ - 'priority': Either 'ascending' (default) or 'descending'.
931
+ Sort order when determining which points get moved in the
932
+ y-direction first.
933
+
934
+ - marker_pad_px : Gap between markers in units of pixels,
935
+ default 0.
936
+ parcoord_kwargs : dict
937
+ Keyword arguments to be passed to `p.line()` when making lines
938
+ for the parallel coordinate plot. Default is to have one-pixel
939
+ gray lines.
940
+ whisker_caps : bool, default True
941
+ If True, put caps on whiskers. If False, omit caps.
942
+ min_data : int, default 5
943
+ Minimum number of data points in a given category in order to
944
+ make a box and whisker. Otherwise, individual data points are
945
+ plotted as in a strip plot.
946
+ box_kwargs : dict, default None
947
+ A dictionary of kwargs to be passed into `p.hbar()` or
948
+ `p.vbar()` when constructing the boxes for the box plot.
949
+ median_kwargs : dict, default None
950
+ A dictionary of kwargs to be passed into `p.hbar()` or
951
+ `p.vbar()` when constructing the median line for the box plot.
952
+ whisker_kwargs : dict, default None
953
+ A dictionary of kwargs to be passed into `p.segment()`
954
+ when constructing the whiskers for the box plot.
955
+ jitter : bool, default False
956
+ Deprecated, use `spread`.
957
+ horizontal : bool or None, default None
958
+ Deprecated. Use `q_axis`.
959
+ val : hashable
960
+ Deprecated, use `q`.
961
+ click_policy : str, default 'hide'
962
+ Deprecated. Use `legend_click_policy`.
963
+ kwargs
964
+ Any kwargs to be passed to `bokeh.plotting.figure()` when
965
+ instantiating the figure.
966
+
967
+ Returns
968
+ -------
969
+ output : bokeh.plotting.Figure instance
970
+ Plot populated with a strip-box plot.
971
+ """
972
+ # display_points not allowed in kwargs
973
+ if "display_points" in kwargs:
974
+ raise ValueError("display_points not allowed as a kwarg for stripbox.")
975
+
976
+ # Protect against mutability of dicts
977
+ box_kwargs = copy.copy(box_kwargs)
978
+ median_kwargs = copy.copy(median_kwargs)
979
+ whisker_kwargs = copy.copy(whisker_kwargs)
980
+ jitter_kwargs = copy.copy(jitter_kwargs)
981
+ swarm_kwargs = copy.copy(swarm_kwargs)
982
+ marker_kwargs = copy.copy(marker_kwargs)
983
+ parcoord_kwargs = copy.copy(parcoord_kwargs)
984
+
985
+ # Set defaults
986
+ if box_kwargs is None:
987
+ box_kwargs = dict(line_color="gray", fill_alpha=0)
988
+ if "color" not in box_kwargs and "line_color" not in box_kwargs:
989
+ box_kwargs["line_color"] = "gray"
990
+ if ("fill_alpha" not in box_kwargs) and ("fill_color" not in box_kwargs):
991
+ box_kwargs["fill_alpha"] = 0
992
+ elif ("fill_color" in box_kwargs) and ("fill_alpha" not in box_kwargs):
993
+ box_kwargs["fill_alpha"] = 0.5
994
+
995
+ if median_kwargs is None:
996
+ median_kwargs = dict(line_color="gray")
997
+ if "color" not in median_kwargs and "line_color" not in median_kwargs:
998
+ median_kwargs["line_color"] = "gray"
999
+
1000
+ if whisker_kwargs is None:
1001
+ whisker_kwargs = dict(line_color="gray")
1002
+ if "color" not in whisker_kwargs and "line_color" not in whisker_kwargs:
1003
+ whisker_kwargs["line_color"] = "gray"
1004
+
1005
+ if top_level == "box":
1006
+ p = strip(
1007
+ data=data,
1008
+ q=q,
1009
+ cats=cats,
1010
+ q_axis=q_axis,
1011
+ palette=palette,
1012
+ order=order,
1013
+ p=p,
1014
+ show_legend=show_legend,
1015
+ legend_location=legend_location,
1016
+ legend_orientation=legend_orientation,
1017
+ legend_click_policy=legend_click_policy,
1018
+ color_column=color_column,
1019
+ parcoord_column=parcoord_column,
1020
+ tooltips=tooltips,
1021
+ marker=marker,
1022
+ spread=spread,
1023
+ cat_grid=cat_grid,
1024
+ marker_kwargs=marker_kwargs,
1025
+ jitter_kwargs=jitter_kwargs,
1026
+ swarm_kwargs=swarm_kwargs,
1027
+ parcoord_kwargs=parcoord_kwargs,
1028
+ jitter=jitter,
1029
+ horizontal=horizontal,
1030
+ val=val,
1031
+ click_policy=click_policy,
1032
+ **kwargs,
1033
+ )
1034
+
1035
+ p = box(
1036
+ data=data,
1037
+ q=q,
1038
+ cats=cats,
1039
+ q_axis=q_axis,
1040
+ palette=palette,
1041
+ order=order,
1042
+ p=p,
1043
+ display_points=False,
1044
+ whisker_caps=whisker_caps,
1045
+ min_data=min_data,
1046
+ box_kwargs=box_kwargs,
1047
+ median_kwargs=median_kwargs,
1048
+ whisker_kwargs=whisker_kwargs,
1049
+ horizontal=horizontal,
1050
+ val=val,
1051
+ )
1052
+ elif top_level == "strip":
1053
+ p = box(
1054
+ data=data,
1055
+ q=q,
1056
+ cats=cats,
1057
+ q_axis=q_axis,
1058
+ palette=palette,
1059
+ order=order,
1060
+ p=p,
1061
+ display_points=False,
1062
+ cat_grid=cat_grid,
1063
+ whisker_caps=whisker_caps,
1064
+ min_data=min_data,
1065
+ box_kwargs=box_kwargs,
1066
+ median_kwargs=median_kwargs,
1067
+ whisker_kwargs=whisker_kwargs,
1068
+ horizontal=horizontal,
1069
+ val=val,
1070
+ **kwargs,
1071
+ )
1072
+
1073
+ p = strip(
1074
+ data=data,
1075
+ q=q,
1076
+ cats=cats,
1077
+ q_axis=q_axis,
1078
+ palette=palette,
1079
+ order=order,
1080
+ p=p,
1081
+ show_legend=show_legend,
1082
+ legend_location=legend_location,
1083
+ legend_orientation=legend_orientation,
1084
+ legend_click_policy=legend_click_policy,
1085
+ color_column=color_column,
1086
+ parcoord_column=parcoord_column,
1087
+ tooltips=tooltips,
1088
+ marker=marker,
1089
+ spread=spread,
1090
+ cat_grid=cat_grid,
1091
+ marker_kwargs=marker_kwargs,
1092
+ jitter_kwargs=jitter_kwargs,
1093
+ swarm_kwargs=swarm_kwargs,
1094
+ parcoord_kwargs=parcoord_kwargs,
1095
+ jitter=jitter,
1096
+ horizontal=horizontal,
1097
+ val=val,
1098
+ click_policy=click_policy,
1099
+ **kwargs,
1100
+ )
1101
+ else:
1102
+ raise RuntimeError("Invalid `top_level`. Allowed values are 'box' and 'strip'.")
1103
+
1104
+ return p
1105
+
1106
+
1107
+ def striphistogram(
1108
+ data=None,
1109
+ q=None,
1110
+ cats=None,
1111
+ q_axis="x",
1112
+ palette=None,
1113
+ order=None,
1114
+ p=None,
1115
+ show_legend=None,
1116
+ legend_location="right",
1117
+ legend_orientation="vertical",
1118
+ legend_click_policy="hide",
1119
+ top_level="strip",
1120
+ color_column=None,
1121
+ parcoord_column=None,
1122
+ tooltips=None,
1123
+ marker="circle",
1124
+ spread=None,
1125
+ cat_grid=True,
1126
+ marker_kwargs=None,
1127
+ jitter_kwargs=None,
1128
+ swarm_kwargs=None,
1129
+ parcoord_kwargs=None,
1130
+ bins="freedman-diaconis",
1131
+ style=None,
1132
+ mirror=True,
1133
+ hist_height=0.75,
1134
+ conf_int=False,
1135
+ ptiles=(2.5, 97.5),
1136
+ n_bs_reps=10000,
1137
+ line_kwargs=None,
1138
+ fill_kwargs=None,
1139
+ conf_int_kwargs=None,
1140
+ kind=None,
1141
+ jitter=None,
1142
+ horizontal=None,
1143
+ val=None,
1144
+ click_policy=None,
1145
+ **kwargs,
1146
+ ):
1147
+ """
1148
+ Make a strip plot with a histogram as annotation.
1149
+
1150
+ Parameters
1151
+ ----------
1152
+ data : Pandas DataFrame, 1D Numpy array, or xarray
1153
+ DataFrame containing tidy data for plotting. If a Numpy array,
1154
+ a single category is assumed and a strip plot generated from
1155
+ data.
1156
+ q : hashable
1157
+ Name of column to use as quantitative variable if `data` is a
1158
+ Pandas DataFrame. Otherwise, `q` is used as the quantitative
1159
+ axis label.
1160
+ cats : hashable or list of hashables
1161
+ Name of column(s) to use as categorical variable(s).
1162
+ q_axis : str, either 'x' or 'y', default 'x'
1163
+ Axis along which the quantitative value varies.
1164
+ palette : list of strings of hex colors, or single hex string
1165
+ If a list, color palette to use. If a single string representing
1166
+ a hex color, all glyphs are colored with that color. Default is
1167
+ colorcet.b_glasbey_category10 from the colorcet package.
1168
+ order : list or None
1169
+ If not None, must be a list of unique group names when the input
1170
+ data frame is grouped by `cats`. The order of the list specifies
1171
+ the ordering of the categorical variables on the categorical
1172
+ axis and legend. If None, the categories appear in the order in
1173
+ which they appeared in the inputted data frame.
1174
+ p : bokeh.plotting.Figure instance, or None (default)
1175
+ If None, create a new figure. Otherwise, populate the existing
1176
+ figure `p`.
1177
+ top_level : str, default 'strip'
1178
+ If 'histogram', the histogram is overlaid. If 'strip', the strip
1179
+ plot is overlaid.
1180
+ show_legend : bool, default False
1181
+ If True, display legend.
1182
+ legend_location : str, default 'right'
1183
+ Location of legend. If one of "right", "left", "above", or
1184
+ "below", the legend is placed outside of the plot area. If one
1185
+ of "top_left", "top_center", "top_right", "center_right",
1186
+ "bottom_right", "bottom_center", "bottom_left", "center_left",
1187
+ or "center", the legend is placed within the plot area. If a
1188
+ 2-tuple, legend is placed according to the coordinates in the
1189
+ tuple.
1190
+ legend_orientation : str, default 'vertical'
1191
+ Either 'horizontal' or 'vertical'.
1192
+ legend_click_policy : str, default 'hide'
1193
+ Either 'hide', 'mute', or None; how the glyphs respond when the
1194
+ corresponding category is clicked in the legend.
1195
+ color_column : hashable, default None
1196
+ Column of `data` to use in determining color of glyphs. The data
1197
+ in the color_column are assumed to be categorical. If the data
1198
+ in color_column consist entirely of hex colors, then those
1199
+ colors are directly used to color the glyphs. If None,
1200
+ then `cats` is used.
1201
+ parcoord_column : hashable, default None
1202
+ Column of `data` to use to construct a parallel coordinate plot.
1203
+ Data points with like entries in the parcoord_column are
1204
+ connected with lines in the strip plot.
1205
+ tooltips : list of 2-tuples
1206
+ Specification for tooltips as per Bokeh specifications. For
1207
+ example, if we want `col1` and `col2` tooltips, we can use
1208
+ `tooltips=[('label 1', '@col1'), ('label 2', '@col2')]`.
1209
+ marker : str, default 'circle'
1210
+ Name of marker to be used in the plot. Must be one of
1211
+ ['asterisk', 'circle', 'circle_cross', 'circle_x', 'cross',
1212
+ 'dash', 'diamond', 'diamond_cross', 'hex', 'inverted_triangle',
1213
+ 'square', 'square_cross', 'square_x', 'triangle', 'x'].
1214
+ spread : str or None, default None
1215
+ If 'jitter', spread points out using a jitter transform. If
1216
+ 'swarm', spread points in beeswarm style. If None or 'none', do
1217
+ not spread.
1218
+ cat_grid : bool, default True
1219
+ If True, display grid line for categorical axis.
1220
+ marker_kwargs : dict
1221
+ Keyword arguments to pass when adding markers to the plot.
1222
+ ["x", "y", "source", "marker", "cat", "legend"] are not allowed
1223
+ because they are determined by other inputs.
1224
+ jitter_kwargs : dict
1225
+ Keyword arguments to be passed to `bokeh.transform.jitter()`. If
1226
+ not specified, default is
1227
+ `{'distribution': 'normal', 'width': 0.1}`. If the user
1228
+ specifies `{'distribution': 'uniform'}`, the `'width'` entry is
1229
+ adjusted to 0.4. Only active if `spread` is `'jitter'`.
1230
+ swarm_kwargs : dict
1231
+ Keyword arguments for use in generating swarm. Only active if
1232
+ `spread` is `'swarm'`. Keys with allowed values are:
1233
+
1234
+ - 'corral': Either 'gutter' (default) or 'wrap'. This
1235
+ specifies how points that are moved too far out are dealt
1236
+ with. Using 'gutter', points are overlaid at the maximum
1237
+ allowed distance. Using 'wrap', points are reflected inwards
1238
+ from the maximal extent and possibly overlaid with other
1239
+ points.
1240
+
1241
+ - 'priority': Either 'ascending' (default) or 'descending'.
1242
+ Sort order when determining which points get moved in the
1243
+ y-direction first.
1244
+
1245
+ - marker_pad_px : Gap between markers in units of pixels,
1246
+ default 0.
1247
+ parcoord_kwargs : dict
1248
+ Keyword arguments to be passed to `p.line()` when making lines
1249
+ for the parallel coordinate plot. Default is to have one-pixel
1250
+ gray lines.
1251
+ bins : int, array_like, or str, default 'freedman-diaconis'
1252
+ If int or array_like, setting for `bins` kwarg to be passed to
1253
+ `np.histogram()`. If 'exact', then each unique value in the
1254
+ data gets its own bin. If 'integer', then integer data is
1255
+ assumed and each integer gets its own bin. If 'sqrt', uses the
1256
+ square root rule to determine number of bins. If
1257
+ `freedman-diaconis`, uses the Freedman-Diaconis rule for number
1258
+ of bins.
1259
+ style : None or one of ['step', 'step_filled']
1260
+ Default for overlaid histograms is 'step' and for stacked
1261
+ histograms 'step_filled'. The exception is when `conf_int` is
1262
+ True, in which case `style` must be 'step'.
1263
+ mirror : bool, default True
1264
+ If True, reflect the histogram through zero.
1265
+ hist_height : float, default 0.75
1266
+ Maximal height of histogram or its confidence interval as a
1267
+ fraction of available height along categorical axis. Only active
1268
+ when `arrangement` is 'stack'.
1269
+ conf_int : bool, default False
1270
+ If True, display confidence interval of the histogram.
1271
+ ptiles : list, default (2.5, 97.5)
1272
+ The percentiles to use for the confidence interval of the
1273
+ histogram. Ignored if `conf_int` is False.
1274
+ n_bs_reps : int, default 10,000
1275
+ Number of bootstrap replicates to do to compute confidence
1276
+ interval of histogram. Ignored if `conf_int` is False.
1277
+ line_kwargs : dict
1278
+ Keyword arguments to pass to `p.line()` in constructing the
1279
+ histograms. By default, {"line_width": 2}.
1280
+ fill_kwargs : dict
1281
+ Keyword arguments to pass to `p.patch()` when making the fill
1282
+ for the step-filled histogram or confidence intervals. Ignored
1283
+ if `style = 'step'` and `conf_int` is False. By default
1284
+ {"fill_alpha": 0.3, "line_alpha": 0}.
1285
+ jitter : bool, default False
1286
+ Deprecated, use `spread`.
1287
+ horizontal : bool or None, default None
1288
+ Deprecated. Use `q_axis`.
1289
+ val : hashable
1290
+ Deprecated, use `q`.
1291
+ click_policy : str, default 'hide'
1292
+ Deprecated. Use `legend_click_policy`.
1293
+ conf_int_kwargs : dict
1294
+ Deprecated. Use `fill_kwargs`.
1295
+ kind : str, default 'step_filled'
1296
+ Deprecated. Use `style`.
1297
+ kwargs
1298
+ Any kwargs to be passed to `bokeh.plotting.figure()` when
1299
+ instantiating the figure.
1300
+
1301
+ Returns
1302
+ -------
1303
+ output : bokeh.plotting.Figure instance
1304
+ Plot populated with a strip-histogram plot.
1305
+
1306
+ Notes
1307
+ -----
1308
+ .. Histograms are all normalized, as would be the case using the
1309
+ `density=True` kwargs of iqplot.histogram()`. This is necessary
1310
+ because there is no quantitative axis for the height of the
1311
+ histogram in a strip plot.
1312
+ """
1313
+ # Protect against mutability of dicts
1314
+ jitter_kwargs = copy.copy(jitter_kwargs)
1315
+ swarm_kwargs = copy.copy(swarm_kwargs)
1316
+ marker_kwargs = copy.copy(marker_kwargs)
1317
+ parcoord_kwargs = copy.copy(parcoord_kwargs)
1318
+ line_kwargs = copy.copy(line_kwargs)
1319
+ fill_kwargs = copy.copy(fill_kwargs)
1320
+
1321
+ # Set defaults
1322
+ if color_column is not None and color_column != cats:
1323
+ if line_kwargs is None:
1324
+ line_kwargs = {}
1325
+ if fill_kwargs is None:
1326
+ fill_kwargs = {}
1327
+ if "color" not in line_kwargs and "line_color" not in line_kwargs:
1328
+ line_kwargs["line_color"] = "gray"
1329
+ if "color" not in fill_kwargs and "fill_color" not in fill_kwargs:
1330
+ fill_kwargs["fill_color"] = "gray"
1331
+
1332
+ if style is None:
1333
+ if conf_int:
1334
+ style = "step"
1335
+ else:
1336
+ style = "step_filled"
1337
+
1338
+ if top_level == "histogram":
1339
+ p = strip(
1340
+ data=data,
1341
+ q=q,
1342
+ cats=cats,
1343
+ q_axis=q_axis,
1344
+ palette=palette,
1345
+ order=order,
1346
+ p=p,
1347
+ show_legend=show_legend,
1348
+ color_column=color_column,
1349
+ parcoord_column=parcoord_column,
1350
+ tooltips=tooltips,
1351
+ marker=marker,
1352
+ spread=spread,
1353
+ cat_grid=cat_grid,
1354
+ marker_kwargs=marker_kwargs,
1355
+ jitter_kwargs=jitter_kwargs,
1356
+ swarm_kwargs=swarm_kwargs,
1357
+ parcoord_kwargs=parcoord_kwargs,
1358
+ jitter=jitter,
1359
+ horizontal=horizontal,
1360
+ val=val,
1361
+ click_policy=click_policy,
1362
+ **kwargs,
1363
+ )
1364
+
1365
+ p = histogram(
1366
+ data=data,
1367
+ q=q,
1368
+ cats=cats,
1369
+ q_axis=q_axis,
1370
+ palette=palette,
1371
+ order=order,
1372
+ p=p,
1373
+ rug=False,
1374
+ show_legend=False,
1375
+ bins=bins,
1376
+ density=True,
1377
+ style=style,
1378
+ arrangement="stack",
1379
+ mirror=mirror,
1380
+ hist_height=hist_height,
1381
+ conf_int=conf_int,
1382
+ ptiles=ptiles,
1383
+ n_bs_reps=n_bs_reps,
1384
+ line_kwargs=line_kwargs,
1385
+ fill_kwargs=fill_kwargs,
1386
+ conf_int_kwargs=conf_int_kwargs,
1387
+ kind=kind,
1388
+ )
1389
+ elif top_level == "strip":
1390
+ p = histogram(
1391
+ data=data,
1392
+ q=q,
1393
+ cats=cats,
1394
+ q_axis=q_axis,
1395
+ palette=palette,
1396
+ order=order,
1397
+ p=p,
1398
+ rug=False,
1399
+ show_legend=False,
1400
+ bins=bins,
1401
+ density=True,
1402
+ style=style,
1403
+ arrangement="stack",
1404
+ mirror=mirror,
1405
+ hist_height=hist_height,
1406
+ conf_int=conf_int,
1407
+ ptiles=ptiles,
1408
+ n_bs_reps=n_bs_reps,
1409
+ line_kwargs=line_kwargs,
1410
+ fill_kwargs=fill_kwargs,
1411
+ conf_int_kwargs=conf_int_kwargs,
1412
+ kind=kind,
1413
+ **kwargs,
1414
+ )
1415
+
1416
+ p = strip(
1417
+ data=data,
1418
+ q=q,
1419
+ cats=cats,
1420
+ q_axis=q_axis,
1421
+ palette=palette,
1422
+ order=order,
1423
+ p=p,
1424
+ cat_grid=cat_grid,
1425
+ show_legend=show_legend,
1426
+ color_column=color_column,
1427
+ parcoord_column=parcoord_column,
1428
+ tooltips=tooltips,
1429
+ marker=marker,
1430
+ spread=spread,
1431
+ marker_kwargs=marker_kwargs,
1432
+ jitter_kwargs=jitter_kwargs,
1433
+ swarm_kwargs=swarm_kwargs,
1434
+ parcoord_kwargs=parcoord_kwargs,
1435
+ jitter=jitter,
1436
+ horizontal=horizontal,
1437
+ val=val,
1438
+ )
1439
+
1440
+ if not cat_grid:
1441
+ p.xgrid.grid_line_color = None
1442
+ else:
1443
+ raise RuntimeError(
1444
+ "Invalid `top_level`. Allowed values are 'histogram' and 'strip'."
1445
+ )
1446
+
1447
+ return p
1448
+
1449
+
1450
+ def _get_cat_range(df, grouped, order, color_column, q_axis):
1451
+ if order is None:
1452
+ if isinstance(list(grouped.groups.keys())[0], tuple):
1453
+ factors = tuple(
1454
+ [tuple([str(k) for k in key]) for key in grouped.groups.keys()]
1455
+ )
1456
+ else:
1457
+ factors = tuple([str(key) for key in grouped.groups.keys()])
1458
+ else:
1459
+ if isinstance(order[0], (list, tuple)):
1460
+ factors = tuple([tuple([str(k) for k in key]) for key in order])
1461
+ else:
1462
+ factors = tuple([str(entry) for entry in order])
1463
+
1464
+ if q_axis == "x":
1465
+ cat_range = bokeh.models.FactorRange(*(factors[::-1]))
1466
+ elif q_axis == "y":
1467
+ cat_range = bokeh.models.FactorRange(*factors)
1468
+
1469
+ if color_column is None:
1470
+ color_factors = factors
1471
+ elif _color_column_hexcodes(df, color_column):
1472
+ color_factors = "hex"
1473
+ else:
1474
+ color_factors = tuple(sorted(list(df[color_column].unique().astype(str))))
1475
+
1476
+ return cat_range, factors, color_factors
1477
+
1478
+
1479
+ def _color_column_hexcodes(df, color_column):
1480
+ """Return True if the color column consists of all hex codes."""
1481
+ try:
1482
+ return df[color_column].str.match(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").all()
1483
+ except:
1484
+ return False
1485
+
1486
+
1487
+ def _cat_figure(df, grouped, q, order, color_column, q_axis, kwargs):
1488
+ cat_range, factors, color_factors = _get_cat_range(
1489
+ df, grouped, order, color_column, q_axis
1490
+ )
1491
+
1492
+ kwargs = utils._fig_dimensions(kwargs)
1493
+
1494
+ if q_axis == "x":
1495
+ if "x_axis_label" not in kwargs:
1496
+ kwargs["x_axis_label"] = q
1497
+
1498
+ if "y_axis_type" in kwargs:
1499
+ warnings.warn("`y_axis_type` specified for categorical axis. Ignoring.")
1500
+ del kwargs["y_axis_type"]
1501
+
1502
+ kwargs["y_range"] = cat_range
1503
+ elif q_axis == "y":
1504
+ if "y_axis_label" not in kwargs:
1505
+ kwargs["y_axis_label"] = q
1506
+
1507
+ if "x_axis_type" in kwargs:
1508
+ warnings.warn("`x_axis_type` specified for categorical axis. Ignoring.")
1509
+ del kwargs["x_axis_type"]
1510
+
1511
+ kwargs["x_range"] = cat_range
1512
+
1513
+ return bokeh.plotting.figure(**kwargs), factors, color_factors
1514
+
1515
+
1516
+ def _cat_source_dict(df, cats, cols, color_column):
1517
+ cat_source, labels = utils._source_and_labels_from_cats(df, cats)
1518
+
1519
+ if isinstance(cols, (list, tuple, pd.Index)):
1520
+ source_dict = {col: list(df[col].values) for col in cols}
1521
+ else:
1522
+ source_dict = {cols: list(df[cols].values)}
1523
+
1524
+ source_dict["cat"] = cat_source
1525
+ if color_column in [None, "cat"]:
1526
+ source_dict["__label"] = labels
1527
+ else:
1528
+ source_dict["__label"] = list(df[color_column].astype(str).values)
1529
+ source_dict[color_column] = list(df[color_column].astype(str).values)
1530
+
1531
+ return source_dict
1532
+
1533
+
1534
+ def _parcoord_source(data, q, cats, q_axis, parcoord_column, factors):
1535
+ if not isinstance(cats, (list, tuple)):
1536
+ cats = [cats]
1537
+ tuple_factors = False
1538
+ else:
1539
+ tuple_factors = True
1540
+
1541
+ grouped_parcoord = data.groupby(parcoord_column)
1542
+ xs = []
1543
+ ys = []
1544
+ for t, g in grouped_parcoord:
1545
+ xy = []
1546
+ for _, r in g.iterrows():
1547
+ if tuple_factors:
1548
+ xy.append([tuple([r[cat] for cat in cats]), r[q]])
1549
+ else:
1550
+ xy.append([r[cats[0]], r[q]])
1551
+
1552
+ if len(xy) > 1:
1553
+ xy.sort(key=lambda a: factors.index(a[0]))
1554
+ xs_pc = []
1555
+ ys_pc = []
1556
+ for pair in xy:
1557
+ xs_pc.append(pair[0])
1558
+ ys_pc.append(pair[1])
1559
+
1560
+ if q_axis == "y":
1561
+ xs.append(xs_pc)
1562
+ ys.append(ys_pc)
1563
+ else:
1564
+ xs.append(ys_pc)
1565
+ ys.append(xs_pc)
1566
+
1567
+ return bokeh.models.ColumnDataSource(dict(xs=xs, ys=ys))
1568
+
1569
+
1570
+ def _outliers(data, min_data):
1571
+ if len(data) >= min_data:
1572
+ bottom, middle, top = np.percentile(data, [25, 50, 75])
1573
+ iqr = top - bottom
1574
+ outliers = data[(data > top + 1.5 * iqr) | (data < bottom - 1.5 * iqr)]
1575
+ return outliers
1576
+ else:
1577
+ return data
1578
+
1579
+
1580
+ def _box_and_whisker(data, min_data):
1581
+ if len(data) >= min_data:
1582
+ middle = data.median()
1583
+ bottom = data.quantile(0.25)
1584
+ top = data.quantile(0.75)
1585
+ iqr = top - bottom
1586
+ top_whisker = max(data[data <= top + 1.5 * iqr].max(), top)
1587
+ bottom_whisker = min(data[data >= bottom - 1.5 * iqr].min(), bottom)
1588
+ return pd.Series(
1589
+ {
1590
+ "middle": middle,
1591
+ "bottom": bottom,
1592
+ "top": top,
1593
+ "top_whisker": top_whisker,
1594
+ "bottom_whisker": bottom_whisker,
1595
+ }
1596
+ )
1597
+ else:
1598
+ return pd.Series(
1599
+ {
1600
+ "middle": np.nan,
1601
+ "bottom": np.nan,
1602
+ "top": np.nan,
1603
+ "top_whisker": np.nan,
1604
+ "bottom_whisker": np.nan,
1605
+ }
1606
+ )
1607
+
1608
+
1609
+ def _box_source(df, cats, q, cols, min_data):
1610
+ """Construct a data frame for making box plot."""
1611
+ # Need to reset index for use in slicing outliers
1612
+ df_source = df.reset_index(drop=True)
1613
+
1614
+ if cats is None:
1615
+ grouped = df_source
1616
+ else:
1617
+ grouped = df_source.groupby(cats, sort=False)
1618
+
1619
+ # Data frame for boxes and whiskers
1620
+ df_box = grouped[q].apply(_box_and_whisker, min_data).unstack().reset_index()
1621
+ df_box = df_box.dropna()
1622
+
1623
+ source_box = bokeh.models.ColumnDataSource(
1624
+ _cat_source_dict(
1625
+ df_box,
1626
+ cats,
1627
+ ["middle", "bottom", "top", "top_whisker", "bottom_whisker"],
1628
+ None,
1629
+ )
1630
+ )
1631
+
1632
+ # Data frame for outliers
1633
+ s_outliers = grouped[q].apply(_outliers, min_data)
1634
+
1635
+ # If no cat has enough data, just use everything as an "outlier"
1636
+ if len(s_outliers) == len(df_source):
1637
+ df_outliers = df_source.copy()
1638
+ inds = df_source.index
1639
+ else:
1640
+ df_outliers = s_outliers.reset_index()
1641
+ inds = s_outliers.index.get_level_values(-1)
1642
+
1643
+ df_outliers.index = inds
1644
+ df_outliers[cols] = df_source.loc[inds, cols]
1645
+
1646
+ source_outliers = bokeh.models.ColumnDataSource(
1647
+ _cat_source_dict(df_outliers, cats, cols, None)
1648
+ )
1649
+
1650
+ return source_box, source_outliers
1651
+
1652
+
1653
+ def _out_every_interval(y, intervals, epsilon=1e-6):
1654
+ """Check to see if a value `y` lies outside every interval in a
1655
+ list of 2-tuples `intervals`."""
1656
+ for interval in intervals:
1657
+ if y > interval[0] + epsilon and y < interval[1] - epsilon:
1658
+ return False
1659
+
1660
+ return True
1661
+
1662
+
1663
+ def _swarm_px(
1664
+ x,
1665
+ frame_width,
1666
+ r,
1667
+ x_range,
1668
+ max_y_px=np.inf,
1669
+ corral="gutter",
1670
+ priority="ascending",
1671
+ marker_pad_px=0,
1672
+ ):
1673
+ """Computes y-coordinates in pixel units for a swarm plot, where x
1674
+ is the quantitative axis.
1675
+
1676
+ Parameters
1677
+ ----------
1678
+ x : array_like
1679
+ Array of values of quantitative variable.
1680
+ frame_width : int or float
1681
+ Width of plot frame in pixels.
1682
+ r : float
1683
+ Radius of marker, which is typically the (marker size + 1) / 2,
1684
+ where the +1 is due to the standard line width for a marker of
1685
+ one pixel.
1686
+ x_range : list
1687
+ List of length 2, where the first entry is the lower limit of
1688
+ the quantitative axis and the second entry is the upper limit of
1689
+ the quantitative axis.
1690
+ max_y_px : float, default np.inf
1691
+ Maximum allowed displacement. Any points with computed y-values
1692
+ beyond this will be corralled.
1693
+ corral : str, default 'gutter'
1694
+ Either 'gutter' or 'wrap'. How to corral points beyond the
1695
+ maximum displacement.
1696
+ priority : str, default 'ascending'
1697
+ Sort order when determining which points get moved in the
1698
+ y-direction first. Either 'ascending' or 'descending'.
1699
+ marker_pad_px : int or float
1700
+ Gap between markers in units of pixels.
1701
+
1702
+ Returns
1703
+ -------
1704
+ y : array_like
1705
+ Array of y-values in units of pixels.
1706
+ n_overrun : int
1707
+ Number of data points that overrun max_y_px.
1708
+ """
1709
+ # Sort x according to priority
1710
+ if priority == "ascending":
1711
+ inds = [i[0] for i in sorted(enumerate(x), key=lambda x: x[1])]
1712
+ elif priority == "descending":
1713
+ inds = [i[0] for i in sorted(enumerate(x), key=lambda x: -x[1])]
1714
+ elif priority == "random":
1715
+ raise NotImplementedError("'random' priority is not yet implemented.")
1716
+ else:
1717
+ raise NotImplementedError("Custom `priority` not yet implemented.")
1718
+
1719
+ x_pixels = (x[inds] - x_range[0]) * frame_width / (x_range[1] - x_range[0])
1720
+ y_pixels = np.inf * np.ones_like(x_pixels)
1721
+
1722
+ for i in range(len(x_pixels)):
1723
+ intervals = []
1724
+
1725
+ # Scan points to the right
1726
+ for j in range(i + 1, len(x_pixels)):
1727
+ dist = abs(x_pixels[i] - x_pixels[j])
1728
+ if dist > 2 * r:
1729
+ if priority in ["ascending", "descending"]:
1730
+ break
1731
+ else:
1732
+ continue
1733
+ if y_pixels[j] < np.inf:
1734
+ offset = np.sqrt(4 * r**2 - dist**2) + marker_pad_px
1735
+ intervals.append([y_pixels[j] - offset, y_pixels[j] + offset])
1736
+
1737
+ # Scan points to the left
1738
+ for j in range(i - 1, -1, -1):
1739
+ dist = abs(x_pixels[i] - x_pixels[j])
1740
+ if dist > 2 * r:
1741
+ if priority in ["ascending", "descending"]:
1742
+ break
1743
+ else:
1744
+ continue
1745
+ if y_pixels[j] < np.inf:
1746
+ offset = np.sqrt(4 * r**2 - dist**2) + marker_pad_px
1747
+ intervals.append([y_pixels[j] - offset, y_pixels[j] + offset])
1748
+
1749
+ # Any y-position must be outside all intervals and should be at the edge of one of the intervals
1750
+ # Need to find the first candidate that satisfies this
1751
+ y_cand = 0
1752
+ if len(intervals) > 0:
1753
+ candidates = sorted(np.array(intervals).flatten(), key=abs)
1754
+ for cand in candidates:
1755
+ if _out_every_interval(cand, intervals):
1756
+ y_cand = cand
1757
+ break
1758
+ y_pixels[i] = y_cand
1759
+
1760
+ # Clean up points landing too far out
1761
+ n_overrun = 0
1762
+ for i in range(len(x)):
1763
+ if abs(y_pixels[i]) > max_y_px:
1764
+ n_overrun += 1
1765
+ if corral == "gutter":
1766
+ y_pixels[i] = np.sign(y_pixels[i]) * max_y_px
1767
+ elif corral == "wrap":
1768
+ y_pixels[i] = np.sign(y_pixels[i]) * 2 * max_y_px - y_pixels[i]
1769
+
1770
+ # Build output for y in pixels
1771
+ y_out = np.empty_like(x)
1772
+ for i in range(len(x)):
1773
+ y_out[inds[i]] = y_pixels[i]
1774
+
1775
+ return y_out, n_overrun
1776
+
1777
+
1778
+ def _swarm(
1779
+ x,
1780
+ p,
1781
+ r,
1782
+ x_range,
1783
+ q_axis,
1784
+ log_q,
1785
+ corral="gutter",
1786
+ priority="ascending",
1787
+ marker_pad_px=0,
1788
+ ):
1789
+ if q_axis == "x":
1790
+ extra_padding = 0
1791
+ if isinstance(p.y_range.factors[0], tuple):
1792
+ if len(p.y_range.factors[0]) >= 2:
1793
+ extra_padding += p.y_range.group_padding
1794
+ if len(p.y_range.factors[0]) > 2:
1795
+ extra_padding += (
1796
+ len(p.y_range.factors[0]) - 2
1797
+ ) * p.y_range.subgroup_padding
1798
+ h = p.frame_height
1799
+ w = p.frame_width
1800
+ n_factors = len(p.y_range.factors) + extra_padding
1801
+ else:
1802
+ extra_padding = 0
1803
+ if isinstance(p.x_range.factors[0], tuple):
1804
+ if len(p.x_range.factors[0]) >= 2:
1805
+ extra_padding += p.x_range.group_padding
1806
+ if len(p.x_range.factors[0]) > 2:
1807
+ extra_padding += (
1808
+ len(p.x_range.factors[0]) - 2
1809
+ ) * p.x_range.subgroup_padding
1810
+ w = p.frame_height
1811
+ h = p.frame_width
1812
+ n_factors = len(p.x_range.factors) + extra_padding
1813
+
1814
+ max_y_px = h / n_factors / 2 - 2 * r
1815
+
1816
+ # Make adjustments in case we have log axes
1817
+ xvals = np.array(x)
1818
+ if log_q:
1819
+ xvals = np.log10(xvals)
1820
+
1821
+ y_pixels, n_overrun = _swarm_px(
1822
+ xvals,
1823
+ w,
1824
+ r,
1825
+ x_range,
1826
+ max_y_px=max_y_px,
1827
+ corral=corral,
1828
+ priority=priority,
1829
+ marker_pad_px=marker_pad_px,
1830
+ )
1831
+
1832
+ if n_overrun > 0:
1833
+ hw = "height" if q_axis == "x" else "width"
1834
+ warnings.warn(
1835
+ f"{n_overrun} data points exceed maximum {hw}. Consider using spread='jitter' or increasing the frame {hw}."
1836
+ )
1837
+
1838
+ return y_pixels / h * n_factors