Skip to content

Commit 8d8572f

Browse files
math-formula (#824)
* math-formula * Move mathjs to dev deps * Use MenuField with stepper instead of Radio * Add raster toggle * Merge branch 'next' into pr/cycle4passion/824 * Add color interpolator input
1 parent 5bab8e7 commit 8d8572f

4 files changed

Lines changed: 242 additions & 6 deletions

File tree

docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"vite-plugin-devtools-json": "^1.0.0",
119119
"vitest": "^4.1.4",
120120
"vitest-browser-svelte": "^2.1.0",
121+
"mathjs": "^15.2.0",
121122
"zod": "^4.3.6"
122123
},
123124
"dependencies": {

docs/src/examples/components/Raster/math-functions.svelte

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
import {
44
interpolateRainbow,
55
interpolateSinebow,
6+
interpolateWarm,
67
interpolateCool,
78
interpolateInferno,
89
interpolateViridis,
10+
interpolateMagma,
911
interpolateTurbo,
12+
interpolateCividis,
1013
interpolateYlGnBu,
1114
interpolateSpectral,
1215
interpolatePlasma,
13-
interpolateCubehelixDefault
16+
interpolateCubehelixDefault,
17+
interpolateRdYlBu
1418
} from 'd3-scale-chromatic';
1519
import { Axis, Chart, Contour, Layer, Raster } from 'layerchart';
1620
import { Field, MenuField, Switch } from 'svelte-ux';
@@ -49,20 +53,24 @@
4953
];
5054
5155
const interpolators = [
52-
{ label: 'Rainbow', value: 'rainbow', fn: interpolateRainbow },
53-
{ label: 'Sinebow', value: 'sinebow', fn: interpolateSinebow },
54-
{ label: 'Turbo', value: 'turbo', fn: interpolateTurbo },
5556
{ label: 'Viridis', value: 'viridis', fn: interpolateViridis },
5657
{ label: 'Inferno', value: 'inferno', fn: interpolateInferno },
58+
{ label: 'Magma', value: 'magma', fn: interpolateMagma },
5759
{ label: 'Plasma', value: 'plasma', fn: interpolatePlasma },
60+
{ label: 'Cividis', value: 'cividis', fn: interpolateCividis },
61+
{ label: 'Turbo', value: 'turbo', fn: interpolateTurbo },
62+
{ label: 'Rainbow', value: 'rainbow', fn: interpolateRainbow },
63+
{ label: 'Sinebow', value: 'sinebow', fn: interpolateSinebow },
64+
{ label: 'Warm', value: 'warm', fn: interpolateWarm },
5865
{ label: 'Cool', value: 'cool', fn: interpolateCool },
66+
{ label: 'Cubehelix', value: 'cubehelix', fn: interpolateCubehelixDefault },
5967
{ label: 'YlGnBu', value: 'ylgnbu', fn: interpolateYlGnBu },
6068
{ label: 'Spectral', value: 'spectral', fn: interpolateSpectral },
61-
{ label: 'Cubehelix', value: 'cubehelix', fn: interpolateCubehelixDefault }
69+
{ label: 'RdYlBu', value: 'rdylbu', fn: interpolateRdYlBu }
6270
];
6371
6472
let selectedFn = $state(functions[0].value);
65-
let selectedInterp = $state(interpolators[0].value);
73+
let selectedInterp = $state('rainbow');
6674
let fn = $derived(functions.find((f) => f.value === selectedFn)!);
6775
let interp = $derived(interpolators.find((i) => i.value === selectedInterp)!);
6876
let showContours = $state(false);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<script lang="ts">
2+
import { defaultChartPadding, LineChart, Raster, Spline, Tooltip } from 'layerchart';
3+
import { Field, MenuField, Switch, TextField } from 'svelte-ux';
4+
import { format } from '@layerstack/utils';
5+
import { evaluate } from 'mathjs';
6+
import { scaleSequential } from 'd3-scale';
7+
import {
8+
interpolateRainbow,
9+
interpolateSinebow,
10+
interpolateWarm,
11+
interpolateCool,
12+
interpolateInferno,
13+
interpolateViridis,
14+
interpolateMagma,
15+
interpolateTurbo,
16+
interpolateCividis,
17+
interpolateYlGnBu,
18+
interpolateSpectral,
19+
interpolatePlasma,
20+
interpolateCubehelixDefault,
21+
interpolateRdYlBu
22+
} from 'd3-scale-chromatic';
23+
24+
const options = [
25+
{ label: '', value: 'x^2' },
26+
{ label: 'log(x)', value: 'log(x)' },
27+
{ label: '(x²-4)/(x-2)', value: '(x^2-4)/(x-2)' },
28+
{ label: '', value: '2^x' },
29+
{ label: 'x³ - 2x', value: 'x^3 - 2*x' },
30+
{ label: 'sin(x)', value: 'sin(x)' },
31+
{ label: 'sqrt(x)', value: 'sqrt(x)' },
32+
{ label: 'abs(x) - 3', value: 'abs(x) - 3' },
33+
{ label: 'Custom', value: 'custom' }
34+
];
35+
36+
const xs = Array.from({ length: 100 }, (_, i) => -8 + i * 0.2);
37+
let selected = $state('x^2');
38+
let customFormula = $state('');
39+
let showRaster = $state(false);
40+
41+
const interpolators = [
42+
{ label: 'Viridis', value: 'viridis', fn: interpolateViridis },
43+
{ label: 'Inferno', value: 'inferno', fn: interpolateInferno },
44+
{ label: 'Magma', value: 'magma', fn: interpolateMagma },
45+
{ label: 'Plasma', value: 'plasma', fn: interpolatePlasma },
46+
{ label: 'Cividis', value: 'cividis', fn: interpolateCividis },
47+
{ label: 'Turbo', value: 'turbo', fn: interpolateTurbo },
48+
{ label: 'Rainbow', value: 'rainbow', fn: interpolateRainbow },
49+
{ label: 'Sinebow', value: 'sinebow', fn: interpolateSinebow },
50+
{ label: 'Warm', value: 'warm', fn: interpolateWarm },
51+
{ label: 'Cool', value: 'cool', fn: interpolateCool },
52+
{ label: 'Cubehelix', value: 'cubehelix', fn: interpolateCubehelixDefault },
53+
{ label: 'YlGnBu', value: 'ylgnbu', fn: interpolateYlGnBu },
54+
{ label: 'Spectral', value: 'spectral', fn: interpolateSpectral },
55+
{ label: 'RdYlBu', value: 'rdylbu', fn: interpolateRdYlBu }
56+
];
57+
let selectedInterp = $state('viridis');
58+
let interp = $derived(interpolators.find((i) => i.value === selectedInterp)!);
59+
60+
const formula = $derived(selected === 'custom' ? customFormula : selected);
61+
const { data, error } = $derived(computeGraph(formula));
62+
63+
const rasterValue = $derived.by(() => {
64+
const f = formula;
65+
if (!f?.trim()) return (_x: number, _y: number) => 0;
66+
return (x: number, _y: number) => {
67+
try {
68+
const y = evaluate(f, { x });
69+
return isFinite(y) ? y : NaN;
70+
} catch {
71+
return NaN;
72+
}
73+
};
74+
});
75+
76+
function computeGraph(formula: string) {
77+
if (!formula?.trim()) return { data: [], error: null };
78+
79+
try {
80+
const data = xs.flatMap((x) => {
81+
try {
82+
const y = evaluate(formula, { x });
83+
return isFinite(y) && Math.abs(y) < 1e6 ? [{ x, y }] : [];
84+
} catch {
85+
return [];
86+
}
87+
});
88+
return data.length === 0
89+
? { data: [], error: 'No valid points — check domain' }
90+
: { data, error: null };
91+
} catch (err) {
92+
return {
93+
data: [],
94+
error: err instanceof Error ? err.message : String(err)
95+
};
96+
}
97+
}
98+
</script>
99+
100+
<div class="grid gap-2 mb-4">
101+
<div class="grid grid-cols-[1fr_1fr] gap-2">
102+
<MenuField
103+
label="Formula"
104+
{options}
105+
bind:value={selected}
106+
stepper
107+
classes={{ menuIcon: 'hidden' }}
108+
/>
109+
<TextField
110+
label="Custom"
111+
bind:value={customFormula}
112+
placeholder="e.g. tan(x) or x^3 - x"
113+
error={selected === 'custom' && customFormula && error ? error : false}
114+
disabled={selected !== 'custom'}
115+
onfocusin={() => (selected = 'custom')}
116+
/>
117+
</div>
118+
<div class="grid grid-cols-[auto_1fr] gap-2">
119+
<Field label="Raster">
120+
<Switch bind:checked={showRaster} />
121+
</Field>
122+
<MenuField
123+
label="Color"
124+
options={interpolators}
125+
bind:value={selectedInterp}
126+
disabled={!showRaster}
127+
stepper
128+
classes={{ menuIcon: 'hidden' }}
129+
/>
130+
</div>
131+
</div>
132+
133+
<LineChart
134+
{data}
135+
x="x"
136+
y="y"
137+
cScale={showRaster ? scaleSequential(interp.fn) : undefined}
138+
props={{
139+
yAxis: {
140+
rule: true
141+
}
142+
}}
143+
clip
144+
yNice
145+
height={400}
146+
padding={defaultChartPadding({ left: 45, right: 45 })}
147+
>
148+
{#snippet marks({ context })}
149+
{#if showRaster}
150+
<Raster value={rasterValue} opacity={0.5} />
151+
{/if}
152+
{#each context.series.visibleSeries as s (s.key)}
153+
<Spline seriesKey={s.key} />
154+
{/each}
155+
{/snippet}
156+
{#snippet tooltip({ context })}
157+
<Tooltip.Root>
158+
{#snippet children({ data })}
159+
<Tooltip.List>
160+
<Tooltip.Item label="x" value={format(context.x(data), 'decimal')} />
161+
<Tooltip.Item label="y" value={format(context.y(data), 'decimal')} />
162+
</Tooltip.List>
163+
{/snippet}
164+
</Tooltip.Root>
165+
{/snippet}
166+
</LineChart>

0 commit comments

Comments
 (0)