Skip to content
Open
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
57 changes: 53 additions & 4 deletions packages/layerchart/src/lib/components/Labels.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
y?: Accessor<T>;

/**
* The placement of the label relative to the point
* The placement of the label relative to the point.
* `smart` dynamically positions labels based on neighboring point values (peak, trough, rising, falling).
* @default 'outside'
*/
placement?: 'inside' | 'outside' | 'center';
placement?: 'inside' | 'outside' | 'center' | 'smart';

/**
* The offset of the label from the point
Expand Down Expand Up @@ -172,21 +173,69 @@
}
}
}

function getDynamicTextProps(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this getSmartTextProps(), although I wonder if we could merge this into getTextProps() (instead of early returning in getTextProps(), capture the baseProps and then use them if placement === 'smart' or return as is if not.

Although not sure how much of baseProps we really need at first glance

point: Point,
points: Point[],
i: number
): ComponentProps<typeof Text> {
const baseProps = getTextProps(point);

const getValue = (p: Point): number => (isScaleBand(ctx.yScale) ? p.xValue : p.yValue);
const curr = getValue(point);
const prev = i > 0 ? getValue(points[i - 1]) : curr;
const next = i < points.length - 1 ? getValue(points[i + 1]) : curr;

const xPrevTight = Math.abs(prev - curr) < offset;
const xNextTight = Math.abs(curr - next) < offset;
const isPeak = (prev <= curr && curr >= next) || (xPrevTight && xNextTight);
const isTrough = (prev >= curr && curr <= next) || (xPrevTight && xNextTight);
const isRising = !isPeak && !isTrough && prev < curr;
const isFalling = !isPeak && !isTrough && prev >= curr;

return {
...baseProps,
x: point.x,
y: point.y,
dx: isRising
? xPrevTight
? offset
: -offset
: isFalling
? xNextTight
? -offset
: offset
: 0,
dy: isPeak ? -offset : isTrough ? offset : 0,
textAnchor: isRising
? xPrevTight
? 'start'
: 'end'
: isFalling
? xNextTight
? 'end'
: 'start'
: 'middle',
verticalAnchor: isPeak ? 'end' : isTrough ? 'start' : 'middle',
};
}
</script>

<Group class="lc-labels-g">
<Points {data} {x} {y}>
{#snippet children({ points })}
{#each points as point, i (key(point.data, i))}
{@const textProps = extractLayerProps(getTextProps(point), 'lc-labels-text')}
{@const baseProps =
placement === 'smart' ? getDynamicTextProps(point, points, i) : getTextProps(point)}
{@const textProps = extractLayerProps(baseProps, 'lc-labels-text')}
{#if childrenProp}
{@render childrenProp({ data: point, textProps })}
{:else}
<Text
data-placement={placement}
{...textProps}
{...restProps}
{...extractLayerProps(getTextProps(point), 'lc-labels-text', className ?? '')}
{...extractLayerProps(baseProps, 'lc-labels-text', className ?? '')}
/>
{/if}
{/each}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@
{@render labels(snippetProps)}
{:else if labels}
{#each seriesState.visibleSeries as s, i (s.key)}
<Labels {...getLabelsProps(s, i)} />
<Labels {...getLabelsProps(s, i)} placement="smart" />
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placement="smart" should come before getLabelProps so it can be overridden (ex. Labels within points)

Image

Note: where this is handled is changing within state-refactor branch/PR, which is going to cause a merge conflict. I might keep this PR open and manually merge into state-refactor once the dust settles there.

{/each}
{/if}

Expand Down
Loading