dysonsphere 0.4.0__tar.gz

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.
Files changed (39) hide show
  1. dysonsphere-0.4.0/.github/workflows/pages.yml +28 -0
  2. dysonsphere-0.4.0/.github/workflows/publish.yml +25 -0
  3. dysonsphere-0.4.0/.gitignore +57 -0
  4. dysonsphere-0.4.0/.python-version +1 -0
  5. dysonsphere-0.4.0/LICENSE +21 -0
  6. dysonsphere-0.4.0/PKG-INFO +436 -0
  7. dysonsphere-0.4.0/README.md +394 -0
  8. dysonsphere-0.4.0/docs/.nojekyll +0 -0
  9. dysonsphere-0.4.0/docs/index.html +42 -0
  10. dysonsphere-0.4.0/docs/thumbnail_light.png +0 -0
  11. dysonsphere-0.4.0/dysonsphere/__init__.py +6 -0
  12. dysonsphere-0.4.0/dysonsphere/layers.py +651 -0
  13. dysonsphere-0.4.0/dysonsphere/palettes.py +3016 -0
  14. dysonsphere-0.4.0/dysonsphere/theme.py +388 -0
  15. dysonsphere-0.4.0/dysonsphere/transforms.py +226 -0
  16. dysonsphere-0.4.0/pyproject.toml +48 -0
  17. dysonsphere-0.4.0/scripts/build/build_gallery.py +676 -0
  18. dysonsphere-0.4.0/scripts/build/build_palettes.py +561 -0
  19. dysonsphere-0.4.0/scripts/build/build_swatches_for_illustrator.py +60 -0
  20. dysonsphere-0.4.0/scripts/build/build_thumbnail.py +209 -0
  21. dysonsphere-0.4.0/scripts/import_palettes_to_illustrator.jsx +2943 -0
  22. dysonsphere-0.4.0/scripts/plot/area_chart.py +46 -0
  23. dysonsphere-0.4.0/scripts/plot/area_chart_gradient.py +54 -0
  24. dysonsphere-0.4.0/scripts/plot/boxplot.py +88 -0
  25. dysonsphere-0.4.0/scripts/plot/bubble.py +47 -0
  26. dysonsphere-0.4.0/scripts/plot/heatmap.py +34 -0
  27. dysonsphere-0.4.0/scripts/plot/histogram.py +39 -0
  28. dysonsphere-0.4.0/scripts/plot/line.py +34 -0
  29. dysonsphere-0.4.0/scripts/plot/p-value.py +62 -0
  30. dysonsphere-0.4.0/scripts/plot/paired.py +58 -0
  31. dysonsphere-0.4.0/scripts/plot/palette_test.py +152 -0
  32. dysonsphere-0.4.0/scripts/plot/ridgeplot.py +88 -0
  33. dysonsphere-0.4.0/scripts/plot/scatter.py +30 -0
  34. dysonsphere-0.4.0/scripts/plot/sequences.py +156 -0
  35. dysonsphere-0.4.0/scripts/plot/stacked_bar_chart.py +45 -0
  36. dysonsphere-0.4.0/scripts/plot/strip.py +32 -0
  37. dysonsphere-0.4.0/scripts/plot/violin.py +39 -0
  38. dysonsphere-0.4.0/scripts/plot/volcano.py +63 -0
  39. dysonsphere-0.4.0/uv.lock +570 -0
@@ -0,0 +1,28 @@
1
+ name: Deploy to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ environment:
12
+ name: github-pages
13
+ url: ${{ steps.deploy.outputs.page_url }}
14
+ permissions:
15
+ pages: write
16
+ id-token: write
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Upload pages artifact
22
+ uses: actions/upload-pages-artifact@v3
23
+ with:
24
+ path: docs/
25
+
26
+ - name: Deploy to GitHub Pages
27
+ id: deploy
28
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,25 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Build
22
+ run: uv build
23
+
24
+ - name: Publish
25
+ run: uv publish
@@ -0,0 +1,57 @@
1
+ # Compiled source #
2
+ ###################
3
+ *.com
4
+ *.class
5
+ *.dll
6
+ *.exe
7
+ *.o
8
+ *.so
9
+
10
+ # Logs and databases #
11
+ ######################
12
+ *.log
13
+ *.sql
14
+ *.sqlite
15
+
16
+ # Cache files #
17
+ ###############
18
+ __pycache__
19
+ *.pyc
20
+
21
+ # OS generated files #
22
+ ######################
23
+ .DS_Store
24
+ .DS_Store?
25
+ ._*
26
+ .Spotlight-V100
27
+ .Trashes
28
+ ehthumbs.db
29
+ Thumbs.db
30
+
31
+ # Notes / scratchpads #
32
+ #########
33
+ *rule*.md
34
+ *thoughts.md
35
+ *notes.md
36
+ *planning.md
37
+ *plans.md
38
+ *scratch*
39
+
40
+ # Virtual environments #
41
+ ########################
42
+ .venv
43
+ *venv
44
+ pipenv
45
+ *conda*
46
+
47
+ # Raw data files #
48
+ ##################
49
+ *.tif
50
+ *.tiff
51
+ *.nd2
52
+
53
+ # Project-specific #
54
+ *.png
55
+ *.svg
56
+ *.json
57
+ !docs/thumbnail_light.png
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dkkung
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.
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: dysonsphere
3
+ Version: 0.4.0
4
+ Summary: An Altair configuration wrapper with perceptually uniform palettes and chart utilities for publication-ready figures.
5
+ Project-URL: Repository, https://github.com/dkkung/dysonsphere
6
+ Author-email: dkkung <128324842+dkkung@users.noreply.github.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 dkkung
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: altair,color,oklab,palette,theme,vega-lite,visualization
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Science/Research
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Topic :: Scientific/Engineering :: Visualization
36
+ Requires-Python: >=3.11
37
+ Requires-Dist: altair>=5.5.0
38
+ Requires-Dist: numpy>=1.26.0
39
+ Requires-Dist: polars>=1.19.0
40
+ Requires-Dist: scipy>=1.11.0
41
+ Description-Content-Type: text/markdown
42
+
43
+ # dysonsphere
44
+
45
+ An Altair configuration wrapper with perceptually uniform palettes and chart utilities for publication-ready figures.
46
+
47
+ *This is a personal project under active development, so there may be breaking changes between minor versions.*
48
+
49
+ ![thumbnail](https://raw.githubusercontent.com/dkkung/dysonsphere/main/docs/thumbnail_light.png)
50
+
51
+ ## Installation
52
+
53
+ ```sh
54
+ # uv
55
+ uv pip install dysonsphere
56
+
57
+ # pip
58
+ pip install dysonsphere
59
+ ```
60
+
61
+ Requires Python 3.11+. Dependencies: `altair`, `numpy`, `polars`, `scipy`.
62
+
63
+ ---
64
+
65
+ ## Quick start
66
+
67
+ ```python
68
+ import altair as alt
69
+ import polars as pl
70
+ import dysonsphere as theme # or: import dysonsphere
71
+
72
+ theme.options(chartWidth=300, chartHeight=200)
73
+
74
+ chart = (
75
+ alt.Chart(df)
76
+ .mark_point()
77
+ .encode(
78
+ x=alt.X("x:Q"),
79
+ y=alt.Y("y:Q"),
80
+ color=alt.Color("y:Q", scale=alt.Scale(range=theme.palette("blues"))),
81
+ )
82
+ )
83
+
84
+ theme.save(chart, "plots/myplot")
85
+ # writes: plots/myplot_light.png, plots/myplot_light.svg
86
+ # plots/myplot_dark.png, plots/myplot_dark.svg
87
+ # plots/myplot_vegalite.json
88
+ ```
89
+
90
+ ---
91
+
92
+ ## dysonsphere.options()
93
+
94
+ **Call before building any Altair charts to configure global theme defaults.**
95
+
96
+ ```python
97
+ theme.options() # apply defaults
98
+
99
+ theme.options( # custom configuration
100
+ chartWidth=400,
101
+ chartHeight=250,
102
+ fontSize=8,
103
+ grid=True,
104
+ palette="blues",
105
+ )
106
+ ```
107
+
108
+ | Parameter | Default | Description |
109
+ |---|---|---|
110
+ | `angledX` | `False` | Angle x-axis labels 45° |
111
+ | `axisOffset` | `tickSize` | Distance between axis line and data area |
112
+ | `axisWidth` | `0.25` | Stroke width of axes, ticks, and rules |
113
+ | `bandPadding` | `0.1` | Inner and outer padding for ordinal bands |
114
+ | `chartFill` | `"white"` | Background fill of the entire chart |
115
+ | `chartHeight` | `100` | Default chart height in pixels |
116
+ | `chartWidth` | `100` | Default chart width in pixels |
117
+ | `closed` | auto | Draw a border around the plot area. Auto-enabled when `viewFill` is set |
118
+ | `darkmode` | `False` | Invert text and axis colors for dark backgrounds |
119
+ | `dashedLine` | `False` | Render line marks dashed |
120
+ | `dashedRule` | `True` | Render rule marks dashed |
121
+ | `dashedWidth` | `[2, 2]` | Dash/gap pattern `[dash, gap]` in pixels |
122
+ | `font` | `"HelveticaNeue"` | Font family for all labels and titles |
123
+ | `fontSize` | `7` | Font size in points |
124
+ | `fontWeight` | `400` | Font weight: 300 = light, 400 = normal, 700 = bold |
125
+ | `grid` | `False` | Show axis grid lines |
126
+ | `gridColor` | `"darkGray"` | Grid line color |
127
+ | `legend` | `True` | Show legends |
128
+ | `legendOffset` | `tickSize` | Distance between legend and chart edge |
129
+ | `legendStroke` | `False` | Draw a border around the legend box |
130
+ | `markFill` | `"black"` | Default fill color for marks |
131
+ | `markFillOpacity` | `1.0` | Default mark fill opacity |
132
+ | `markSize` | `min(W, H) × 0.1` | Mark size; for points, this is area in sq px |
133
+ | `markStroke` | `"black"` | Default stroke color for marks |
134
+ | `markStrokeOpacity` | `1` | Default mark stroke opacity |
135
+ | `palette` | `None` | Default color scheme applied to category, diverging, heatmap, and ramp scales. Accepts a key from `colors` or a raw list |
136
+ | `strokeCap` | `"round"` | Stroke end cap: `"butt"`, `"round"`, or `"square"` |
137
+ | `ticks` | `True` | Show axis ticks |
138
+ | `tickSize` | `5` | Tick length in pixels |
139
+ | `transparentBackground` | `False` | Transparent chart background (overrides `chartFill`) |
140
+ | `verticalY` | `False` | Rotate y-axis labels 90° |
141
+ | `viewFill` | `None` | Fill color of the plot area only. Setting this auto-enables `closed` |
142
+ | `xTicks` | `True` | Show ticks on the x-axis |
143
+ | `yTicks` | `True` | Show ticks on the y-axis |
144
+
145
+ ---
146
+
147
+ ## Palettes
148
+
149
+ All custom palettes are built in [Oklab](https://bottosson.github.io/posts/oklab/) (Ottosson, *A perceptual color space for image processing*, 2020) for perceptual uniformity. They are stored in `dysonsphere.colors`, a plain `dict[str, list[str]]` mapping palette names to 12-stop hex lists (13 stops for diverging palettes).
150
+
151
+ ### Accessing palettes
152
+
153
+ ```python
154
+ from dysonsphere.palettes import colors
155
+
156
+ blues = colors["blues"] # list of 12 hex strings, light → dark
157
+ ```
158
+
159
+ ### dysonsphere.palette()
160
+
161
+ Samples a slice or subset from any named palette.
162
+
163
+ ```python
164
+ theme.palette("blues") # all 12 stops
165
+ theme.palette("blues", n=5) # 5 evenly-spaced stops
166
+ theme.palette("blues", start=3) # stops 3–11
167
+ theme.palette("blues", end=6, step=2) # indices 0, 2, 4, 6
168
+ theme.palette("blues", n=4, reverse=True) # reversed
169
+ ```
170
+
171
+ | Parameter | Default | Description |
172
+ |---|---|---|
173
+ | `name` | required | Key in `colors` |
174
+ | `n` | `None` | Return `n` evenly-spaced stops (overrides `step`) |
175
+ | `start` | `0` | Index of the first stop to include |
176
+ | `end` | last | Index of the last stop to include (inclusive) |
177
+ | `step` | `1` | Step between indices (used when `n` is not set) |
178
+ | `reverse` | `False` | Reverse the returned list |
179
+
180
+ ### Theme defaults
181
+
182
+ When no explicit `scale=` is set on a color encoding, Vega-Lite falls back to the theme's range defaults:
183
+
184
+ | Range type | Default palette | Used for |
185
+ |---|---|---|
186
+ | `category` | `blues` (even indices: 0, 2, 4, 6, 8, 10) | Nominal/unordered groups |
187
+ | `ordinal` | `blues` | Ordered discrete values |
188
+ | `ramp` | `blues` | Sequential continuous (legend ramps) |
189
+ | `heatmap` | `blues` | Rect/heatmap marks |
190
+ | `diverging` | `redsblues` | Diverging scales |
191
+
192
+ Setting `theme.options(palette="mypalette")` overrides all five types simultaneously.
193
+
194
+ ### Available palettes
195
+
196
+ See the [palette gallery](https://dkkung.github.io/dysonsphere/) for a visual overview of all palettes, or open `docs/index.html` locally.
197
+
198
+ **Sequential — Single-hue** (12 stops, light → dark):
199
+ `blues`, `greens`, `purples`, `lavenders`, `violets`, `greys`, `reds`, `rose`, `oranges`, `browns`, `yellows`, `cyans`, `magentas`, `neongreens`
200
+
201
+ **Sequential — Single-hue 2** (12 stops, deeper saturation built with Oklab arc-length resampling):
202
+ `blues2`, `greens2`, `purples2`, `lavenders2`, `violets2`, `greys2`, `reds2`, `roses2`, `oranges2`, `browns2`, `yellows2`, `cyans2`, `magentas2`, `neongreens2`
203
+
204
+ **Sequential — Multi-hue** (12 stops, two or more hues blended in Oklab):
205
+ `yellowgreen`, `ember`, `dusk`, `shoal`, `moss`, `GnBu`, `YlGnBu`, `candy`, `lagoon`, `bluestlagoon`, `bluerlagoon`, `bluelagoon`
206
+
207
+ **Diverging** (13 stops, exact-white pivot at stop 6):
208
+ `RdBu`, `RdYlBu`, `PuGn`, `MgGn`, `PkTe`, `GdBu`, `BrTe`, `BrGn`
209
+
210
+ **Diverging — Sequential pairs** (13 stops, one sequential hue per arm):
211
+ `greensblues`, `redsblues`, `redsgreens`, `redscyans`, `redslavenders`, `redsviolets`, `redsneongreens`, `rosesblues`, `rosescyans`, `rosesgreens`, `rosesneongreens`, `orangesblues`, `orangescyans`, `orangespurples`, `orangeslavenders`, `orangesviolets`, `orangesneongreens`, `yellowsblues`, `yellowspurples`, `yellowslavenders`, `brownsblues`, `brownsgreens`, `brownscyans`, `brownsneongreens`, `magentasneongreens`, `magentasgreens`, `magentasblues`, `magentascyans`, `violetsoranges`, `violetsyellows`, `purplesgreens`, `purplesblues`, `purplesneongreens`, `lavendersgreens`, `lavendersblues`, `lavendersneongreens`, `cyanspurples`, `cyanslavenders`, `cyansviolets`, `greysblues`, `greysreds`, `greysgreens`, `greyscyans`, `greysyellows`, `greysoranges`, `greysmagentas`, `greysviolets`, `greysneongreens`, `greyspurples`, `greyslavender`, `greysrose`
212
+
213
+ **Discrete:**
214
+ `nucleotides` (5 colors: A, T, G, C, U), `proteins` (8 biochemical groups: hydrophobic, aromatic, positive, negative, polar, proline, glycine, cysteine)
215
+
216
+ **Matplotlib ported** (prefixed with `mpl_`):
217
+ `mpl_viridis`, `mpl_plasma`, `mpl_inferno`, `mpl_magma`, `mpl_cividis`, `mpl_turbo`, `mpl_Blues`, `mpl_Greens`, `mpl_Greys`, `mpl_Oranges`, `mpl_Purples`, `mpl_Reds`, `mpl_YlGnBu`, `mpl_YlOrBr`, `mpl_YlOrRd`, and more.
218
+
219
+ **cmocean ported** (prefixed with `cmocean_`):
220
+ `cmocean_algae`, `cmocean_amp`, `cmocean_balance`, `cmocean_curl`, `cmocean_deep`, `cmocean_delta`, `cmocean_dense`, `cmocean_diff`, `cmocean_gray`, `cmocean_haline`, `cmocean_ice`, `cmocean_matter`, `cmocean_oxy`, `cmocean_phase`, `cmocean_rain`, `cmocean_solar`, `cmocean_speed`, `cmocean_tarn`, `cmocean_tempo`, `cmocean_thermal`, `cmocean_topo`, `cmocean_turbid`
221
+
222
+ ---
223
+
224
+ ## Saving charts
225
+
226
+ ```python
227
+ theme.save(chart, "plots/myplot")
228
+ # writes: plots/myplot_light.png, plots/myplot_light.svg
229
+ # plots/myplot_dark.png, plots/myplot_dark.svg
230
+ # plots/myplot_vegalite.json
231
+ ```
232
+
233
+ Produces light and dark PNG and SVG files from a single call. SVG output is post-processed to flatten Vega's redundant `<g>` wrappers, making it easier to navigate in Illustrator. A Vega-Lite JSON spec is also saved by default for full reproducibility.
234
+
235
+ ```python
236
+ theme.save(chart, "myplot", ppi=1200) # default PPI; reduce for faster exports
237
+ theme.save(chart, "myplot", save_vega_spec=False) # skip the JSON spec
238
+ theme.save(chart, "myplot", description="Figure 1") # embed a description in the SVG
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Custom marks
244
+
245
+ ### dysonsphere.mark_violin()
246
+
247
+ Violin plot with an embedded boxplot.
248
+
249
+ ```python
250
+ theme.options(chartWidth=300)
251
+ palette = theme.palette("lavenders", n=len(CATEGORIES))
252
+
253
+ chart = theme.mark_violin(df, "group", "value", CATEGORIES, palette=palette)
254
+ theme.save(chart, "violin")
255
+ ```
256
+
257
+ | Parameter | Default | Description |
258
+ |---|---|---|
259
+ | `df` | required | Polars DataFrame |
260
+ | `x_col` | required | Grouping column name |
261
+ | `y_col` | required | Value column name |
262
+ | `categories` | required | Ordered list of group labels |
263
+ | `palette` | `None` | Single color or list of colors for violin fills |
264
+ | `boxplot_size` | `markSize × 0.8` | Boxplot box width in pixels |
265
+ | `boxplot_color` | `"black"` | Boxplot fill color |
266
+ | `fillOpacity` | theme default | Violin fill opacity |
267
+ | `stroke` | `None` | Violin outline color (`None` = no outline) |
268
+ | `strokeWidth` | theme default | Violin outline width |
269
+ | `legend` | `False` | Show a color legend |
270
+ | `angledX` | theme default | Angle x-axis labels |
271
+ | `steps` | `200` | KDE grid resolution per group |
272
+
273
+ ### dysonsphere.mark_strip()
274
+
275
+ Jittered or beeswarm points with a median tick and optional mean ± error bars.
276
+
277
+ ```python
278
+ chart = theme.mark_strip(df, "group", "value", CATEGORIES)
279
+ chart = theme.mark_strip(df, "group", "value", CATEGORIES, scatter="beeswarm")
280
+ ```
281
+
282
+ | Parameter | Default | Description |
283
+ |---|---|---|
284
+ | `scatter` | `"jitter"` | `"jitter"` (fast, random Gaussian) or `"beeswarm"` (collision-avoidance) |
285
+ | `palette` | `None` | List of colors for points |
286
+ | `point_size` | theme `markSize` | Point size in sq px |
287
+ | `jitter_scale` | `4.0` | Jitter standard deviation in pixels |
288
+ | `errorbars` | `True` | Show mean ± error bars |
289
+ | `errorbar_extent` | `"sem"` | `"sem"` or `"sd"` |
290
+
291
+ ---
292
+
293
+ ## Statistical annotations
294
+
295
+ Add a p-value bracket between two groups using `dysonsphere.pvalue_layer()`. Combine with any chart using `+`.
296
+
297
+ ```python
298
+ ann = theme.pvalue_layer(
299
+ df, "group", "value", "Control", "Drug A",
300
+ test="mannwhitneyu",
301
+ categories=["Control", "Drug A", "Drug B"],
302
+ chartWidth=300,
303
+ y=210,
304
+ )
305
+ chart + ann
306
+ ```
307
+
308
+ From a pre-computed p-value:
309
+
310
+ ```python
311
+ ann = theme.pvalue_layer(
312
+ group1="Control", group2="Drug A",
313
+ pvalue=0.023, y=210,
314
+ categories=CATEGORIES,
315
+ chartWidth=300,
316
+ )
317
+ ```
318
+
319
+ | Parameter | Default | Description |
320
+ |---|---|---|
321
+ | `df` | `None` | Polars DataFrame (required unless `pvalue` and `y` are both provided) |
322
+ | `x_col`, `y_col` | `None` | Column names for groups and values |
323
+ | `group1`, `group2` | required | Group labels to compare |
324
+ | `test` | `"mannwhitneyu"` | Statistical test: `"mannwhitneyu"`, `"ttest_ind"`, `"ttest_rel"`, `"wilcoxon"`, `"tukey_hsd"` |
325
+ | `pvalue` | `None` | Pre-computed p-value (skips the test) |
326
+ | `correction` | `None` | `"bonferroni"` or `None` |
327
+ | `n_comparisons` | `1` | Number of comparisons for Bonferroni correction |
328
+ | `y` | auto | Y position of the bracket in data units |
329
+ | `y_pad` | `5` | Padding above the group max when `y` is auto-placed |
330
+ | `style` | `"line"` | `"line"` (bar only) or `"bracket"` (bar + end ticks) |
331
+ | `categories` | inferred | Ordered list of all x-axis categories |
332
+ | `chartWidth` | theme default | Chart width used to compute text x position |
333
+ | `reverse` | `False` | Flip the annotation below the bar |
334
+ | `decimals` | `3` | Decimal places in the p-value label |
335
+
336
+ ---
337
+
338
+ ## Data transforms
339
+
340
+ ### Jitter
341
+
342
+ Adds random Gaussian x-offsets to each row, useful for strip plots.
343
+
344
+ ```python
345
+ df = theme.add_jitter_offsets(df, scale=5)
346
+
347
+ alt.Chart(df).mark_circle().encode(
348
+ x=alt.X("group:N"),
349
+ y=alt.Y("value:Q"),
350
+ xOffset=alt.XOffset("jitter_x:Q"),
351
+ )
352
+ ```
353
+
354
+ | Parameter | Default | Description |
355
+ |---|---|---|
356
+ | `scale` | `5.0` | Standard deviation of jitter in pixels |
357
+ | `out_col` | `"jitter_x"` | Output column name |
358
+ | `seed` | `20220701` | Random seed |
359
+
360
+ ### Beeswarm
361
+
362
+ Computes collision-avoiding x-offsets per group. Better than jitter for small n, but slower.
363
+
364
+ ```python
365
+ df = theme.add_beeswarm_offsets(
366
+ df,
367
+ y_col="value",
368
+ group_by=["group"],
369
+ height_px=200,
370
+ markSize=10,
371
+ )
372
+ ```
373
+
374
+ | Parameter | Default | Description |
375
+ |---|---|---|
376
+ | `y_col` | required | Value column |
377
+ | `group_by` | required | Column(s) defining each beeswarm group |
378
+ | `height_px` | theme `chartHeight` | Chart height in pixels |
379
+ | `markSize` | `10` | Point size (area in sq px) |
380
+ | `out_col` | `"beeswarm_x"` | Output column name |
381
+
382
+ ---
383
+
384
+ ## Development
385
+
386
+ ### Building palettes
387
+
388
+ `scripts/build/build_palettes.py` documents the Oklab recipes for all custom palette families and prints updated hex literals to stdout. Use this to calibrate or extend palettes.
389
+
390
+ ```sh
391
+ # uv
392
+ uv run python scripts/build/build_palettes.py
393
+
394
+ # pip
395
+ python scripts/build/build_palettes.py
396
+ ```
397
+
398
+ The four recipes are:
399
+
400
+ 1. **Sequential single-hue** — fix hue; sweep L from light to dark with C = `frac × Cmax(L, hue)`; arc-length resample to 12 stops.
401
+ 2. **Sequential multi-hue** — interpolate `(L, hue)` between keyframes; same chroma and arc-length logic.
402
+ 3. **Diverging** — two arms meeting at an exact-white pivot; 13 stops so the white center lands exactly on the V-corner.
403
+ 4. **Chroma-scaling** — preserve L, scale `(a, b)` by a constant to derive lighter variants.
404
+
405
+ Palette hex values live in `dysonsphere/palettes.py` as plain lists — no color math runs at import time.
406
+
407
+ ### Building the gallery
408
+
409
+ ```sh
410
+ # uv
411
+ uv run python scripts/build/build_gallery.py
412
+
413
+ # pip
414
+ python scripts/build/build_gallery.py
415
+ ```
416
+
417
+ Writes `docs/index.html`. Open in a browser to browse all palettes across 11 chart types.
418
+
419
+ ### Exporting swatches for Adobe Illustrator
420
+
421
+ ```sh
422
+ # uv
423
+ uv run python scripts/build/build_swatches_for_illustrator.py
424
+
425
+ # pip
426
+ python scripts/build/build_swatches_for_illustrator.py
427
+ ```
428
+
429
+ Generates `scripts/import_palettes_to_illustrator.jsx`. To import into Illustrator:
430
+
431
+ 1. Open or create a document in Adobe Illustrator.
432
+ 2. Go to **File > Scripts > Other Script...**
433
+ 3. Select `scripts/import_palettes_to_illustrator.jsx`.
434
+ 4. All palettes are added as named swatch groups in the Swatches panel.
435
+
436
+ Re-run this script after adding or modifying palettes in `dysonsphere/palettes.py`.