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/utils.py ADDED
@@ -0,0 +1,516 @@
1
+ """Utility functions for parsing inputs."""
2
+
3
+ import copy
4
+ import warnings
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ import bokeh.core.enums
10
+
11
+
12
+ def _fig_dimensions(kwargs):
13
+ if (
14
+ "width" not in kwargs
15
+ and "plot_width" not in kwargs
16
+ and "frame_width" not in kwargs
17
+ ):
18
+ kwargs["frame_width"] = 375
19
+ if (
20
+ "height" not in kwargs
21
+ and "plot_height" not in kwargs
22
+ and "frame_height" not in kwargs
23
+ ):
24
+ kwargs["frame_height"] = 275
25
+
26
+ if "toolbar_location" not in kwargs:
27
+ kwargs["toolbar_location"] = "above"
28
+
29
+ return kwargs
30
+
31
+
32
+ def _parse_deprecations(
33
+ q,
34
+ q_axis,
35
+ val,
36
+ horizontal,
37
+ horiz_q_axis,
38
+ click_policy,
39
+ legend_click_policy,
40
+ conf_int_kwargs,
41
+ fill_kwargs,
42
+ ):
43
+ if q_axis not in ("x", "y"):
44
+ raise RuntimeError("Invalid `q_axis`. Must be 'x' or 'y'.")
45
+
46
+ if horizontal is not None:
47
+ if (horizontal and q_axis != horiz_q_axis) or (
48
+ not horizontal and q_axis == horiz_q_axis
49
+ ):
50
+ raise RuntimeError(
51
+ "`horizontal` and `q_axis` kwargs in disagreement. "
52
+ "Use `q_axis`; `horizontal` is deprecated."
53
+ )
54
+
55
+ warnings.warn(f"`horizontal` is deprecated. Use `q_axis`.", DeprecationWarning)
56
+
57
+ if val is not None:
58
+ if q is None:
59
+ q = val
60
+ elif q != val:
61
+ raise RuntimeError(
62
+ "`val` and `q` in disagreement. Use `q`; `val` is deprecated."
63
+ )
64
+
65
+ warnings.warn(f"`val` is deprecated. Use `q`. Using q={q}.", DeprecationWarning)
66
+
67
+ if click_policy is not None:
68
+ if legend_click_policy is None:
69
+ legend_click_policy = click_policy
70
+ elif click_policy != legend_click_policy:
71
+ raise RuntimeError(
72
+ "`click_policy` and `legend_click_policy` in disagreement. Use `legend_click_policy`; `click_policy` is deprecated."
73
+ )
74
+
75
+ warnings.warn(
76
+ f"`click_policy` is deprecated. Use `legend_click_policy`. Using legend_click_policy='{legend_click_policy}'."
77
+ )
78
+
79
+ if conf_int_kwargs is not None:
80
+ if fill_kwargs is None:
81
+ fill_kwargs = copy.copy(conf_int_kwargs)
82
+ elif conf_int_kwargs != fill_kwargs:
83
+ raise RuntimeError(
84
+ "`fill_kwargs` and `conf_int_kwargs` in disagreement. Use `fill_kwargs`; `conf_int_kwargs` is deprecated."
85
+ )
86
+
87
+ warnings.warn(f"`conf_int_kwargs is deprecated. Use `fill_kwargs`.")
88
+
89
+ return q, legend_click_policy, fill_kwargs
90
+
91
+
92
+ def _check_cats_none(cats, order, show_legend=False, legend_label=None):
93
+ """Check for kwargs that are disallowed when `cats` is None.
94
+
95
+ Must be called before `_data_cats()`, which replaces a `cats` of
96
+ None with a dummy categorical variable. `show_legend` and
97
+ `legend_label` are omitted for plots whose legend is not built from
98
+ `cats`, in which case only `order` is checked.
99
+ """
100
+ if cats is None:
101
+ if show_legend and legend_label is None:
102
+ raise RuntimeError(
103
+ "No legend to show if `cats` and `legend_label` are None."
104
+ )
105
+ if order is not None:
106
+ raise RuntimeError("No `order` is allowed if `cats` is None.")
107
+
108
+
109
+ def _data_cats(data, q, cats, show_legend, legend_label):
110
+ if "xarray.core.dataarray.DataArray" in str(type(data)):
111
+ if q is None:
112
+ if data.name is None:
113
+ q = "x"
114
+ else:
115
+ q = data.name
116
+ data = pd.DataFrame({q: data.squeeze().values})
117
+ elif isinstance(data, np.ndarray):
118
+ if q is None:
119
+ q = "x"
120
+ data = pd.DataFrame({q: data.squeeze()})
121
+ if cats is not None:
122
+ raise RuntimeError("If `data` is a Numpy array, `cats` must be None.")
123
+ elif "polars.dataframe.frame.DataFrame" in str(type(data)):
124
+ # For now, we just convert to Pandas. We will add functionality
125
+ # to work with polars data frames to take advantage of their
126
+ # performance in the future.
127
+ data = data.to_pandas()
128
+ elif "polars.series.series.Series" in str(type(data)) or isinstance(
129
+ data, pd.Series
130
+ ):
131
+ # For now, just convert to Pandas series if it's a Polars series
132
+ if "polars.series.series.Series" in str(type(data)):
133
+ data = data.to_pandas()
134
+ if q is None:
135
+ if data.name is None:
136
+ q = "x"
137
+ else:
138
+ q = data.name
139
+ data = pd.DataFrame({q: data})
140
+ if cats is not None:
141
+ raise RuntimeError("If `data` is a Pandas series, `cats` must be None.")
142
+ elif not isinstance(data, pd.DataFrame):
143
+ raise RuntimeError(
144
+ f"Data type {type(data)} for argument `data` is not supported."
145
+ )
146
+
147
+ # Make a copy of the data frame
148
+ data = data.copy()
149
+
150
+ # A tuple or Index of column names is converted to a list. Pandas
151
+ # takes a tuple to be a single column name and an Index to be a
152
+ # grouping vector, neither of which is meant here.
153
+ if isinstance(cats, (tuple, pd.Index)):
154
+ cats = list(cats)
155
+
156
+ if cats is None:
157
+ if legend_label is None:
158
+ data["__dummy_cat"] = " "
159
+ show_legend = False
160
+ else:
161
+ data["__dummy_cat"] = legend_label
162
+ cats = "__dummy_cat"
163
+
164
+ # Ensure the categorical columns are present and have data type str
165
+ for cat in cats if isinstance(cats, (list, tuple)) else [cats]:
166
+ if cat not in data.columns:
167
+ raise RuntimeError(f"{cat} is not a column in the inputted data frame")
168
+ data[cat] = data[cat].astype(str)
169
+ # data.loc[:, cat] = data.loc[:, cat].astype(str)
170
+
171
+ return data, q, cats, show_legend
172
+
173
+
174
+ def _order_to_str(order):
175
+ """Convert entries in `order` to strings."""
176
+ if order is None:
177
+ return order
178
+
179
+ order = list(order)
180
+
181
+ for i, item in enumerate(order):
182
+ if not isinstance(item, (list, tuple, np.ndarray)):
183
+ order[i] = str(order[i])
184
+ else:
185
+ order[i] = tuple([str(x) for x in item])
186
+
187
+ return order
188
+
189
+
190
+ def _fill_between(p, x1=None, y1=None, x2=None, y2=None, **kwargs):
191
+ """
192
+ Create a filled region between two curves.
193
+
194
+ Parameters
195
+ ----------
196
+ p : bokeh.plotting.Figure instance
197
+ Figure to be populated.
198
+ x1 : array_like
199
+ Array of x-values for first curve
200
+ y1 : array_like
201
+ Array of y-values for first curve
202
+ x2 : array_like
203
+ Array of x-values for second curve
204
+ y2 : array_like
205
+ Array of y-values for second curve
206
+ kwargs
207
+ Any kwargs passed to p.patch.
208
+
209
+ Returns
210
+ -------
211
+ output : bokeh.plotting.Figure instance
212
+ Plot populated with fill-between.
213
+
214
+ """
215
+ x = list(x1) + list(x2)[::-1]
216
+ y = list(y1) + list(y2)[::-1]
217
+ patch = p.patch(x=x, y=y, **kwargs)
218
+
219
+ # Old way; only works for Numpy arrays
220
+ # patch = p.patch(
221
+ # x=np.concatenate((x1, x2[::-1])), y=np.concatenate((y1, y2[::-1])), **kwargs
222
+ # )
223
+
224
+ return p, patch
225
+
226
+
227
+ def _check_marker_kwargs(marker_kwargs):
228
+ if marker_kwargs is None:
229
+ marker_kwargs = {}
230
+ elif not isinstance(marker_kwargs, dict):
231
+ raise RuntimeError("`marker_kwargs` must be a dict.")
232
+
233
+ if "marker" in marker_kwargs:
234
+ raise RuntimeError(
235
+ "'marker' cannot be a key in `marker_kwargs`. Specify using the `marker` kwargs instead."
236
+ )
237
+ if "source" in marker_kwargs:
238
+ raise RuntimeError("'source' cannot be a key in `marker_kwargs`.")
239
+ if "x" in marker_kwargs:
240
+ raise RuntimeError("'x' cannot be a key in `marker_kwargs`.")
241
+ if "y" in marker_kwargs:
242
+ raise RuntimeError("'y' cannot be a key in `marker_kwargs`.")
243
+ if "cat" in marker_kwargs:
244
+ raise RuntimeError("'cat' cannot be a key in `marker_kwargs`.")
245
+ if "legend" in marker_kwargs:
246
+ raise RuntimeError("'legend' cannot be a key in `marker_kwargs`.")
247
+ if "legend_label" in marker_kwargs:
248
+ raise RuntimeError("'legend_label' cannot be a key in `marker_kwargs`.")
249
+
250
+ return marker_kwargs
251
+
252
+
253
+ def _check_marker(marker):
254
+ if marker not in bokeh.core.enums.MarkerType:
255
+ err_str = (
256
+ f"{marker} is an invalid marker specification. Acceptable values are ["
257
+ )
258
+ for marker in list(bokeh.core.enums.MarkerType)[:-1]:
259
+ err_str += f"{marker}, "
260
+ err_str += list(bokeh.core.enums.MarkerType)[-1] + "]."
261
+
262
+ raise RuntimeError(err_str)
263
+
264
+ return marker
265
+
266
+
267
+ def _source_and_labels_from_cats(df, cats):
268
+ if isinstance(cats, (list, tuple)):
269
+ cat_source = list(zip(*tuple([df[cat].astype(str) for cat in cats])))
270
+ return cat_source, [", ".join(cat) for cat in cat_source]
271
+ else:
272
+ cat_source = list(df[cats].astype(str).values)
273
+ return cat_source, cat_source
274
+
275
+
276
+ def _tooltip_cols(tooltips):
277
+ if tooltips is None:
278
+ return []
279
+ if not isinstance(tooltips, (list, tuple)):
280
+ raise RuntimeError("`tooltips` must be a list or tuple of two-tuples.")
281
+
282
+ cols = []
283
+ for tip in tooltips:
284
+ if not isinstance(tip, (list, tuple)) or len(tip) != 2:
285
+ raise RuntimeError("Invalid tooltip.")
286
+ if tip[1][0] == "@":
287
+ if tip[1][1] == "{":
288
+ cols.append(tip[1][2 : tip[1].find("}")])
289
+ elif "{" in tip[1]:
290
+ cols.append(tip[1][1 : tip[1].find("{")])
291
+ else:
292
+ cols.append(tip[1][1:])
293
+
294
+ return cols
295
+
296
+
297
+ def _cols_to_keep(cats, q, color_column, tooltips):
298
+ cols = _tooltip_cols(tooltips)
299
+ cols += [q]
300
+
301
+ if isinstance(cats, (list, tuple)):
302
+ cols += list(cats)
303
+ else:
304
+ cols += [cats]
305
+
306
+ if color_column is not None:
307
+ cols += [color_column]
308
+
309
+ return list(set(cols))
310
+
311
+
312
+ def _check_cat_input(
313
+ df, cats, q, color_column, parcoord_column, tooltips, palette, order, kwargs
314
+ ):
315
+ if df is None:
316
+ raise RuntimeError("`df` argument must be provided.")
317
+ if cats is None:
318
+ raise RuntimeError("`cats` argument must be provided.")
319
+ if q is None:
320
+ raise RuntimeError("`q` argument must be provided.")
321
+
322
+ if not isinstance(palette, (list, tuple, str)):
323
+ raise RuntimeError("`palette` must be a list, tuple or string.")
324
+
325
+ if q not in df.columns:
326
+ raise RuntimeError(f"{q} is not a column in the inputted data frame")
327
+
328
+ cats_array = isinstance(cats, (list, tuple))
329
+ if cats_array and len(cats) == 1:
330
+ cats = cats[0]
331
+ cats_array = False
332
+
333
+ if cats_array:
334
+ for cat in cats:
335
+ if cat not in df.columns:
336
+ raise RuntimeError(f"{cat} is not a column in the inputted data frame")
337
+ else:
338
+ if isinstance(cats, tuple):
339
+ raise RuntimeError(
340
+ "Cannot have tuples as data frame column names if there is only one categorical variable."
341
+ )
342
+ if cats not in df.columns:
343
+ raise RuntimeError(f"{cats} is not a column in the inputted data frame")
344
+
345
+ if color_column is not None and color_column not in df.columns:
346
+ raise RuntimeError(f"{color_column} is not a column in the inputted data frame")
347
+
348
+ cols = _cols_to_keep(cats, q, color_column, tooltips)
349
+
350
+ for col in cols:
351
+ if col not in df.columns:
352
+ raise RuntimeError(f"{col} is not a column in the inputted data frame")
353
+
354
+ bad_kwargs = ["x", "y", "source", "cat", "legend"]
355
+ if kwargs is not None and any([key in kwargs for key in bad_kwargs]):
356
+ raise RuntimeError(", ".join(bad_kwargs) + " are not allowed kwargs.")
357
+
358
+ if q == "cat":
359
+ raise RuntimeError("`'cat'` cannot be used as `q`.")
360
+
361
+ if q == "__label" or (cats == "__label" or (cats_array and "__label" in cats)):
362
+ raise RuntimeError("'__label' cannot be used for `q` or `cats`.")
363
+
364
+ if order is not None:
365
+ grouped = df.groupby(cats)
366
+ if grouped.ngroups > len(order):
367
+ raise RuntimeError(
368
+ "`order` must have at least as many elements as the number of unique groups in `cats`."
369
+ )
370
+ for entry in order:
371
+ if entry not in grouped.groups.keys():
372
+ raise RuntimeError(
373
+ f"Entry {entry} in `order` but not present as a group in the inputted data."
374
+ )
375
+
376
+ if parcoord_column is not None:
377
+ if parcoord_column not in df.columns:
378
+ raise RuntimeError(
379
+ f"{parcoord_column} is not a column in the inputted data frame"
380
+ )
381
+ if cats == "__dummy_cat":
382
+ raise RuntimeError(
383
+ "`cats` must be provided in `parcoord_column` is provided."
384
+ )
385
+ grouped = df.groupby(parcoord_column)
386
+
387
+ return cats, cols
388
+
389
+
390
+ def _specific_fill_and_color_kwargs(kwargs, kwarg_type):
391
+ if "color" in kwargs:
392
+ if "fill_color" in kwargs or "line_color" in kwargs:
393
+ raise RuntimeError(
394
+ "Specifing both color and fill_color or line_color in a set of kwargs is ambiguous."
395
+ )
396
+
397
+ kwargs["line_color"] = kwargs["color"]
398
+
399
+ if kwarg_type != "line":
400
+ kwargs["fill_color"] = kwargs["color"]
401
+
402
+ del kwargs["color"]
403
+
404
+ if "alpha" in kwargs:
405
+ if "fill_alpha" in kwargs or "line_alpha" in kwargs:
406
+ raise RuntimeError(
407
+ "Specifing both alpha and fill_alpha or line_alpha in a set of kwargs is ambiguous."
408
+ )
409
+
410
+ if kwarg_type != "fill":
411
+ kwargs["line_alpha"] = kwargs["alpha"]
412
+
413
+ if kwarg_type != "line":
414
+ kwargs["fill_alpha"] = kwargs["alpha"]
415
+
416
+ del kwargs["alpha"]
417
+
418
+ return kwargs
419
+
420
+
421
+ def _convert_data(data, inf_ok=False, min_len=1):
422
+ """
423
+ Convert inputted 1D data set into NumPy array of floats.
424
+ All NaNs are dropped.
425
+
426
+ Parameters
427
+ ----------
428
+ data : int, float, or array_like
429
+ Input data, to be converted.
430
+ inf_ok : bool, default False
431
+ If True, np.inf values are allowed in the arrays.
432
+ min_len : int, default 1
433
+ Minimum length of array.
434
+
435
+ Returns
436
+ -------
437
+ output : ndarray
438
+ `data` as a one-dimensional NumPy array, dtype float.
439
+ """
440
+ # If it's scalar, convert to array
441
+ if np.isscalar(data):
442
+ data = np.array([data], dtype=np.float64)
443
+
444
+ # Convert data to NumPy array
445
+ data = np.array(data, dtype=np.float64)
446
+
447
+ # Make sure it is 1D
448
+ if len(data.shape) != 1:
449
+ raise RuntimeError("Input must be a 1D array or Pandas series.")
450
+
451
+ # Remove NaNs
452
+ data = data[~np.isnan(data)]
453
+
454
+ # Check for infinite entries
455
+ if not inf_ok and np.isinf(data).any():
456
+ raise RuntimeError("All entries must be finite.")
457
+
458
+ # Check for minimal length
459
+ if len(data) < min_len:
460
+ raise RuntimeError(
461
+ "Array must have at least {0:d} non-NaN entries.".format(min_len)
462
+ )
463
+
464
+ return data
465
+
466
+
467
+ def _edge_value_given(p_edge_value):
468
+
469
+ ret_val = True
470
+
471
+ if p_edge_value is None:
472
+ ret_val = False
473
+ else:
474
+ try:
475
+ if np.isnan(p_edge_value):
476
+ ret_val = False
477
+ else:
478
+ ret_val = True
479
+ except:
480
+ ret_val = True
481
+
482
+ return ret_val
483
+
484
+
485
+ def _range_specified(axis_range):
486
+ """
487
+ Missing x_range and y_range start and end values are None in
488
+ Bokeh 2.x and np.nan in Bokeh 3.x. This checks to see if the start
489
+ and end attributes of a Range1d instance are None or nan and returns
490
+ True if not.
491
+ """
492
+ return _edge_value_given(axis_range.start), _edge_value_given(axis_range.end)
493
+
494
+
495
+ def _dummy_jit(*args, **kwargs):
496
+ """Dummy wrapper for jitting if numba not applicable."""
497
+
498
+ def wrapper(f):
499
+ return f
500
+
501
+ def marker(*args, **kwargs):
502
+ return marker
503
+
504
+ if (
505
+ len(args) > 0
506
+ and (args[0] is marker or not callable(args[0]))
507
+ or len(kwargs) > 0
508
+ ):
509
+ # @jit(int32(int32, int32)), @jit(signature="void(int32)")
510
+ return wrapper
511
+ elif len(args) == 0:
512
+ # @jit()
513
+ return wrapper
514
+ else:
515
+ # @jit
516
+ return args[0]
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.5
2
+ Name: iqplot
3
+ Version: 0.3.8
4
+ Summary: Generate Bokeh plots for data sets with one quantitative variable.
5
+ Project-URL: Homepage, https://github.com/justinbois/iqplot
6
+ Project-URL: Documentation, http://iqplot.github.io/
7
+ Project-URL: Repository, https://github.com/justinbois/iqplot
8
+ Project-URL: Issues, https://github.com/justinbois/iqplot/issues
9
+ Author-email: Justin Bois <bois@caltech.edu>
10
+ Maintainer-email: Justin Bois <bois@caltech.edu>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: bokeh,box plot,ecdf,histogram,plotting,visualization
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: bokeh>=3.0
25
+ Requires-Dist: colorcet
26
+ Requires-Dist: numpy
27
+ Requires-Dist: pandas
28
+ Provides-Extra: data
29
+ Requires-Dist: polars; extra == 'data'
30
+ Requires-Dist: xarray; extra == 'data'
31
+ Provides-Extra: speedups
32
+ Requires-Dist: numba; extra == 'speedups'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # iqplot
36
+
37
+ [![DOI](https://data.caltech.edu/badge/DOI/10.22002/D1.20286.svg)](https://doi.org/10.22002/D1.20286)
38
+
39
+ A utility to use Bokeh to generate plots for data sets containing one quantitative variable and an arbitrary many categorical variables. It generates strip plots, box plots, histograms, and ECDFs.
40
+
41
+ Read the [documentation](http://iqplot.github.io/) for details.
@@ -0,0 +1,8 @@
1
+ iqplot/__init__.py,sha256=8FGWpx8kin9c9ZYyEdyWrhQh3L_RB_9S1Y5G0DmVCVs,186
2
+ iqplot/cat.py,sha256=A-skxz_LmnXPqWo-mc8y8tW_sQZu3cYM2o8PI5MuDgY,66255
3
+ iqplot/dist.py,sha256=yuFl97IxcuhdnCQE3pG_AwLm4rkEZul5atgbDWWBjAY,96684
4
+ iqplot/utils.py,sha256=_SkYNlna-XDrPzcmWuN0h8FUbw5VJj58GqOK4U6PemA,16040
5
+ iqplot-0.3.8.dist-info/METADATA,sha256=ciPIjiBStfdatwvSvmtZBiT330SR_YPEPrwEBqb7trM,1718
6
+ iqplot-0.3.8.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
7
+ iqplot-0.3.8.dist-info/licenses/LICENSE,sha256=odDJ-SW1-H_SrEZaQx1g7hqC4aaujIVEhPOcH93C8Jo,1068
8
+ iqplot-0.3.8.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Justin Bois
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.