entasis 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AppShell/appShell.theme.js +3 -3
- package/dist/components/ButtonGroup/ButtonGroup.svelte +10 -4
- package/dist/components/ButtonGroup/buttonGroup.mcp.d.ts +1 -1
- package/dist/components/ButtonGroup/buttonGroup.mcp.js +23 -3
- package/dist/components/ButtonGroup/buttonGroup.props.d.ts +19 -5
- package/dist/components/Chart/Chart.svelte +1 -1
- package/dist/components/Chart/chart.cartesian.js +3 -2
- package/dist/components/Chart/chart.fit.d.ts +53 -0
- package/dist/components/Chart/chart.fit.js +81 -0
- package/dist/components/Chart/chart.mcp.d.ts +1 -1
- package/dist/components/Chart/chart.mcp.js +2 -1
- package/dist/components/Chart/chart.polar.js +137 -18
- package/dist/components/Chart/chart.polar.props.d.ts +5 -0
- package/dist/components/Chart/chart.proportion.js +12 -6
- package/dist/components/Chart/chart.relation.network.js +32 -4
- package/dist/components/Chart/chart.relation.sankey.js +53 -6
- package/dist/components/Chart/chart.relation.tree.js +84 -24
- package/dist/components/Chart/chart.series.props.d.ts +6 -0
- package/dist/components/Chart/chart.state.svelte.d.ts +1 -0
- package/dist/components/Chart/chart.state.svelte.js +9 -6
- package/dist/components/Chart/chart.viewport.svelte.d.ts +1 -0
- package/dist/components/Chart/chart.viewport.svelte.js +25 -7
- package/dist/components/DataTable/DataTable.svelte +25 -8
- package/dist/components/DataTable/DataTableRow.svelte +33 -10
- package/dist/components/DataTable/dataTable.mcp.d.ts +1 -1
- package/dist/components/DataTable/dataTable.mcp.js +12 -1
- package/dist/components/DataTable/dataTable.model.svelte.js +4 -3
- package/dist/components/DataTable/dataTable.props.d.ts +17 -0
- package/dist/components/DataTable/dataTable.theme.d.ts +12 -0
- package/dist/components/DataTable/dataTable.theme.js +3 -2
- package/dist/components/DataTable/index.d.ts +1 -1
- package/dist/components/Dialog/Dialog.svelte +12 -7
- package/dist/components/Dialog/dialog.state.svelte.d.ts +2 -1
- package/dist/components/Dialog/dialog.state.svelte.js +7 -7
- package/dist/components/Dialog/dialog.theme.js +1 -1
- package/dist/components/FloatingWindow/floatingWindow.theme.js +1 -1
- package/dist/components/Form/Select/Select.svelte +3 -1
- package/dist/components/PageShell/pageShell.state.svelte.d.ts +1 -1
- package/dist/components/PageShell/pageShell.state.svelte.js +6 -13
- package/dist/components/Popover/Popover.svelte +5 -5
- package/dist/components/Popover/index.d.ts +1 -1
- package/dist/components/Popover/popover.mcp.d.ts +1 -1
- package/dist/components/Popover/popover.mcp.js +33 -6
- package/dist/components/Popover/popover.state.svelte.d.ts +9 -1
- package/dist/components/Popover/popover.state.svelte.js +61 -8
- package/dist/components/Popover/popover.theme.js +1 -1
- package/dist/components/Sidebar/Sidebar.svelte +1 -1
- package/dist/components/Sidebar/SidebarDesktopShell.svelte +26 -6
- package/dist/components/Sidebar/sidebar-layout.js +7 -6
- package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
- package/dist/components/Sidebar/sidebar.mcp.js +2 -2
- package/dist/components/Sidebar/sidebar.theme.js +36 -20
- package/dist/components/Theme/index.d.ts +1 -1
- package/dist/components/Theme/index.js +1 -1
- package/dist/components/Theme/theme.mcp.d.ts +1 -1
- package/dist/components/Theme/theme.mcp.js +3 -0
- package/dist/components/Theme/theme.state.svelte.d.ts +4 -0
- package/dist/components/Theme/theme.state.svelte.js +4 -0
- package/dist/components/Tooltip/Tooltip.svelte +2 -4
- package/dist/components/Tooltip/tooltip.attachment.svelte.js +5 -0
- package/dist/components/Tooltip/tooltip.mcp.d.ts +1 -1
- package/dist/components/Tooltip/tooltip.mcp.js +4 -2
- package/dist/generated/componentAliases.d.ts +1 -0
- package/dist/generated/componentAliases.js +1 -0
- package/dist/generated/componentContract.d.ts +13 -3
- package/dist/generated/componentContract.js +15 -1
- package/dist/generated/componentMcpRegistry.d.ts +8 -7
- package/dist/generated/componentMcpRegistry.js +2 -0
- package/dist/i18n/ar.js +1 -1
- package/dist/i18n/de.js +1 -1
- package/dist/i18n/en.js +1 -1
- package/dist/i18n/es.js +1 -1
- package/dist/i18n/fr.js +1 -1
- package/dist/i18n/pt.js +1 -1
- package/dist/i18n/zh.js +1 -1
- package/dist/tailwind/colors.d.ts +10 -3
- package/dist/tailwind/colors.js +6 -0
- package/dist/tailwind/palette.d.ts +5 -0
- package/dist/tailwind/palette.js +4 -0
- package/dist/tailwind/palette.mcp.d.ts +1 -0
- package/dist/tailwind/palette.mcp.js +34 -0
- package/dist/utils/layers.svelte.js +9 -8
- package/dist/utils/registry.svelte.d.ts +21 -0
- package/dist/utils/registry.svelte.js +49 -0
- package/package.json +8 -1
|
@@ -41,7 +41,7 @@ const defaultPage = cva({
|
|
|
41
41
|
variants: {
|
|
42
42
|
variant: {
|
|
43
43
|
admin: 'bg-surface [--page-shell-chrome:var(--color-surface-canvas)] [--page-shell-surface:var(--color-surface)]',
|
|
44
|
-
floating: 'bg-surface-canvas [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface-canvas)] [--page-shell-chrome-inline-gap:
|
|
44
|
+
floating: 'bg-surface-canvas [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface-canvas)] [--page-shell-chrome-inline-gap:var(--space-md)] [--page-shell-chrome-block-gap:var(--space-md)] [--page-shell-header-top-radius:var(--radius-lg)] [--page-shell-header-bottom-radius:var(--radius-lg)] [--page-shell-footer-top-radius:var(--radius-lg)] [--page-shell-footer-bottom-radius:var(--radius-lg)] [--page-shell-chrome-border:var(--color-neutral-muted)] [--page-shell-chrome-shadow:var(--elevation-1)]',
|
|
45
45
|
inset: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[page-flush=true]/sidebar-wrapper:border-transparent md:group-data-[page-flush=true]/sidebar-wrapper:shadow-none',
|
|
46
46
|
split: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[page-flush=true]/sidebar-wrapper:border-transparent md:group-data-[page-flush=true]/sidebar-wrapper:shadow-none',
|
|
47
47
|
// The frame already owns the radius, border and elevation, so the page inside it is flat,
|
|
@@ -61,12 +61,12 @@ const defaultPage = cva({
|
|
|
61
61
|
{
|
|
62
62
|
variant: 'floating',
|
|
63
63
|
side: 'left',
|
|
64
|
-
class: 'md:[--page-shell-chrome-left-gap:0px] md:group-data-[display-state=hidden]/sidebar-wrapper:[--page-shell-chrome-left-gap:
|
|
64
|
+
class: 'md:[--page-shell-chrome-left-gap:0px] md:group-data-[display-state=hidden]/sidebar-wrapper:[--page-shell-chrome-left-gap:var(--space-md)]'
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
variant: 'floating',
|
|
68
68
|
side: 'right',
|
|
69
|
-
class: 'md:[--page-shell-chrome-right-gap:0px] md:group-data-[display-state=hidden]/sidebar-wrapper:[--page-shell-chrome-right-gap:
|
|
69
|
+
class: 'md:[--page-shell-chrome-right-gap:0px] md:group-data-[display-state=hidden]/sidebar-wrapper:[--page-shell-chrome-right-gap:var(--space-md)]'
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
72
|
defaultVariants: {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
items,
|
|
8
|
+
children,
|
|
9
|
+
label,
|
|
8
10
|
size,
|
|
9
11
|
color,
|
|
10
12
|
variant,
|
|
@@ -17,8 +19,12 @@
|
|
|
17
19
|
const classes = $derived(useButtonGroupTheme(theme));
|
|
18
20
|
</script>
|
|
19
21
|
|
|
20
|
-
<div class={classes.root({ className })} {...attachments}>
|
|
21
|
-
{#
|
|
22
|
-
|
|
23
|
-
{
|
|
22
|
+
<div role="group" aria-label={label} class={classes.root({ className })} {...attachments}>
|
|
23
|
+
{#if children}
|
|
24
|
+
{@render children()}
|
|
25
|
+
{:else}
|
|
26
|
+
{#each items ?? [] as button, index (index)}
|
|
27
|
+
<Button {size} {color} {variant} {...button} disabled={disabled || button.disabled} />
|
|
28
|
+
{/each}
|
|
29
|
+
{/if}
|
|
24
30
|
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const buttonGroupDescription = "\n# ButtonGroup Component\n\nThe ButtonGroup component displays a collection of related buttons as a cohesive group with shared styling properties.\n\n## Basic Usage\n\n```svelte\n<ButtonGroup \n\titems={[\n\t\t{ children: 'First' },\n\t\t{ children: 'Second' },\n\t\t{ children: 'Third' }\n\t]}\n/>\n```\n\n## Props\n\n### Core Props\n- **items**: Array<ButtonProps>
|
|
1
|
+
export declare const buttonGroupDescription = "\n# ButtonGroup Component\n\nThe ButtonGroup component displays a collection of related buttons as a cohesive group with shared styling properties.\n\n## Basic Usage\n\n```svelte\n<ButtonGroup \n\titems={[\n\t\t{ children: 'First' },\n\t\t{ children: 'Second' },\n\t\t{ children: 'Third' }\n\t]}\n/>\n```\n\n## Props\n\n### Core Props\nPass either `items` or `children`.\n- **items**: Array<ButtonProps> - Array of button configurations\n - Each button can have all standard Button component props\n- **children**: Snippet - Buttons composed directly, for content an item object cannot describe\n (a Tooltip or Popover trigger, a Button with a custom body). Each direct child is joined to its\n neighbours; set size, color and variant on each Button, since the shared props below apply to\n `items` only.\n- **label**: string - Accessible name of the group (the root has `role=\"group\"`)\n\n### Shared Button Props (items)\n- **size**: 'small' | 'normal' | 'large' - Applied to all buttons in the group\n- **color**: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Shared color for all buttons\n- **variant**: 'solid' | 'outline' | 'soft' | 'ghost' | 'link' - Shared variant for all buttons\n- **disabled**: boolean - Disables all buttons in the group, even one whose item sets `disabled: false`\n\n### Styling Props\n- **class**: string - Additional CSS classes for the group container\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<ButtonGroup>\n\t<Button />\n\t<Button />\n\t<Button />\n</ButtonGroup>\n```\n\n## Examples\n\n### Composed Children\n```svelte\n<script lang=\"ts\">\n\timport { Button } from 'entasis/button';\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<ButtonGroup label=\"History\">\n\t<Button variant=\"outline\">Undo</Button>\n\t<Tooltip content=\"Redo the last change\" trigger={{ content: 'Redo', variant: 'outline' }} />\n</ButtonGroup>\n```\n\n### Basic Button Group\n```svelte\n<ButtonGroup \n\titems={[\n\t\t{ children: 'Left' },\n\t\t{ children: 'Center' },\n\t\t{ children: 'Right' }\n\t]}\n/>\n```\n\n### With Shared Styling\n```svelte\n<ButtonGroup \n\tsize=\"large\"\n\tcolor=\"primary\"\n\tvariant=\"outline\"\n\titems={[\n\t\t{ children: 'Option 1' },\n\t\t{ children: 'Option 2' },\n\t\t{ children: 'Option 3' }\n\t]}\n/>\n```\n\n### With Icons\n```svelte\n<script lang=\"ts\">\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { textAlignLeftIcon } from 'entasis/icons/textAlignLeft';\n\timport { textAlignCenterIcon } from 'entasis/icons/textAlignCenter';\n\timport { textAlignRightIcon } from 'entasis/icons/textAlignRight';\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tprefix: textAlignLeftIcon,\n\t\t\tchildren: 'Left' \n\t\t},\n\t\t{ \n\t\t\tprefix: textAlignCenterIcon,\n\t\t\tchildren: 'Center' \n\t\t},\n\t\t{ \n\t\t\tprefix: textAlignRightIcon,\n\t\t\tchildren: 'Right' \n\t\t}\n\t]}\n/>\n```\n\n### With Individual Click Handlers\n```svelte\n<script>\n\tfunction handleOption(option) {\n\t\tconsole.log(`Selected: ${option}`);\n\t}\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tchildren: 'Save',\n\t\t\tonclick: () => handleOption('save')\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Cancel',\n\t\t\tonclick: () => handleOption('cancel')\n\t\t}\n\t]}\n/>\n```\n\n### Disabled Group\n```svelte\n<ButtonGroup \n\tdisabled\n\titems={[\n\t\t{ children: 'Option 1' },\n\t\t{ children: 'Option 2' }\n\t]}\n/>\n```\n\n### Icon Only Buttons\n```svelte\n<script lang=\"ts\">\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { textBIcon } from 'entasis/icons/textB';\n\timport { textItalicIcon } from 'entasis/icons/textItalic';\n\timport { textUnderlineIcon } from 'entasis/icons/textUnderline';\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textBIcon\n\t\t},\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textItalicIcon\n\t\t},\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textUnderlineIcon\n\t\t}\n\t]}\n/>\n```\n\n### Mixed Button States\n```svelte\n<ButtonGroup \n\tvariant=\"outline\"\n\titems={[\n\t\t{ children: 'Active', color: 'primary' },\n\t\t{ children: 'Default', color: 'neutral' },\n\t\t{ children: 'Disabled', disabled: true }\n\t]}\n/>\n```\n\n### Segmented Control\n```svelte\n<script>\n\tlet selected = $state('week');\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tchildren: 'Day',\n\t\t\tvariant: selected === 'day' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'day'\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Week',\n\t\t\tvariant: selected === 'week' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'week'\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Month',\n\t\t\tvariant: selected === 'month' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'month'\n\t\t}\n\t]}\n/>\n```\n\n## Styling\n\nButtonGroup automatically:\n- Removes border-radius from middle buttons\n- Adjusts borders to prevent double borders\n- Creates a cohesive, connected appearance\n- Maintains consistent spacing\n\n## Accessibility\n\n- Each button maintains full keyboard accessibility\n- Focus styles are preserved\n- Disabled state cascades properly\n- Screen readers announce each button individually\n\n## Notes\n\n- Individual button props override shared props\n- Buttons are rendered in the order provided\n- The group container can be styled with the `class` prop\n- All Button component features are supported for individual buttons\n\n## Theme Customization\n\nThe ButtonGroup component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **root**: Main button group container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for button group container (handles border radius and border connections between buttons)\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<ButtonGroup \n items={buttons}\n theme={{\n root: {\n base: 'flex items-center rounded-lg overflow-hidden'\n }\n }}\n/>\n```\n\n**Custom Group Styling**:\n```svelte\n<ButtonGroup \n items={buttons}\n theme={{\n root: {\n base: 'flex items-center gap-0 border-2 border-primary rounded-lg overflow-hidden'\n }\n }}\n/>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setButtonGroupTheme } from './index.ts';\n \n setButtonGroupTheme({\n root: {\n base: 'flex items-center first-child:rounded-r-none last-child:rounded-l-none'\n }\n });\n</script>\n```\n";
|
|
@@ -18,14 +18,20 @@ The ButtonGroup component displays a collection of related buttons as a cohesive
|
|
|
18
18
|
## Props
|
|
19
19
|
|
|
20
20
|
### Core Props
|
|
21
|
-
|
|
21
|
+
Pass either \`items\` or \`children\`.
|
|
22
|
+
- **items**: Array<ButtonProps> - Array of button configurations
|
|
22
23
|
- Each button can have all standard Button component props
|
|
24
|
+
- **children**: Snippet - Buttons composed directly, for content an item object cannot describe
|
|
25
|
+
(a Tooltip or Popover trigger, a Button with a custom body). Each direct child is joined to its
|
|
26
|
+
neighbours; set size, color and variant on each Button, since the shared props below apply to
|
|
27
|
+
\`items\` only.
|
|
28
|
+
- **label**: string - Accessible name of the group (the root has \`role="group"\`)
|
|
23
29
|
|
|
24
|
-
### Shared Button Props
|
|
30
|
+
### Shared Button Props (items)
|
|
25
31
|
- **size**: 'small' | 'normal' | 'large' - Applied to all buttons in the group
|
|
26
32
|
- **color**: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Shared color for all buttons
|
|
27
33
|
- **variant**: 'solid' | 'outline' | 'soft' | 'ghost' | 'link' - Shared variant for all buttons
|
|
28
|
-
- **disabled**: boolean - Disables all buttons in the group
|
|
34
|
+
- **disabled**: boolean - Disables all buttons in the group, even one whose item sets \`disabled: false\`
|
|
29
35
|
|
|
30
36
|
### Styling Props
|
|
31
37
|
- **class**: string - Additional CSS classes for the group container
|
|
@@ -43,6 +49,20 @@ The ButtonGroup component displays a collection of related buttons as a cohesive
|
|
|
43
49
|
|
|
44
50
|
## Examples
|
|
45
51
|
|
|
52
|
+
### Composed Children
|
|
53
|
+
\`\`\`svelte
|
|
54
|
+
<script lang="ts">
|
|
55
|
+
import { Button } from '../Button/index.ts';
|
|
56
|
+
import { ButtonGroup } from './index.ts';
|
|
57
|
+
import { Tooltip } from '../Tooltip/index.ts';
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<ButtonGroup label="History">
|
|
61
|
+
<Button variant="outline">Undo</Button>
|
|
62
|
+
<Tooltip content="Redo the last change" trigger={{ content: 'Redo', variant: 'outline' }} />
|
|
63
|
+
</ButtonGroup>
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
46
66
|
### Basic Button Group
|
|
47
67
|
\`\`\`svelte
|
|
48
68
|
<ButtonGroup
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { Sizes, Colors } from '../../types/theme.js';
|
|
2
3
|
import type { WithAttachments } from '../../types/props.js';
|
|
3
4
|
import type { ButtonProps, ButtonVariant } from '../Button/index.js';
|
|
4
5
|
import type { ButtonGroupThemeProps } from './buttonGroup.theme.js';
|
|
5
|
-
|
|
6
|
+
type ButtonGroupContent = {
|
|
6
7
|
/** Items rendered in order inside the group. */
|
|
7
8
|
items: ButtonProps[];
|
|
8
|
-
|
|
9
|
+
children?: never;
|
|
10
|
+
} | {
|
|
11
|
+
items?: never;
|
|
12
|
+
/**
|
|
13
|
+
* Buttons (or Tooltip and Popover triggers, which render one) composed directly. Each
|
|
14
|
+
* direct child is joined to its neighbours; set size, color and variant on each one.
|
|
15
|
+
*/
|
|
16
|
+
children: Snippet;
|
|
17
|
+
};
|
|
18
|
+
export type ButtonGroupProps = WithAttachments<ButtonGroupContent & {
|
|
19
|
+
/** Accessible name of the group. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Size applied to every item in the group. */
|
|
9
22
|
size?: Sizes;
|
|
10
|
-
/** Color applied to every
|
|
23
|
+
/** Color applied to every item in the group. */
|
|
11
24
|
color?: Colors;
|
|
12
|
-
/** Visual variant applied to every
|
|
25
|
+
/** Visual variant applied to every item in the group. */
|
|
13
26
|
variant?: ButtonVariant;
|
|
14
|
-
/** When true, disables
|
|
27
|
+
/** When true, disables every item in the group, whatever the item says. */
|
|
15
28
|
disabled?: boolean;
|
|
16
29
|
/** Class name on the root group container element. */
|
|
17
30
|
class?: string;
|
|
18
31
|
/** Theme overrides for the group container layout. */
|
|
19
32
|
theme?: ButtonGroupThemeProps;
|
|
20
33
|
}>;
|
|
34
|
+
export {};
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
children (and calls `replaceChildren` when it swaps renderers), so the button is a
|
|
141
141
|
sibling of the host inside this box, not a child of it.
|
|
142
142
|
-->
|
|
143
|
-
<div class={chart.plotClass}>
|
|
143
|
+
<div class={chart.plotClass} style={chart.plotStyle}>
|
|
144
144
|
<div {@attach chart.host} data-chart-host class="absolute inset-0">
|
|
145
145
|
<!-- Markup serialized by the chart engine itself from the typed mark specs, never user HTML. -->
|
|
146
146
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
@@ -261,6 +261,7 @@ function compileAreaSeries(data, mark, path, gradients, line, fallbackSeries, fo
|
|
|
261
261
|
const curve = line?.curve ?? mark.curve;
|
|
262
262
|
const curveFactory = curve ? compileChartCurve(curve, `${path}.curve`) : undefined;
|
|
263
263
|
const lineCurve = curveFactory ? d3Curve(curveFactory) : undefined;
|
|
264
|
+
const areaGradients = mark.areaFill === 'solid' ? undefined : gradients;
|
|
264
265
|
const style = {
|
|
265
266
|
...compileMarkChannels(mark, fallbackSeries),
|
|
266
267
|
fill: compileColorVisual(mark.fill),
|
|
@@ -277,7 +278,7 @@ function compileAreaSeries(data, mark, path, gradients, line, fallbackSeries, fo
|
|
|
277
278
|
layout: compileStackLayout(mark.layout, `${path}.layout`),
|
|
278
279
|
curve: lineCurve
|
|
279
280
|
});
|
|
280
|
-
const area = applyAreaPresentation(compiled, 'vertical',
|
|
281
|
+
const area = applyAreaPresentation(compiled, 'vertical', areaGradients, {
|
|
281
282
|
curve: lineCurve,
|
|
282
283
|
strokeOpacity: line?.strokeOpacity ?? mark.strokeOpacity,
|
|
283
284
|
strokeDasharray: line?.strokeDasharray ?? mark.strokeDasharray,
|
|
@@ -302,7 +303,7 @@ function compileAreaSeries(data, mark, path, gradients, line, fallbackSeries, fo
|
|
|
302
303
|
layout: compileStackLayout(mark.layout, `${path}.layout`),
|
|
303
304
|
curve: curveFactory ? d3AreaXCurve(curveFactory) : undefined
|
|
304
305
|
});
|
|
305
|
-
const area = applyAreaPresentation(compiled, 'horizontal',
|
|
306
|
+
const area = applyAreaPresentation(compiled, 'horizontal', areaGradients, {
|
|
306
307
|
curve: lineCurve,
|
|
307
308
|
strokeOpacity: line?.strokeOpacity ?? mark.strokeOpacity,
|
|
308
309
|
strokeDasharray: line?.strokeDasharray ?? mark.strokeDasharray,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitting a layout to its plot. Tree, network and sankey layouts place nodes along an axis at a
|
|
3
|
+
* fraction of the available length, and polar charts draw their angle labels a fixed distance
|
|
4
|
+
* outside the circle; either way the labels hang off the geometry by a known amount. Sizing the
|
|
5
|
+
* geometry to those extents fills the plot instead of reserving guessed margins.
|
|
6
|
+
*/
|
|
7
|
+
/** Space kept between the outermost node or label and the plot edge, in px. */
|
|
8
|
+
export declare const CHART_FIT_MARGIN = 2;
|
|
9
|
+
/**
|
|
10
|
+
* How far a label's line box reaches above and below its anchor, as shares of the font size, for
|
|
11
|
+
* each SVG baseline the charts use.
|
|
12
|
+
*/
|
|
13
|
+
export declare const LABEL_LINE_BOX: {
|
|
14
|
+
readonly middle: {
|
|
15
|
+
readonly above: 0.65;
|
|
16
|
+
readonly below: 0.55;
|
|
17
|
+
};
|
|
18
|
+
readonly auto: {
|
|
19
|
+
readonly above: 0.95;
|
|
20
|
+
readonly below: 0.25;
|
|
21
|
+
};
|
|
22
|
+
readonly hanging: {
|
|
23
|
+
readonly above: 0.15;
|
|
24
|
+
readonly below: 1.05;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type RelationAxisItem = {
|
|
28
|
+
/** Position along the axis, 0 at the start of the span and 1 at its end. */
|
|
29
|
+
fraction: number;
|
|
30
|
+
/** Extent drawn before the node's anchor: its radius, or a label on that side. */
|
|
31
|
+
before: number;
|
|
32
|
+
/** Extent drawn after the node's anchor. */
|
|
33
|
+
after: number;
|
|
34
|
+
};
|
|
35
|
+
export type RelationAxisFit = {
|
|
36
|
+
/** Where fraction 0 lands. */
|
|
37
|
+
start: number;
|
|
38
|
+
/** Length covered by fractions 0 to 1. */
|
|
39
|
+
span: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The largest span whose items, with their extents, stay within `[margin, length - margin]`.
|
|
43
|
+
* Any two items bound it: the distance from the earlier item's leading extent to the later
|
|
44
|
+
* item's trailing extent must fit the plot.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fitAxis(items: readonly RelationAxisItem[], length: number, margin?: number): RelationAxisFit;
|
|
47
|
+
/** `value` within `[min, max]` as a 0–1 fraction; a single position sits in the middle. */
|
|
48
|
+
export declare function axisFraction(value: number, min: number, max: number): number;
|
|
49
|
+
/**
|
|
50
|
+
* A deterministic width estimate for a chart label, so layouts match between the server and
|
|
51
|
+
* the browser. Per-character advances approximate a UI sans-serif and err on the wide side.
|
|
52
|
+
*/
|
|
53
|
+
export declare function estimateLabelWidth(label: string, fontSize: number, fontWeight: number): number;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitting a layout to its plot. Tree, network and sankey layouts place nodes along an axis at a
|
|
3
|
+
* fraction of the available length, and polar charts draw their angle labels a fixed distance
|
|
4
|
+
* outside the circle; either way the labels hang off the geometry by a known amount. Sizing the
|
|
5
|
+
* geometry to those extents fills the plot instead of reserving guessed margins.
|
|
6
|
+
*/
|
|
7
|
+
/** Space kept between the outermost node or label and the plot edge, in px. */
|
|
8
|
+
export const CHART_FIT_MARGIN = 2;
|
|
9
|
+
/**
|
|
10
|
+
* How far a label's line box reaches above and below its anchor, as shares of the font size, for
|
|
11
|
+
* each SVG baseline the charts use.
|
|
12
|
+
*/
|
|
13
|
+
export const LABEL_LINE_BOX = {
|
|
14
|
+
middle: { above: 0.65, below: 0.55 },
|
|
15
|
+
auto: { above: 0.95, below: 0.25 },
|
|
16
|
+
hanging: { above: 0.15, below: 1.05 }
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The largest span whose items, with their extents, stay within `[margin, length - margin]`.
|
|
20
|
+
* Any two items bound it: the distance from the earlier item's leading extent to the later
|
|
21
|
+
* item's trailing extent must fit the plot.
|
|
22
|
+
*/
|
|
23
|
+
export function fitAxis(items, length, margin = CHART_FIT_MARGIN) {
|
|
24
|
+
const room = Math.max(1, length - margin * 2);
|
|
25
|
+
if (items.length === 0)
|
|
26
|
+
return { start: margin, span: room };
|
|
27
|
+
// Items sharing a fraction only ever contribute their largest extents.
|
|
28
|
+
const byFraction = new Map();
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
const current = byFraction.get(item.fraction);
|
|
31
|
+
byFraction.set(item.fraction, {
|
|
32
|
+
before: Math.max(current?.before ?? 0, item.before),
|
|
33
|
+
after: Math.max(current?.after ?? 0, item.after)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const stops = [...byFraction].map(([fraction, extent]) => ({ fraction, ...extent }));
|
|
37
|
+
let span = room;
|
|
38
|
+
for (const earlier of stops) {
|
|
39
|
+
for (const later of stops) {
|
|
40
|
+
const distance = later.fraction - earlier.fraction;
|
|
41
|
+
if (distance <= 0)
|
|
42
|
+
continue;
|
|
43
|
+
span = Math.min(span, (room - earlier.before - later.after) / distance);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Labels wider than the plot cannot fit at any span; keep the layout visible regardless.
|
|
47
|
+
span = Math.max(1, span);
|
|
48
|
+
const start = Math.max(...stops.map((stop) => margin + stop.before - stop.fraction * span));
|
|
49
|
+
// A span limited by two items touches both edges. One that is not (a single position, or
|
|
50
|
+
// extents too small to matter) leaves room at the end; split it so the layout stays centred.
|
|
51
|
+
const end = Math.max(...stops.map((stop) => start + stop.fraction * span + stop.after));
|
|
52
|
+
return { start: start + Math.max(0, length - margin - end) / 2, span };
|
|
53
|
+
}
|
|
54
|
+
/** `value` within `[min, max]` as a 0–1 fraction; a single position sits in the middle. */
|
|
55
|
+
export function axisFraction(value, min, max) {
|
|
56
|
+
return max > min ? (value - min) / (max - min) : 0.5;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A deterministic width estimate for a chart label, so layouts match between the server and
|
|
60
|
+
* the browser. Per-character advances approximate a UI sans-serif and err on the wide side.
|
|
61
|
+
*/
|
|
62
|
+
export function estimateLabelWidth(label, fontSize, fontWeight) {
|
|
63
|
+
let width = 0;
|
|
64
|
+
for (const character of label) {
|
|
65
|
+
if (/[ilj.,:;'!|()[\]\s]/.test(character))
|
|
66
|
+
width += 0.3;
|
|
67
|
+
else if (/[ftr"-]/.test(character))
|
|
68
|
+
width += 0.4;
|
|
69
|
+
else if (/[mwMW@%]/.test(character))
|
|
70
|
+
width += 0.88;
|
|
71
|
+
else if (/[A-Z]/.test(character))
|
|
72
|
+
width += 0.68;
|
|
73
|
+
else if (/[0-9]/.test(character))
|
|
74
|
+
width += 0.6;
|
|
75
|
+
else if (/[a-z]/.test(character))
|
|
76
|
+
width += 0.56;
|
|
77
|
+
else
|
|
78
|
+
width += 0.8;
|
|
79
|
+
}
|
|
80
|
+
return width * fontSize * (fontWeight >= 600 ? 1.06 : 1);
|
|
81
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const chartDescription = "\n# Chart Component\n\nChart renders layered cartesian, polar, relation, or faceted marks from one typed data array. Consumers import only from `entasis/chart`; TanStack Charts and D3 stay private implementation details of the component, but they must be installed as optional peer dependencies.\n\n## Requires\n\nChart renders through TanStack Charts and D3. Those packages are optional peer dependencies of entasis, so install them alongside it:\n\n`pnpm add @tanstack/charts d3-array d3-force d3-hierarchy d3-sankey d3-scale d3-shape`\n\nOne public type (`curve`) is D3's `CurveFactory`, so TypeScript users add its typings: `pnpm add -D @types/d3-shape`.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { Chart } from './index.ts'\n\n type Row = { month: Date; actual: number; forecast: number; low: number; high: number }\n const x = { scale: { type: 'utc' }, axis: { label: 'Month' } } as const\n const y = { scale: { type: 'linear' }, grid: true } as const\n const marks = [\n {\n type: 'series',\n x: 'month',\n y: 'forecast',\n interval: { lower: 'low', upper: 'high', fill: 'secondary' },\n analysis: [\n { type: 'reference', statistic: 'median' },\n { type: 'rolling', statistic: 'mean', window: 3 }\n ]\n },\n { type: 'series', x: 'month', y: 'actual', stroke: 'primary', points: true }\n ] as const\n</script>\n\n<Chart\n data={rows}\n {x}\n {y}\n {marks}\n tooltip\n viewport\n label=\"Monthly revenue\"\n height={320}\n/>\n```\n\n## Contract\n\n- `data` is one immutable array shared by every mark.\n- `marks` is a required non-empty discriminated union; array order is paint order.\n- A `series` mark renders a line by default. Set `area`, `points`, or `line` with booleans or local option objects to compose its visible layers.\n- A series `interval` adds a non-interactive band behind the same line. Its required `lower` and `upper` numeric channels represent explicit bounds such as confidence, prediction, credible, or min/max intervals. `interval` and `area` are mutually exclusive because both own the filled surface.\n- `analysis` is a non-empty list of derived statistical layers owned by a series, scatter, bar, or distribution mark. Reference analysis supports mean, median, quantile, and standard deviation. Series and scatter support linear regression with optional confidence or prediction intervals. Series also supports rolling mean and rolling median. Analysis can use the complete plot or each series independently and does not add tooltip points. Stacked layouts reject analysis because their displayed values differ from the source channels.\n- A `scatter` mark renders independent observations with the default `points` variant. A numeric `size` is a constant pixel radius. A `size` data channel uses `sqrt` by default; `sizeScale` accepts `linear`, `sqrt`, `log`, `exp`, or an object with `type`, `domain`, `range`, and an optional `base` for logarithmic or exponential scales. The `hexbin` variant accepts numeric `x` and `y` channels and aggregates dense observations into responsive pixel-space hexagons; `radius` controls the bin size.\n- A `bar` mark is simple by default. Its optional `variant` is `group` or `stack`. Use `offset: 'normalize'` on a stacked bar to compare proportions with a 0\u20131 value axis. A stacked bar also accepts `gap` (pixels of surface between consecutive segments of one stack).\n- Wide data: a stacked bar, and an area series with `layout: { type: 'stack' }`, accept a list of numeric fields as their value channel (`y: ['completed', 'inProgress', 'pending']`, or `x` when horizontal). The rows are melted internally into one series per field, the field name becoming the series key, in the order the fields are listed. A wide mark owns its series identity, so it takes no `series` or `colorBy`, and rejects `annotations` and `analysis`. Long format with a `series` channel keeps working unchanged.\n- A `matrix` mark uses the default `grid` variant with categorical `x` and `y` channels. Use `colorBy` for a categorical matrix, or use a numeric `value` with `color` for a heatmap. The `calendar` variant replaces `x` and `y` with a `date` channel and derives week and weekday axes automatically. Its optional `colorScale` accepts a `quantize` domain and an ordered color range.\n- A data mark can own `arrow`, `label`, `rule`, `band`, and `marker` annotations. Targets resolve against the parent mark's final rendered coordinates. A band annotation uses `thickness` for its cross-axis size.\n- Position scales use local string discriminants such as `linear`, `utc`, and `band`.\n- `tooltip: true` groups cartesian marks on the categorical or x axis, renders native color rows, and stays inside the chart surface without adding chart focus states.\n- `palette` is either an ordered list consumed in series-discovery order, or a record keyed by series key (`{ completed: 'success', pending: 'danger' }`). Record entries win by key; a series without an entry falls back to the default palette in discovery order.\n- `ChartColor` accepts the seven semantic roles and the surface family (`surface`, `surface-recessed`, `surface-canvas`, `surface-raised`, `surface-floating`); anything else is passed through as a CSS color.\n- `legend: true` shows a categorical color key, or a numeric heatmap/hexbin color ramp. Use `legend: { interactive: true }` to hide and show series, bars, scatter points, and empirical distributions whose series and color identities match. Other layouts use static legends; facets keep their own labels. Visibility preserves axes, colors, and stack totals. The object accepts `placement: 'top' | 'bottom'`, `label`, controlled `value` and `onValueChange`, or an initial `defaultValue`; omitted visibility shows every series.\n- `legend.format: (key) => string` sets the display text of a series independently of its key. The same formatter labels the series in the tooltip.\n- A categorical legend is drawn by the library, not by the rendering engine: the interactive one is a `ToggleButtonGroup` of small ghost toggles, one per series, each carrying a color swatch of the resolved series color and the `legend.format` label, pressed when the series is visible; the static one is the same swatch and label as plain items. It takes its own row above or below the plot and the plot shrinks by that row. A numeric legend stays a color ramp drawn inside the plot. Style the row with the `legend`, `legendItem`, and `legendSwatch` theme parts.\n- Legend layout accepts `align: 'left' | 'center' | 'right'` and `orientation: 'horizontal' | 'vertical'`. Vertical stacks categorical entries in one column; numeric color ramps stay horizontal. Both static and interactive legends support alignment and top/bottom placement.\n- Histogram and rolling preparation use native transforms. Regression uses native Student-t confidence bounds; prediction intervals add observation uncertainty to the fitted-mean bounds. Small samples therefore have wider intervals than a fixed normal approximation.\n- Tooltip objects can override grouping with `groupBy: 'x'`, `groupBy: 'y'`, or `groupBy: false`.\n- A tooltip can be pinned: `tooltip.value` / `tooltip.defaultValue` take the row key of the pinned datum (the mark's `key` channel when it has one, its x value otherwise; `null` pins nothing). A pinned row shows its tooltip on mount, hover moves the tooltip normally, pointer leave restores the pinned row, and clicking a datum pins it (clicking it again unpins) through `tooltip.onValueChange`. Pinning is disabled while `viewport` owns the press gesture.\n- `viewport: true` enables native x-axis brush zoom with pointer and touch input, an accessible reset control, and a reduced-motion-aware transition. Numeric and date axes select continuous windows; categorical axes snap to values and also expose keyboard handles. The object form accepts `reset` and `transition` options. Native 2D brushing is not supported.\n- A `distribution` mark reads one categorical `group` channel and one numeric `value` channel. Its summary variants are `violin`, `box`, and `error-bar`. Its raw-sample variants are `histogram`, `density`, and `ecdf`. Histogram accepts `bins`; density accepts `bandwidth` and `samples`; `direction` can be `vertical` or `horizontal`.\n- Distribution tooltips use a built-in statistical summary. They do not accept custom `tooltip.fields`.\n- A `proportion` mark reads one categorical `category` channel and one non-negative numeric `value` channel. Its `variant` is `pie`, `donut`, or `waffle`; consumers do not calculate angles or cells.\n- A `polar` mark reads `angle` and `radius` channels. The `circular` and `radar` variants compose boolean `area`, `line`, and `points` layers. The `radial-bar` and `rose` variants render wedges and expose only bar options.\n- A `relation` mark owns a complete topology layout. Its `tree` variant reads `nodeId` and `parent`; its `network` and `sankey` variants read `nodeId` and an outgoing `relations` channel. Relation marks cannot use axes, sibling marks, facets, or custom tooltip fields.\n- A `facet` mark owns nested `marks` with the same public union and inherits the plot positions.\n- Proportion tooltips show the category value and its share. They do not accept custom `tooltip.fields` or grouped axes.\n- `label` is required and `ariaDescription` is optional.\n- Sizing has one input: `height` (pixels) or `aspectRatio`. Either one sizes the plot and the server-rendered SVG (laid out at 800px wide), and they cannot be combined. With neither, the root class owns the height (320px by default, replaced by a height class on `class`) and SSR emits a stable empty host whose SVG mounts only in the browser.\n- Replace `data`, `marks`, or another configuration prop to update a mounted chart. In-place mutation is not an update contract.\n- Configuration errors throw a prefixed `TypeError`; dependency and accessor errors propagate.\n\n## Motion\n\n- **motion** theme slot: one preset (no variants) whose `duration` / `easing` are the default\n timing of the viewport zoom settle; `viewport.transition` still overrides per chart.\n- Ladder: `<Theme components={{ chart: { motion } }}>` \u2192 `setChartTheme({ motion })` \u2192\n `theme.motion`. Reduced motion collapses the duration to 0 and the chart snaps.\n";
|
|
1
|
+
export declare const chartDescription = "\n# Chart Component\n\nChart renders layered cartesian, polar, relation, or faceted marks from one typed data array. Consumers import only from `entasis/chart`; TanStack Charts and D3 stay private implementation details of the component, but they must be installed as optional peer dependencies.\n\n## Requires\n\nChart renders through TanStack Charts and D3. Those packages are optional peer dependencies of entasis, so install them alongside it:\n\n`pnpm add @tanstack/charts d3-array d3-force d3-hierarchy d3-sankey d3-scale d3-shape`\n\nOne public type (`curve`) is D3's `CurveFactory`, so TypeScript users add its typings: `pnpm add -D @types/d3-shape`.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { Chart } from './index.ts'\n\n type Row = { month: Date; actual: number; forecast: number; low: number; high: number }\n const x = { scale: { type: 'utc' }, axis: { label: 'Month' } } as const\n const y = { scale: { type: 'linear' }, grid: true } as const\n const marks = [\n {\n type: 'series',\n x: 'month',\n y: 'forecast',\n interval: { lower: 'low', upper: 'high', fill: 'secondary' },\n analysis: [\n { type: 'reference', statistic: 'median' },\n { type: 'rolling', statistic: 'mean', window: 3 }\n ]\n },\n { type: 'series', x: 'month', y: 'actual', stroke: 'primary', points: true }\n ] as const\n</script>\n\n<Chart\n data={rows}\n {x}\n {y}\n {marks}\n tooltip\n viewport\n label=\"Monthly revenue\"\n height={320}\n/>\n```\n\n## Contract\n\n- `data` is one immutable array shared by every mark.\n- `marks` is a required non-empty discriminated union; array order is paint order.\n- A `series` mark renders a line by default. Set `area`, `points`, or `line` with booleans or local option objects to compose its visible layers.\n- An area is filled with a gradient of the series colour along its value axis. `areaFill: 'solid'` paints one flat fill at `fillOpacity` (20% by default) instead.\n- A series `interval` adds a non-interactive band behind the same line. Its required `lower` and `upper` numeric channels represent explicit bounds such as confidence, prediction, credible, or min/max intervals. `interval` and `area` are mutually exclusive because both own the filled surface.\n- `analysis` is a non-empty list of derived statistical layers owned by a series, scatter, bar, or distribution mark. Reference analysis supports mean, median, quantile, and standard deviation. Series and scatter support linear regression with optional confidence or prediction intervals. Series also supports rolling mean and rolling median. Analysis can use the complete plot or each series independently and does not add tooltip points. Stacked layouts reject analysis because their displayed values differ from the source channels.\n- A `scatter` mark renders independent observations with the default `points` variant. A numeric `size` is a constant pixel radius. A `size` data channel uses `sqrt` by default; `sizeScale` accepts `linear`, `sqrt`, `log`, `exp`, or an object with `type`, `domain`, `range`, and an optional `base` for logarithmic or exponential scales. The `hexbin` variant accepts numeric `x` and `y` channels and aggregates dense observations into responsive pixel-space hexagons; `radius` controls the bin size.\n- A `bar` mark is simple by default. Its optional `variant` is `group` or `stack`. Use `offset: 'normalize'` on a stacked bar to compare proportions with a 0\u20131 value axis. A stacked bar also accepts `gap` (pixels of surface between consecutive segments of one stack).\n- Wide data: a stacked bar, and an area series with `layout: { type: 'stack' }`, accept a list of numeric fields as their value channel (`y: ['completed', 'inProgress', 'pending']`, or `x` when horizontal). The rows are melted internally into one series per field, the field name becoming the series key, in the order the fields are listed. A wide mark owns its series identity, so it takes no `series` or `colorBy`, and rejects `annotations` and `analysis`. Long format with a `series` channel keeps working unchanged.\n- A `matrix` mark uses the default `grid` variant with categorical `x` and `y` channels. Use `colorBy` for a categorical matrix, or use a numeric `value` with `color` for a heatmap. The `calendar` variant replaces `x` and `y` with a `date` channel and derives week and weekday axes automatically. Its optional `colorScale` accepts a `quantize` domain and an ordered color range.\n- A data mark can own `arrow`, `label`, `rule`, `band`, and `marker` annotations. Targets resolve against the parent mark's final rendered coordinates. A band annotation uses `thickness` for its cross-axis size.\n- Position scales use local string discriminants such as `linear`, `utc`, and `band`.\n- `tooltip: true` groups cartesian marks on the categorical or x axis, renders native color rows, and stays inside the chart surface without adding chart focus states.\n- `palette` is either an ordered list consumed in series-discovery order, or a record keyed by series key (`{ completed: 'success', pending: 'danger' }`). Record entries win by key; a series without an entry falls back to the default palette in discovery order.\n- `ChartColor` accepts the seven semantic roles and the surface family (`surface`, `surface-recessed`, `surface-canvas`, `surface-raised`, `surface-floating`); anything else is passed through as a CSS color.\n- `legend: true` shows a categorical color key, or a numeric heatmap/hexbin color ramp. Use `legend: { interactive: true }` to hide and show series, bars, scatter points, and empirical distributions whose series and color identities match. Other layouts use static legends; facets keep their own labels. Visibility preserves axes, colors, and stack totals. The object accepts `placement: 'top' | 'bottom'`, `label`, controlled `value` and `onValueChange`, or an initial `defaultValue`; omitted visibility shows every series.\n- `legend.format: (key) => string` sets the display text of a series independently of its key. The same formatter labels the series in the tooltip.\n- A categorical legend is drawn by the library, not by the rendering engine: the interactive one is a `ToggleButtonGroup` of small ghost toggles, one per series, each carrying a color swatch of the resolved series color and the `legend.format` label, pressed when the series is visible; the static one is the same swatch and label as plain items. It takes its own row above or below the plot and the plot shrinks by that row. A numeric legend stays a color ramp drawn inside the plot. Style the row with the `legend`, `legendItem`, and `legendSwatch` theme parts.\n- Legend layout accepts `align: 'left' | 'center' | 'right'` and `orientation: 'horizontal' | 'vertical'`. Vertical stacks categorical entries in one column; numeric color ramps stay horizontal. Both static and interactive legends support alignment and top/bottom placement.\n- Histogram and rolling preparation use native transforms. Regression uses native Student-t confidence bounds; prediction intervals add observation uncertainty to the fitted-mean bounds. Small samples therefore have wider intervals than a fixed normal approximation.\n- Tooltip objects can override grouping with `groupBy: 'x'`, `groupBy: 'y'`, or `groupBy: false`.\n- A tooltip can be pinned: `tooltip.value` / `tooltip.defaultValue` take the row key of the pinned datum (the mark's `key` channel when it has one, its x value otherwise; `null` pins nothing). A pinned row shows its tooltip on mount, hover moves the tooltip normally, pointer leave restores the pinned row, and clicking a datum pins it (clicking it again unpins) through `tooltip.onValueChange`. Pinning is disabled while `viewport` owns the press gesture.\n- `viewport: true` enables native x-axis brush zoom with pointer and touch input, an accessible reset control, and a reduced-motion-aware transition. Numeric and date axes select continuous windows; categorical axes snap to values and also expose keyboard handles. The object form accepts `reset` and `transition` options. Native 2D brushing is not supported.\n- A `distribution` mark reads one categorical `group` channel and one numeric `value` channel. Its summary variants are `violin`, `box`, and `error-bar`. Its raw-sample variants are `histogram`, `density`, and `ecdf`. Histogram accepts `bins`; density accepts `bandwidth` and `samples`; `direction` can be `vertical` or `horizontal`.\n- Distribution tooltips use a built-in statistical summary. They do not accept custom `tooltip.fields`.\n- A `proportion` mark reads one categorical `category` channel and one non-negative numeric `value` channel. Its `variant` is `pie`, `donut`, or `waffle`; consumers do not calculate angles or cells.\n- A `polar` mark reads `angle` and `radius` channels. The `circular` and `radar` variants compose boolean `area`, `line`, and `points` layers. The `radial-bar` and `rose` variants render wedges and expose only bar options.\n- A `relation` mark owns a complete topology layout. Its `tree` variant reads `nodeId` and `parent`; its `network` and `sankey` variants read `nodeId` and an outgoing `relations` channel. Relation marks cannot use axes, sibling marks, facets, or custom tooltip fields.\n- A `facet` mark owns nested `marks` with the same public union and inherits the plot positions.\n- Proportion tooltips show the category value and its share. They do not accept custom `tooltip.fields` or grouped axes.\n- `label` is required and `ariaDescription` is optional.\n- Sizing has one input: `height` (pixels) or `aspectRatio`. Either one sizes the plot and the server-rendered SVG (laid out at 800px wide), and they cannot be combined. A legend row sits outside the plot and adds to the chart's height. With neither, the root class owns the height (320px by default, replaced by a height class on `class`) and SSR emits a stable empty host whose SVG mounts only in the browser.\n- Replace `data`, `marks`, or another configuration prop to update a mounted chart. In-place mutation is not an update contract.\n- Configuration errors throw a prefixed `TypeError`; dependency and accessor errors propagate.\n\n## Motion\n\n- **motion** theme slot: one preset (no variants) whose `duration` / `easing` are the default\n timing of the viewport zoom settle; `viewport.transition` still overrides per chart.\n- Ladder: `<Theme components={{ chart: { motion } }}>` \u2192 `setChartTheme({ motion })` \u2192\n `theme.motion`. Reduced motion collapses the duration to 0 and the chart snaps.\n";
|
|
@@ -52,6 +52,7 @@ One public type (\`curve\`) is D3's \`CurveFactory\`, so TypeScript users add it
|
|
|
52
52
|
- \`data\` is one immutable array shared by every mark.
|
|
53
53
|
- \`marks\` is a required non-empty discriminated union; array order is paint order.
|
|
54
54
|
- A \`series\` mark renders a line by default. Set \`area\`, \`points\`, or \`line\` with booleans or local option objects to compose its visible layers.
|
|
55
|
+
- An area is filled with a gradient of the series colour along its value axis. \`areaFill: 'solid'\` paints one flat fill at \`fillOpacity\` (20% by default) instead.
|
|
55
56
|
- A series \`interval\` adds a non-interactive band behind the same line. Its required \`lower\` and \`upper\` numeric channels represent explicit bounds such as confidence, prediction, credible, or min/max intervals. \`interval\` and \`area\` are mutually exclusive because both own the filled surface.
|
|
56
57
|
- \`analysis\` is a non-empty list of derived statistical layers owned by a series, scatter, bar, or distribution mark. Reference analysis supports mean, median, quantile, and standard deviation. Series and scatter support linear regression with optional confidence or prediction intervals. Series also supports rolling mean and rolling median. Analysis can use the complete plot or each series independently and does not add tooltip points. Stacked layouts reject analysis because their displayed values differ from the source channels.
|
|
57
58
|
- A \`scatter\` mark renders independent observations with the default \`points\` variant. A numeric \`size\` is a constant pixel radius. A \`size\` data channel uses \`sqrt\` by default; \`sizeScale\` accepts \`linear\`, \`sqrt\`, \`log\`, \`exp\`, or an object with \`type\`, \`domain\`, \`range\`, and an optional \`base\` for logarithmic or exponential scales. The \`hexbin\` variant accepts numeric \`x\` and \`y\` channels and aggregates dense observations into responsive pixel-space hexagons; \`radius\` controls the bin size.
|
|
@@ -79,7 +80,7 @@ One public type (\`curve\`) is D3's \`CurveFactory\`, so TypeScript users add it
|
|
|
79
80
|
- A \`facet\` mark owns nested \`marks\` with the same public union and inherits the plot positions.
|
|
80
81
|
- Proportion tooltips show the category value and its share. They do not accept custom \`tooltip.fields\` or grouped axes.
|
|
81
82
|
- \`label\` is required and \`ariaDescription\` is optional.
|
|
82
|
-
- Sizing has one input: \`height\` (pixels) or \`aspectRatio\`. Either one sizes the plot and the server-rendered SVG (laid out at 800px wide), and they cannot be combined. With neither, the root class owns the height (320px by default, replaced by a height class on \`class\`) and SSR emits a stable empty host whose SVG mounts only in the browser.
|
|
83
|
+
- Sizing has one input: \`height\` (pixels) or \`aspectRatio\`. Either one sizes the plot and the server-rendered SVG (laid out at 800px wide), and they cannot be combined. A legend row sits outside the plot and adds to the chart's height. With neither, the root class owns the height (320px by default, replaced by a height class on \`class\`) and SSR emits a stable empty host whose SVG mounts only in the browser.
|
|
83
84
|
- Replace \`data\`, \`marks\`, or another configuration prop to update a mounted chart. In-place mutation is not an update contract.
|
|
84
85
|
- Configuration errors throw a prefixed \`TypeError\`; dependency and accessor errors propagate.
|
|
85
86
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {} from '@tanstack/charts';
|
|
2
2
|
import { angleGrid, polar, radialArea, radialBarRadius, radialDot, radialGrid, radialLine } from '@tanstack/charts/polar';
|
|
3
3
|
import { compileChannel, compileColor, compileColorVisual, compileMarkChannels } from './chart.channels.js';
|
|
4
|
+
import { CHART_FIT_MARGIN, estimateLabelWidth, LABEL_LINE_BOX } from './chart.fit.js';
|
|
4
5
|
import { compileChartCurve, compileChartScale } from './chart.scale.js';
|
|
5
6
|
export function compilePolarChartMark(data, mark, path) {
|
|
6
7
|
let marks;
|
|
@@ -26,9 +27,10 @@ export function compilePolarChartMark(data, mark, path) {
|
|
|
26
27
|
if (mark.domain && mark.radiusScale?.domain) {
|
|
27
28
|
throw new TypeError(`[Chart] ${path}.domain cannot be combined with ${path}.radiusScale.domain.`);
|
|
28
29
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const radiusRatio = mark.radiusRatio ?? 1;
|
|
31
|
+
// TanStack reads `inset` from these options on every render, which lets `fitPolarToPlot`
|
|
32
|
+
// size the circle to the plot it is drawn in.
|
|
33
|
+
const options = {
|
|
32
34
|
id: mark.id,
|
|
33
35
|
marks,
|
|
34
36
|
guides: compilePolarGuides(mark),
|
|
@@ -46,10 +48,15 @@ export function compilePolarChartMark(data, mark, path) {
|
|
|
46
48
|
},
|
|
47
49
|
startAngle: mark.startAngle,
|
|
48
50
|
endAngle: mark.endAngle,
|
|
49
|
-
inset,
|
|
51
|
+
inset: mark.inset ?? 24,
|
|
50
52
|
radiusRatio
|
|
53
|
+
};
|
|
54
|
+
return fitPolarToPlot(polar(options), {
|
|
55
|
+
options,
|
|
56
|
+
fit: mark.inset === undefined,
|
|
57
|
+
radar: mark.variant === 'radar',
|
|
58
|
+
shapeMargin: CHART_FIT_MARGIN + polarShapeOverhang(mark)
|
|
51
59
|
});
|
|
52
|
-
return mark.variant === 'radar' ? centerPolarPolygon(compiled, inset, radiusRatio) : compiled;
|
|
53
60
|
}
|
|
54
61
|
function compilePolarLayers(data, mark, path) {
|
|
55
62
|
const isDefault = mark.area === undefined && mark.line === undefined && mark.points === undefined;
|
|
@@ -135,7 +142,25 @@ function compilePolarGuides(mark) {
|
|
|
135
142
|
const shape = mark.variant === 'radar' ? 'polygon' : 'circle';
|
|
136
143
|
return [radialGrid({ ticks: 5, shape, strokeOpacity: 0.14 }), angleGrid({ strokeOpacity: 0.14 })];
|
|
137
144
|
}
|
|
138
|
-
|
|
145
|
+
/** What a polar mark draws beyond its outer radius: half its stroke, or its points. */
|
|
146
|
+
function polarShapeOverhang(mark) {
|
|
147
|
+
if (!isPolarPathMark(mark))
|
|
148
|
+
return (mark.strokeWidth ?? 0) / 2;
|
|
149
|
+
const line = Boolean(mark.line) ||
|
|
150
|
+
(mark.area === undefined && mark.line === undefined && mark.points === undefined);
|
|
151
|
+
return Math.max(line ? (mark.strokeWidth ?? 2.25) / 2 : 0, mark.points ? (mark.pointRadius ?? 3.5) : 0);
|
|
152
|
+
}
|
|
153
|
+
function isPolarPathMark(mark) {
|
|
154
|
+
return mark.variant === 'circular' || mark.variant === 'radar';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sizes a polar chart to its plot. The angle labels sit a fixed distance outside the circle, so
|
|
158
|
+
* how much room they need depends on where they fall: a label at twelve o'clock adds its height
|
|
159
|
+
* above the circle, a band label just past it adds less, and side labels add their width. A first
|
|
160
|
+
* render places the labels; the largest radius that keeps every label and the outer circle inside
|
|
161
|
+
* the plot is then solved directly, and the chart is rendered at that radius.
|
|
162
|
+
*/
|
|
163
|
+
function fitPolarToPlot(mark, fit) {
|
|
139
164
|
return {
|
|
140
165
|
...mark,
|
|
141
166
|
initialize(context) {
|
|
@@ -143,19 +168,28 @@ function centerPolarPolygon(mark, inset, radiusRatio) {
|
|
|
143
168
|
return {
|
|
144
169
|
...initialized,
|
|
145
170
|
render(renderContext) {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
171
|
+
const { chart } = renderContext;
|
|
172
|
+
const half = Math.min(chart.width, chart.height) / 2;
|
|
173
|
+
const ratio = fit.options.radiusRatio ?? 1;
|
|
174
|
+
const radiusFor = () => Math.max(0, half - (fit.options.inset ?? 0)) * ratio;
|
|
175
|
+
let rendered = initialized.render(renderContext);
|
|
176
|
+
const polygon = fit.radar ? radarPolygon(rendered) : null;
|
|
177
|
+
if (fit.fit) {
|
|
178
|
+
const radius = solvePolarRadius(rendered, chart, polygon, fit.shapeMargin, {
|
|
179
|
+
radius: radiusFor(),
|
|
180
|
+
rtl: renderContext.layout?.typography?.direction === 'rtl'
|
|
181
|
+
});
|
|
182
|
+
// The previous frame's inset usually fits already; render again only when not.
|
|
183
|
+
if (radius !== null && Math.abs(radius - radiusFor()) > 0.5) {
|
|
184
|
+
fit.options.inset = Math.max(0, half - radius / ratio);
|
|
185
|
+
rendered = initialized.render(renderContext);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!polygon)
|
|
149
189
|
return rendered;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
});
|
|
154
|
-
const xs = unitPoints.map(([x]) => x);
|
|
155
|
-
const ys = unitPoints.map(([, y]) => y);
|
|
156
|
-
const radius = Math.max(0, Math.min(renderContext.chart.width, renderContext.chart.height) / 2 - inset) * radiusRatio;
|
|
157
|
-
const translateX = -((Math.min(...xs) + Math.max(...xs)) / 2) * radius;
|
|
158
|
-
const translateY = -((Math.min(...ys) + Math.max(...ys)) / 2) * radius;
|
|
190
|
+
const radius = radiusFor();
|
|
191
|
+
const translateX = polygon.shiftX * radius;
|
|
192
|
+
const translateY = polygon.shiftY * radius;
|
|
159
193
|
return {
|
|
160
194
|
nodes: rendered.nodes.map((node) => node.kind === 'group'
|
|
161
195
|
? {
|
|
@@ -175,6 +209,91 @@ function centerPolarPolygon(mark, inset, radiusRatio) {
|
|
|
175
209
|
}
|
|
176
210
|
};
|
|
177
211
|
}
|
|
212
|
+
function radarPolygon(rendered) {
|
|
213
|
+
const angleValues = new Set((rendered.points ?? []).map((point) => chartValueIdentity(point.xValue)));
|
|
214
|
+
if (angleValues.size < 3)
|
|
215
|
+
return null;
|
|
216
|
+
const corners = Array.from({ length: angleValues.size }, (_value, index) => {
|
|
217
|
+
const angle = (index / angleValues.size) * Math.PI * 2;
|
|
218
|
+
return [Math.sin(angle), -Math.cos(angle)];
|
|
219
|
+
});
|
|
220
|
+
const xs = corners.map(([x]) => x);
|
|
221
|
+
const ys = corners.map(([, y]) => y);
|
|
222
|
+
return {
|
|
223
|
+
corners,
|
|
224
|
+
shiftX: -(Math.min(...xs) + Math.max(...xs)) / 2,
|
|
225
|
+
shiftY: -(Math.min(...ys) + Math.max(...ys)) / 2
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function solvePolarRadius(rendered, chart, polygon, shapeMargin, probe) {
|
|
229
|
+
const shape = (polygon?.corners ?? [
|
|
230
|
+
[0, -1],
|
|
231
|
+
[1, 0],
|
|
232
|
+
[0, 1],
|
|
233
|
+
[-1, 0]
|
|
234
|
+
]).map((direction) => ({
|
|
235
|
+
direction,
|
|
236
|
+
offset: 0,
|
|
237
|
+
left: shapeMargin,
|
|
238
|
+
right: shapeMargin,
|
|
239
|
+
up: shapeMargin,
|
|
240
|
+
down: shapeMargin
|
|
241
|
+
}));
|
|
242
|
+
const labels = collectLabels(rendered.nodes).flatMap(({ label, x, y }) => {
|
|
243
|
+
const distance = Math.hypot(x, y);
|
|
244
|
+
if (distance === 0)
|
|
245
|
+
return [];
|
|
246
|
+
const fontSize = label.fontSize ?? 12;
|
|
247
|
+
const width = estimateLabelWidth(label.text, fontSize, label.fontWeight ?? 400);
|
|
248
|
+
const anchor = probe.rtl && label.anchor !== 'middle'
|
|
249
|
+
? label.anchor === 'end'
|
|
250
|
+
? 'start'
|
|
251
|
+
: 'end'
|
|
252
|
+
: label.anchor;
|
|
253
|
+
const lineBox = LABEL_LINE_BOX[label.baseline ?? 'middle'];
|
|
254
|
+
// Labels sit `offset` px beyond the circle along their spoke; recover both from the render.
|
|
255
|
+
return [
|
|
256
|
+
{
|
|
257
|
+
direction: [x / distance, y / distance],
|
|
258
|
+
offset: distance - probe.radius,
|
|
259
|
+
left: (anchor === 'end' ? width : anchor === 'start' ? 0 : width / 2) + CHART_FIT_MARGIN,
|
|
260
|
+
right: (anchor === 'start' ? width : anchor === 'end' ? 0 : width / 2) + CHART_FIT_MARGIN,
|
|
261
|
+
up: lineBox.above * fontSize + CHART_FIT_MARGIN,
|
|
262
|
+
down: lineBox.below * fontSize + CHART_FIT_MARGIN
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
});
|
|
266
|
+
const halfWidth = chart.width / 2;
|
|
267
|
+
const halfHeight = chart.height / 2;
|
|
268
|
+
let radius = Infinity;
|
|
269
|
+
for (const extreme of [...shape, ...labels]) {
|
|
270
|
+
const [dx, dy] = extreme.direction;
|
|
271
|
+
const slopeX = dx + (polygon?.shiftX ?? 0);
|
|
272
|
+
const slopeY = dy + (polygon?.shiftY ?? 0);
|
|
273
|
+
const baseX = dx * extreme.offset;
|
|
274
|
+
const baseY = dy * extreme.offset;
|
|
275
|
+
if (slopeX > 0)
|
|
276
|
+
radius = Math.min(radius, (halfWidth - extreme.right - baseX) / slopeX);
|
|
277
|
+
if (slopeX < 0)
|
|
278
|
+
radius = Math.min(radius, (halfWidth - extreme.left + baseX) / -slopeX);
|
|
279
|
+
if (slopeY > 0)
|
|
280
|
+
radius = Math.min(radius, (halfHeight - extreme.down - baseY) / slopeY);
|
|
281
|
+
if (slopeY < 0)
|
|
282
|
+
radius = Math.min(radius, (halfHeight - extreme.up + baseY) / -slopeY);
|
|
283
|
+
}
|
|
284
|
+
return Number.isFinite(radius) && radius >= 1 ? radius : null;
|
|
285
|
+
}
|
|
286
|
+
/** Angle labels with positions relative to the plot centre, where the polar group is drawn. */
|
|
287
|
+
function collectLabels(nodes) {
|
|
288
|
+
const within = (children, x, y) => children.flatMap((node) => {
|
|
289
|
+
if (node.kind === 'label')
|
|
290
|
+
return [{ label: node, x: node.x + x, y: node.y + y }];
|
|
291
|
+
if (node.kind !== 'group')
|
|
292
|
+
return [];
|
|
293
|
+
return within(node.children, x + (node.translateX ?? 0), y + (node.translateY ?? 0));
|
|
294
|
+
});
|
|
295
|
+
return nodes.flatMap((node) => (node.kind === 'group' ? within(node.children, 0, 0) : []));
|
|
296
|
+
}
|
|
178
297
|
function chartValueIdentity(value) {
|
|
179
298
|
return value instanceof Date ? `date:${value.getTime()}` : `${typeof value}:${String(value)}`;
|
|
180
299
|
}
|
|
@@ -13,7 +13,12 @@ type ChartPolarBase<TRow> = ChartDataMarkProps<TRow> & ChartSeriesChannels<TRow>
|
|
|
13
13
|
color?: ChartColor;
|
|
14
14
|
startAngle?: number;
|
|
15
15
|
endAngle?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Pixels between the outer circle and the plot edge. By default the circle is as large as
|
|
18
|
+
* the plot allows with every angle label inside it.
|
|
19
|
+
*/
|
|
16
20
|
inset?: number;
|
|
21
|
+
/** Share of the radius left after `inset` that the outer circle uses. Defaults to `1`. */
|
|
17
22
|
radiusRatio?: number;
|
|
18
23
|
};
|
|
19
24
|
type ChartPolarPathMark<TRow> = ChartPolarBase<TRow> & ChartFillStyle<TRow> & ChartStrokeStyle<TRow> & {
|