entasis 0.9.3 → 0.10.1

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.
Files changed (82) hide show
  1. package/dist/components/ButtonGroup/ButtonGroup.svelte +10 -4
  2. package/dist/components/ButtonGroup/buttonGroup.mcp.d.ts +1 -1
  3. package/dist/components/ButtonGroup/buttonGroup.mcp.js +23 -3
  4. package/dist/components/ButtonGroup/buttonGroup.props.d.ts +19 -5
  5. package/dist/components/Chart/Chart.svelte +1 -1
  6. package/dist/components/Chart/chart.cartesian.js +3 -2
  7. package/dist/components/Chart/chart.fit.d.ts +53 -0
  8. package/dist/components/Chart/chart.fit.js +81 -0
  9. package/dist/components/Chart/chart.mcp.d.ts +1 -1
  10. package/dist/components/Chart/chart.mcp.js +2 -1
  11. package/dist/components/Chart/chart.polar.js +137 -18
  12. package/dist/components/Chart/chart.polar.props.d.ts +5 -0
  13. package/dist/components/Chart/chart.proportion.js +12 -6
  14. package/dist/components/Chart/chart.relation.network.js +32 -4
  15. package/dist/components/Chart/chart.relation.sankey.js +53 -6
  16. package/dist/components/Chart/chart.relation.tree.js +84 -24
  17. package/dist/components/Chart/chart.series.props.d.ts +6 -0
  18. package/dist/components/Chart/chart.state.svelte.d.ts +1 -0
  19. package/dist/components/Chart/chart.state.svelte.js +9 -6
  20. package/dist/components/Chart/chart.viewport.svelte.d.ts +1 -0
  21. package/dist/components/Chart/chart.viewport.svelte.js +25 -7
  22. package/dist/components/DataTable/DataTable.svelte +25 -8
  23. package/dist/components/DataTable/DataTableRow.svelte +33 -10
  24. package/dist/components/DataTable/dataTable.mcp.d.ts +1 -1
  25. package/dist/components/DataTable/dataTable.mcp.js +12 -1
  26. package/dist/components/DataTable/dataTable.model.svelte.js +4 -3
  27. package/dist/components/DataTable/dataTable.props.d.ts +17 -0
  28. package/dist/components/DataTable/dataTable.theme.d.ts +12 -0
  29. package/dist/components/DataTable/dataTable.theme.js +3 -2
  30. package/dist/components/DataTable/index.d.ts +1 -1
  31. package/dist/components/Dialog/Dialog.svelte +12 -7
  32. package/dist/components/Dialog/dialog.state.svelte.d.ts +2 -1
  33. package/dist/components/Dialog/dialog.state.svelte.js +7 -7
  34. package/dist/components/Dialog/dialog.theme.js +1 -1
  35. package/dist/components/FloatingWindow/floatingWindow.theme.js +1 -1
  36. package/dist/components/Form/Select/Select.svelte +3 -1
  37. package/dist/components/Form/Select/select.align.d.ts +3 -2
  38. package/dist/components/Form/Select/select.align.js +11 -4
  39. package/dist/components/PageShell/pageShell.state.svelte.d.ts +1 -1
  40. package/dist/components/PageShell/pageShell.state.svelte.js +6 -13
  41. package/dist/components/Popover/Popover.svelte +5 -5
  42. package/dist/components/Popover/index.d.ts +1 -1
  43. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  44. package/dist/components/Popover/popover.mcp.js +33 -6
  45. package/dist/components/Popover/popover.state.svelte.d.ts +9 -1
  46. package/dist/components/Popover/popover.state.svelte.js +61 -8
  47. package/dist/components/Popover/popover.theme.js +1 -1
  48. package/dist/components/Sidebar/SidebarDesktopShell.svelte +26 -6
  49. package/dist/components/Sidebar/sidebar.theme.js +1 -1
  50. package/dist/components/Theme/index.d.ts +1 -1
  51. package/dist/components/Theme/index.js +1 -1
  52. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  53. package/dist/components/Theme/theme.mcp.js +3 -0
  54. package/dist/components/Theme/theme.state.svelte.d.ts +4 -0
  55. package/dist/components/Theme/theme.state.svelte.js +4 -0
  56. package/dist/components/Tooltip/Tooltip.svelte +2 -4
  57. package/dist/components/Tooltip/tooltip.attachment.svelte.js +5 -0
  58. package/dist/components/Tooltip/tooltip.mcp.d.ts +1 -1
  59. package/dist/components/Tooltip/tooltip.mcp.js +4 -2
  60. package/dist/generated/componentAliases.d.ts +1 -0
  61. package/dist/generated/componentAliases.js +1 -0
  62. package/dist/generated/componentContract.d.ts +13 -3
  63. package/dist/generated/componentContract.js +15 -1
  64. package/dist/generated/componentMcpRegistry.d.ts +7 -6
  65. package/dist/generated/componentMcpRegistry.js +2 -0
  66. package/dist/i18n/ar.js +1 -1
  67. package/dist/i18n/de.js +1 -1
  68. package/dist/i18n/en.js +1 -1
  69. package/dist/i18n/es.js +1 -1
  70. package/dist/i18n/fr.js +1 -1
  71. package/dist/i18n/pt.js +1 -1
  72. package/dist/i18n/zh.js +1 -1
  73. package/dist/tailwind/colors.d.ts +10 -3
  74. package/dist/tailwind/colors.js +6 -0
  75. package/dist/tailwind/palette.d.ts +5 -0
  76. package/dist/tailwind/palette.js +4 -0
  77. package/dist/tailwind/palette.mcp.d.ts +1 -0
  78. package/dist/tailwind/palette.mcp.js +34 -0
  79. package/dist/utils/layers.svelte.js +9 -8
  80. package/dist/utils/registry.svelte.d.ts +21 -0
  81. package/dist/utils/registry.svelte.js +49 -0
  82. package/package.json +8 -1
@@ -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
- {#each items as button, index (index)}
22
- <Button {size} {color} {variant} {disabled} {...button} />
23
- {/each}
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> (required) - Array of button configurations\n - Each button can have all standard Button component props\n\n### Shared Button Props\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\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### 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";
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
- - **items**: Array<ButtonProps> (required) - Array of button configurations
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
- export type ButtonGroupProps = WithAttachments<{
6
+ type ButtonGroupContent = {
6
7
  /** Items rendered in order inside the group. */
7
8
  items: ButtonProps[];
8
- /** Size applied to every button in the group. */
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 button in the group. */
23
+ /** Color applied to every item in the group. */
11
24
  color?: Colors;
12
- /** Visual variant applied to every button in the group. */
25
+ /** Visual variant applied to every item in the group. */
13
26
  variant?: ButtonVariant;
14
- /** When true, disables all buttons in the group. */
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', gradients, {
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', gradients, {
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 inset = mark.inset ?? (mark.variant === 'radar' ? 24 : 16);
30
- const radiusRatio = mark.radiusRatio ?? (mark.variant === 'radar' ? 1 : 0.92);
31
- const compiled = polar({
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
- function centerPolarPolygon(mark, inset, radiusRatio) {
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 rendered = initialized.render(renderContext);
147
- const angleValues = new Set((rendered.points ?? []).map((point) => chartValueIdentity(point.xValue)));
148
- if (angleValues.size < 3)
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 unitPoints = Array.from({ length: angleValues.size }, (_value, index) => {
151
- const angle = (index / angleValues.size) * Math.PI * 2;
152
- return [Math.sin(angle), -Math.cos(angle)];
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> & {