Skip to content

Commit d6ce354

Browse files
committed
feat: allow passing categorical colors as Record<domain, color>
resolves #50
1 parent 8c165be commit d6ce354

File tree

5 files changed

+149
-3
lines changed

5 files changed

+149
-3
lines changed

src/lib/core/Plot.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
initialWidth: 500,
6363
inset: 0,
6464
colorScheme: 'turbo',
65+
unknown: '#cccccc',
6566
dotRadius: 3,
6667
frame: false,
6768
axes: true,
@@ -160,7 +161,7 @@
160161
);
161162
162163
// if the plot is showing filled dot marks we're using different defaults
163-
// for the symbol axis range, so we're passing on this info to the createScales
164+
// for the symbol axis range, so we're passing on this info to the computeScales
164165
// function below
165166
const hasFilledDotMarks = $derived(
166167
!!explicitMarks.find((d) => d.type === 'dot' && d.options.fill)

src/lib/helpers/autoScales.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import callWithProps from './callWithProps.js';
4747
import { interpolateLab, interpolateRound } from 'd3-interpolate';
4848
import { coalesce, maybeNumber } from './index.js';
4949
import { getLogTicks } from './getLogTicks.js';
50+
import { isPlainObject } from 'es-toolkit';
5051

5152
const Scales: Record<
5253
ScaleType,
@@ -207,12 +208,28 @@ export function autoScaleColor({
207208
scheme,
208209
interpolate,
209210
pivot,
210-
n = type === 'threshold' ? domain.length + 1 : 9
211+
n = type === 'threshold' ? domain.length + 1 : 9,
212+
unknown = plotDefaults.unknown
211213
} = scaleOptions;
212214

213215
if (type === 'categorical' || type === 'ordinal') {
214216
// categorical
215-
const scheme_ = scheme || plotDefaults.categoricalColorScheme;
217+
let scheme_ = scheme || plotDefaults.categoricalColorScheme;
218+
219+
if (isPlainObject(scheme_)) {
220+
const newScheme = Object.values(scheme_);
221+
const newDomain = Object.keys(scheme_);
222+
// for every value in domain that's not part of the scheme, map to unknown
223+
for (const v of domain) {
224+
if (scheme_[v] == null) {
225+
newDomain.push(v);
226+
newScheme.push(unknown)
227+
}
228+
}
229+
domain = newDomain;
230+
scheme_ = newScheme;
231+
}
232+
216233
// categorical scale
217234
range = Array.isArray(scheme_)
218235
? scheme_

src/routes/features/scales/+page.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,54 @@ Note that the colors are picked in the order the categories appear in your datas
569569
</Plot>
570570
```
571571

572+
[fork](https://svelte.dev/playground/573be4d5791648fcb71d8ea9cdcc7112?version=5.33.1)
573+
574+
As a simpler syntax you can also pass domain -> scheme mapping as object:
575+
576+
```svelte live
577+
<script>
578+
import { Plot, Dot } from 'svelteplot';
579+
import { page } from '$app/state';
580+
let { penguins } = $derived(page.data.data);
581+
</script>
582+
583+
<Plot
584+
grid
585+
color={{
586+
legend: true,
587+
scheme: {
588+
FEMALE: 'var(--svp-green)',
589+
MALE: 'var(--svp-violet)'
590+
}
591+
}}>
592+
<Dot
593+
data={penguins}
594+
x="culmen_length_mm"
595+
y="culmen_depth_mm"
596+
fill="sex" />
597+
</Plot>
598+
```
599+
600+
```svelte
601+
<Plot
602+
grid
603+
color={{
604+
legend: true,
605+
scheme: {
606+
FEMALE: 'green',
607+
MALE: 'violet'
608+
}
609+
}}>
610+
<Dot
611+
data={penguins}
612+
x="culmen_length_mm"
613+
y="culmen_depth_mm"
614+
stroke="sex" />
615+
</Plot>
616+
```
617+
618+
[fork](https://svelte.dev/playground/49fb6bcbeb9e4789ba1680c0ffc63678?version=5.33.1)
619+
572620
## Continuous color scales
573621

574622
### Linear

src/tests/colors.test.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
import { Plot, Dot } from 'svelteplot';
3+
4+
let { colorOptions: color, dotOptions } = $props();
5+
</script>
6+
7+
<Plot {color} width={100} height={100} axes={false}>
8+
<Dot {...dotOptions} />
9+
</Plot>

src/tests/colors.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render } from '@testing-library/svelte';
3+
import ColorsTest from './colors.test.svelte';
4+
import { categoricalSchemes } from 'svelteplot/helpers/colors';
5+
6+
const observable10 = categoricalSchemes.get('observable10') as string[];
7+
8+
describe('Colors', () => {
9+
it('default color scheme is observable10', () => {
10+
const { container } = render(ColorsTest, {
11+
props: {
12+
dotOptions: {
13+
data: [{
14+
x: 1, sex: 'male'
15+
}, {
16+
x: 2, sex: 'female'
17+
}
18+
],
19+
x: 'x',
20+
y: 0,
21+
fill: 'sex'
22+
},
23+
colorOptions: {}
24+
}
25+
});
26+
const dots = container.querySelectorAll('g.dots > path') as NodeListOf<SVGPathElement>;
27+
expect(dots.length).toBe(2);
28+
const styles = Array.from(dots).map(getDotStyle);
29+
expect(styles[0].fill).toBe(observable10[0]);
30+
expect(styles[1].fill).toBe(observable10[1]);
31+
});
32+
33+
it('set custom scheme as object', () => {
34+
const { container } = render(ColorsTest, {
35+
props: {
36+
dotOptions: {
37+
data: [{
38+
x: 1, sex: 'male'
39+
}, {
40+
x: 2, sex: 'female'
41+
}, {
42+
x: 3, sex: 'in-between'
43+
}
44+
],
45+
x: 'x',
46+
y: 0,
47+
fill: 'sex'
48+
},
49+
colorOptions: {
50+
scheme: {
51+
male: 'green',
52+
female: 'violet'
53+
}
54+
}
55+
}
56+
});
57+
const dots = container.querySelectorAll('g.dots > path') as NodeListOf<SVGPathElement>;
58+
expect(dots.length).toBe(3);
59+
const styles = Array.from(dots).map(getDotStyle);
60+
expect(styles[0].fill).toBe('green');
61+
expect(styles[1].fill).toBe('violet');
62+
expect(styles[2].fill).toBe('#cccccc');
63+
});
64+
});
65+
66+
function getDotStyle(path: SVGPathElement) {
67+
return {
68+
fill: path.style.fill,
69+
stroke: path.style.stroke,
70+
}
71+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy