Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/annotation-snippets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(AnnotationLine|AnnotationPoint|AnnotationRange): Accept a `label` snippet and a mark snippet (`line`, `circle`, `rect`) to render custom markup
14 changes: 14 additions & 0 deletions docs/src/content/components/AnnotationLine.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@ related: [AnnotationPoint, AnnotationRange]
## Usage

:example{ name="horizontal" showCode }

## Customization

### Custom label

Pass a `label` snippet to render your own markup. It receives the resolved label `props` (`x`, `y`, `textAnchor`, `verticalAnchor`, …) to position it with.

:example{ name="custom-label" showCode }

### Custom line

Pass a `line` snippet to draw the line yourself. It receives the resolved `x1`/`y1`/`x2`/`y2` (and `props.line`) to spread.

:example{ name="custom-line" showCode }
6 changes: 6 additions & 0 deletions docs/src/content/components/AnnotationPoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Pass `link` (or `link={{ type: 'swoop', ... }}` etc.) to draw a `<Link>` from
the ring edge to the label.
:example{ name="link-callouts" }

### Custom marker

Pass a `circle` snippet to draw your own marker, and a `label` snippet for your own label. Each receives the resolved `props` to spread.

:example{ name="custom-marker" showCode }

### Geo

Inside a geo `<Chart>`, `x`/`y` are interpreted as `[lon, lat]` and projected directly.
Expand Down
8 changes: 8 additions & 0 deletions docs/src/content/components/AnnotationRange.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ related: [AnnotationLine, AnnotationPoint]
## Usage

:example{ name="vertical-with-pattern-range" showCode }

## Customization

### Custom label

Pass a `label` snippet to render your own markup (and a `rect` snippet to draw the range yourself). Each receives the resolved `props` to position it with.

:example{ name="custom-label" showCode }
31 changes: 31 additions & 0 deletions docs/src/examples/components/AnnotationLine/custom-label.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script module lang="ts">
import { getAppleStock } from '$lib/data.remote';
const data = await getAppleStock();
</script>

<script lang="ts">
import { AnnotationLine, Layer, LineChart } from 'layerchart';

export { data };
</script>

<LineChart {data} x="date" y="value" height={300} padding={{ top: 10, bottom: 20, left: 25 }}>
{#snippet aboveContext()}
<Layer type="html" class="pointer-events-none">
<AnnotationLine
y={500}
props={{ line: { dashArray: [2, 2], stroke: 'var(--color-danger)' } }}
>
{#snippet label({ props })}
<div
class="absolute -translate-x-full -translate-y-1/2 whitespace-nowrap rounded-full bg-danger px-2 py-0.5 text-xs font-medium text-danger-content shadow"
style:left="{props.x}px"
style:top="{props.y}px"
>
Max · $500
</div>
{/snippet}
</AnnotationLine>
</Layer>
{/snippet}
</LineChart>
22 changes: 22 additions & 0 deletions docs/src/examples/components/AnnotationLine/custom-line.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script module lang="ts">
import { getAppleStock } from '$lib/data.remote';
const data = await getAppleStock();
</script>

<script lang="ts">
import { AnnotationLine, Line, LineChart } from 'layerchart';

export { data };
</script>

<LineChart {data} x="date" y="value" height={300} padding={{ top: 10, bottom: 20, left: 25 }}>
{#snippet aboveMarks()}
<AnnotationLine y={400} label="Target" props={{ label: { fill: 'var(--color-danger)' } }}>
{#snippet line({ props })}
<!-- Halo keeps the rule legible where it crosses the series -->
<Line {...props} stroke="var(--color-surface-100)" strokeWidth={6} />
<Line {...props} stroke="var(--color-danger)" strokeWidth={2} dashArray={[6, 4]} />
{/snippet}
</AnnotationLine>
{/snippet}
</LineChart>
43 changes: 43 additions & 0 deletions docs/src/examples/components/AnnotationPoint/custom-marker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script module lang="ts">
import { getAppleStock } from '$lib/data.remote';
const data = await getAppleStock();
</script>

<script lang="ts">
import { AnnotationPoint, Circle, LineChart } from 'layerchart';

const annotations = [
{ x: new Date('June 29, 2007'), y: 121.89, label: 'iPhone' },
{ x: new Date('April 3, 2010'), y: 232.39, label: 'iPad' },
{ x: new Date('March 7, 2012'), y: 545.18, label: 'Apple TV' }
];

const r = 4;

export { data };
</script>

<LineChart {data} x="date" y="value" height={300} padding={{ top: 10, bottom: 20, left: 25 }}>
{#snippet aboveMarks()}
{#each annotations as annotation}
<AnnotationPoint
x={annotation.x}
y={annotation.y}
{r}
label={annotation.label}
labelPlacement="top"
labelYOffset={6}
props={{
label: {
class: 'fill-primary stroke-surface-100 stroke-[3px] [paint-order:stroke] font-semibold'
}
}}
>
{#snippet circle({ props })}
<Circle {...props} r={r + 4} class="fill-primary/20 stroke-primary" />
<Circle {...props} class="fill-primary stroke-surface-100" strokeWidth={2} />
{/snippet}
</AnnotationPoint>
{/each}
{/snippet}
</LineChart>
39 changes: 39 additions & 0 deletions docs/src/examples/components/AnnotationRange/custom-label.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script module lang="ts">
import { getAppleStock } from '$lib/data.remote';
const data = await getAppleStock();
</script>

<script lang="ts">
import { AnnotationRange, Layer, LineChart, defaultChartPadding } from 'layerchart';

export { data };
</script>

<LineChart
{data}
x="date"
y="value"
height={300}
padding={defaultChartPadding({ left: 25, bottom: 15 })}
>
{#snippet belowContext()}
<Layer type="html" class="pointer-events-none">
<AnnotationRange
x={[new Date('2010-01-01'), new Date('2010-12-31')]}
class="bg-primary/10"
labelPlacement="top"
labelYOffset={8}
>
{#snippet label({ props })}
<div
class="absolute -translate-x-1/2 whitespace-nowrap rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-content shadow"
style:left="{props.x}px"
style:top="{props.y}px"
>
Fiscal 2010
</div>
{/snippet}
</AnnotationRange>
</Layer>
{/snippet}
</LineChart>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<script lang="ts">
import { getChartContext } from '$lib/contexts/chart.js';
import { cls } from '@layerstack/tailwind';
import type { LineProps } from '../Line/Line.shared.svelte.js';
import type { TextProps } from '../Text/Text.shared.svelte.js';

let {
Line,
Expand All @@ -25,6 +27,7 @@
y2: y2Prop,
seriesKey,
label,
line: lineSnippet,
labelPlacement = 'top-right',
labelXOffset = 0,
labelYOffset = 0,
Expand Down Expand Up @@ -153,24 +156,30 @@
verticalAnchor,
};
});

const resolvedLineProps = $derived({
...line,
...props?.line,
class: cls('lc-annotation-line', props?.line?.class),
} as LineProps);

const resolvedLabelProps = $derived({
...labelProps,
...props?.label,
class: cls('lc-annotation-line-label', props?.label?.class),
} as TextProps);
</script>

<Line
x1={line.x1}
y1={line.y1}
x2={line.x2}
y2={line.y2}
{...props?.line}
class={cls('lc-annotation-line', props?.line?.class)}
/>

{#if label}
<Text
value={label}
{...labelProps}
{...props?.label}
class={cls('lc-annotation-line-label', props?.label?.class)}
/>
{#if lineSnippet}
{@render lineSnippet({ props: resolvedLineProps })}
{:else}
<Line {...resolvedLineProps} />
{/if}

{#if typeof label === 'function'}
{@render label({ props: resolvedLabelProps })}
{:else if label}
<Text value={label} {...resolvedLabelProps} />
{/if}

<style>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ComponentProps } from 'svelte';
import type { ComponentProps, Snippet } from 'svelte';
import type { SVGAttributes } from 'svelte/elements';
import type { CommonStyleProps, Without } from '$lib/utils/types.js';
import type { SingleDomainType } from '$lib/utils/scales.svelte.js';
import type Line from '../Line/Line.svelte';
import type { LineProps } from '../Line/Line.shared.svelte.js';
import type Text from '../Text/Text.svelte';
import type { TextProps } from '../Text/Text.shared.svelte.js';
import type { Placement } from '../types.js';

export type AnnotationLinePropsWithoutHTML = {
Expand All @@ -20,10 +22,21 @@ export type AnnotationLinePropsWithoutHTML = {
y1?: SingleDomainType;
x2?: SingleDomainType;
y2?: SingleDomainType;
label?: string;
/**
* The label for the line.
*
* Can either be a string or a snippet to render custom content.
* The snippet receives spreadable props (position, anchors and `props.label`) to apply to the label.
*/
label?: string | Snippet<[{ props: TextProps }]>;
labelPlacement?: Placement;
labelXOffset?: number;
labelYOffset?: number;
/**
* A snippet to render your own line in place of the default `<Line>`.
* The snippet receives spreadable props (`x1`/`y1`/`x2`/`y2` and `props.line`).
*/
line?: Snippet<[{ props: LineProps }]>;
props?: {
label?: Partial<ComponentProps<typeof Text>>;
line?: Partial<ComponentProps<typeof Line>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import { getPointLabelLayout } from '$lib/utils/labelPlacement.js';
import { getPixelValue } from '../Text/Text.shared.svelte.js';
import { cls } from '@layerstack/tailwind';
import type { CircleProps } from '../Circle/Circle.shared.svelte.js';
import type { TextProps } from '../Text/Text.shared.svelte.js';

let {
Circle,
Expand All @@ -41,6 +43,7 @@
fontSize = 12,
labelGap = 2,
link,
circle,
details,
props,
}: AnnotationPointBaseProps = $props();
Expand Down Expand Up @@ -122,7 +125,9 @@
function onPointerMove(e: PointerEvent | MouseEvent | TouchEvent) {
if (details) {
e.stopPropagation();
ctx.tooltip.show(e, { annotation: { label, details } });
ctx.tooltip.show(e, {
annotation: { label: typeof label === 'string' ? label : undefined, details },
});
}
}

Expand All @@ -132,21 +137,33 @@
ctx.tooltip.hide();
}
}

const resolvedCircleProps = $derived({
cx: point.x,
cy: point.y,
r,
onpointermove: onPointerMove,
onmousemove: onPointerMove,
ontouchmove: onPointerMove,
onpointerleave: onPointerLeave,
onmouseleave: onPointerLeave,
ontouchend: onPointerLeave,
...props?.circle,
class: cls('lc-annotation-point', link && 'lc-annotation-point-ring', props?.circle?.class),
} as CircleProps);

const resolvedLabelProps = $derived({
...labelProps,
...props?.label,
class: cls('lc-annotation-point-label', props?.label?.class),
} as TextProps);
</script>

<Circle
cx={point.x}
cy={point.y}
{r}
onpointermove={onPointerMove}
onmousemove={onPointerMove}
ontouchmove={onPointerMove}
onpointerleave={onPointerLeave}
onmouseleave={onPointerLeave}
ontouchend={onPointerLeave}
{...props?.circle}
class={cls('lc-annotation-point', link && 'lc-annotation-point-ring', props?.circle?.class)}
/>
{#if circle}
{@render circle({ props: resolvedCircleProps })}
{:else}
<Circle {...resolvedCircleProps} />
{/if}

{#if linkEndpoints && Link}
<Link
Expand All @@ -163,13 +180,10 @@
/>
{/if}

{#if label}
<Text
value={label}
{...labelProps}
{...props?.label}
class={cls('lc-annotation-point-label', props?.label?.class)}
/>
{#if typeof label === 'function'}
{@render label({ props: resolvedLabelProps })}
{:else if label}
<Text value={label} {...resolvedLabelProps} />
{/if}

<style>
Expand Down
Loading
Loading