Skip to content

feat: simplify custom marks #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/Mark.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
mark: Mark<GenericMarkOptions>;
usedScales: ReturnType<typeof getUsedScales>;
scaledData: ScaledDataRecord[];
scaledData: ScaledDataRecord<Datum>[];
}
]
>;
Expand Down
73 changes: 46 additions & 27 deletions src/lib/marks/CustomMark.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,66 @@
-->
<script lang="ts" generics="Datum extends DataRecord">
interface CustomMarkProps extends BaseMarkProps<Datum> {
data: Datum[];
data?: Datum[];
x?: ChannelAccessor<Datum>;
x1?: ChannelAccessor<Datum>;
x2?: ChannelAccessor<Datum>;
y?: ChannelAccessor<Datum>;
children: Snippet<[{ datum: Datum; x: number; y: number }]>;
y1?: ChannelAccessor<Datum>;
y2?: ChannelAccessor<Datum>;
r?: ChannelAccessor<Datum>;
mark?: Snippet<
[{ record: ScaledDataRecord<Datum>; index: number; usedScales: UsedScales }]
>;
marks?: Snippet<[{ records: ScaledDataRecord<Datum>[]; usedScales: UsedScales }]>;
}

import { getContext } from 'svelte';
import type {
PlotContext,
DataRecord,
ChannelAccessor,
BaseMarkProps
BaseMarkProps,
ScaledDataRecord,
UsedScales,
ScaledChannelName
} from 'svelteplot/types/index.js';
import type { Snippet } from 'svelte';
import { sort } from '$lib/index.js';

const { getPlotState } = getContext<PlotContext>('svelteplot');
let plot = $derived(getPlotState());
import Mark from 'svelteplot/Mark.svelte';

import { resolveChannel } from '$lib/helpers/resolve.js';
import { projectXY } from '$lib/helpers/scales.js';
import { isValid } from '$lib/helpers/index.js';
import GroupMultiple from './helpers/GroupMultiple.svelte';
let { data = [{} as Datum], mark, marks, ...options }: CustomMarkProps = $props();

let {
data = [{} as Datum],
x,
y,
children,
class: className = null
}: CustomMarkProps = $props();
const args = $derived(sort({ data, ...options })) as CustomMarkProps;

const channels: ScaledChannelName[] = [
'x',
'x1',
'x2',
'y',
'y1',
'y2',
'r',
'fill',
'stroke',
'opacity',
'fillOpacity',
'strokeOpacity'
];
</script>

<GroupMultiple class="g-custom-mark {className || ''}" length={className ? 2 : data.length}>
{#each data as datum, i (i)}
{@const x_ = resolveChannel<Datum>('x', datum, { x, y })}
{@const y_ = resolveChannel<Datum>('y', datum, { x, y })}
{#if isValid(x_) && isValid(y_)}
{@const [px, py] = projectXY(plot.scales, x_, y_)}
<g transform="translate({px}, {py})">
{@render children({ datum, x: px, y: py })}
</g>
<Mark type="custom" required={[]} channels={channels.filter((d) => !!options[d])} {...args}>
{#snippet children({ scaledData, usedScales })}
{#if marks}
{@render marks({ records: scaledData.filter((d) => d.valid), usedScales })}
{/if}
{#if mark}
{#each scaledData as datum, i (i)}
{#if datum.valid}
{@render mark({ record: datum, index: i, usedScales })}
{/if}
{/each}
{/if}
{/each}
</GroupMultiple>
{/snippet}
</Mark>
4 changes: 4 additions & 0 deletions src/lib/types/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ export type ScaledChannelName =
| 'y1'
| 'y2';

export type ScaledChannelType<T extends ScaledChannelName> = T extends 'fill' | 'stroke' | 'symbol'
? string
: number;

export type ChannelName = ScaledChannelName | 'z' | 'sort' | 'filter' | 'interval';
8 changes: 4 additions & 4 deletions src/lib/types/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ScaledChannelName } from './channel.js';
import type { ScaledChannelName, ScaledChannelType } from './channel.js';

export type RawValue = number | Date | boolean | string | symbol;

Expand All @@ -12,9 +12,9 @@ export type ResolvedDataRecord<T = Record<string | symbol, RawValue>> = Partial<
datum: DataRecord<T>;
};

export type ScaledDataRecord<T = Record<string | symbol, RawValue>> = Partial<
Record<ScaledChannelName, number | string | boolean | undefined>
> & {
export type ScaledDataRecord<T = Record<string | symbol, RawValue>> = Partial<{
[K in ScaledChannelName]?: ScaledChannelType<K>;
}> & {
datum: DataRecord<T>;
valid: Boolean;
};
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type MarkType =
| 'barX'
| 'barY'
| 'cell'
| 'custom'
| 'dot'
| 'vector'
| 'frame'
Expand Down
39 changes: 39 additions & 0 deletions src/routes/examples/custom/custom-rect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script module lang="ts">
export const title = 'Simple rectangles';
export const repl =
'https://svelte.dev/playground/7a6b0ae12c624ffeb52448adac644b5b?version=5.33.18';
</script>

<script lang="ts">
import { Plot, CustomMark, Text } from 'svelteplot';

const data = [
{
x1: 10,
x2: 15,
y1: 5,
y2: 9
},
{
x1: 7,
x2: 12,
y1: 7,
y2: 13
}
];
</script>

<Plot grid inset={10}>
<CustomMark {data} x1="x1" x2="x2" y1="y1" y2="y2">
{#snippet mark({ record })}
<rect
x={Math.min(record.x1, record.x2)}
y={Math.min(record.y1, record.y2)}
width={Math.abs(record.x2 - record.x1)}
height={Math.abs(record.y2 - record.y1)}
stroke="currentColor"
fill="currentColor"
fill-opacity="0.5" />
{/snippet}
</CustomMark>
</Plot>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script module>
export const title = 'Custom SVG marks';
export const title = 'Multiple SVG marks';
</script>

<script lang="ts">
Expand All @@ -14,9 +14,9 @@

<Plot
grid
height={400}
x={{ domain: [30, 62] }}
y={{ domain: [13, 21.9] }}>
inset={10}
color={{ legend: true }}
r={{ range: [0.4, 1.4], zero: false }}>
<defs>
<symbol
id="spiral"
Expand All @@ -32,16 +32,17 @@
<CustomMark
data={penguins}
x="culmen_length_mm"
y="culmen_depth_mm">
{#snippet children({ datum })}
y="culmen_depth_mm"
fill="species"
r="body_mass_g">
{#snippet mark({ record })}
<use
transform={`translate(${record.x}, ${record.y}) scale(${record.r})`}
href="#spiral"
x="-12"
y="-12"
color={datum.species === 'Adelie'
? 'var(--svp-red)'
: 'var(--svp-blue)'}
><title>{datum.species}</title></use>
color={record.fill}
><title>{record.datum.species}</title></use>
{/snippet}
</CustomMark>
</Plot>
38 changes: 38 additions & 0 deletions src/routes/examples/custom/single.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script module>
export const title = 'Single SVG marks';
export const description =
'You can use the CustomMark to place a single SVG element at a specific x/y coordinate.';
</script>

<script lang="ts">
import { Plot, Dot, CustomMark } from 'svelteplot';
import Spiral from '$lib/ui/Spiral.svelte';
import { page } from '$app/state';
import type { ExamplesData } from '../types';
let { penguins } = $derived(
page.data.data
) as ExamplesData;
</script>

<Plot grid>
<CustomMark x={40} y={16}>
{#snippet children({ record })}
<g
transform="translate({record.x}, {record.y})">
<circle
r={60}
opacity={0.2}
fill={record.fill} />
<circle
r={80}
opacity={0.2}
fill={record.fill} />
</g>
{/snippet}
</CustomMark>
<Dot
data={penguins}
x="culmen_length_mm"
y="culmen_depth_mm"
fill="species" />
</Plot>
Loading