svelteplot 0.6.0-pr-266.0 → 0.7.0-pr-269.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/Mark.svelte CHANGED
@@ -152,6 +152,12 @@
152
152
  if (options?.[channel] !== undefined && out[channel] === undefined) {
153
153
  // resolve value
154
154
  out[channel] = resolveChannel(channel, row, options);
155
+ if (options[channel] === INDEX) {
156
+ const scale = plot.scales[CHANNEL_SCALE[channel]];
157
+ if (scale.type === 'band' || scale.type === 'point') {
158
+ out[channel] = scale.domain[out[channel] % scale.domain.length];
159
+ }
160
+ }
155
161
  }
156
162
  }
157
163
  return [out];
@@ -212,6 +218,7 @@
212
218
  resolvedData.flatMap((row) => {
213
219
  const out: ScaledDataRecord<Datum> = {
214
220
  datum: row.datum,
221
+ resolved: row,
215
222
  index: row[INDEX],
216
223
  valid: true
217
224
  };
package/dist/Plot.svelte CHANGED
@@ -12,7 +12,7 @@
12
12
  <script lang="ts">
13
13
  import Plot from './core/Plot.svelte';
14
14
 
15
- import type { PlotOptions } from './types/index.js';
15
+ import type { PlotOptions, RawValue, ScaleOptions } from './types/index.js';
16
16
 
17
17
  // implicit marks
18
18
  import AxisX from './marks/AxisX.svelte';
@@ -62,12 +62,20 @@
62
62
  const scales = $derived(
63
63
  Object.fromEntries(
64
64
  ['x', 'y', 'r', 'color', 'opacity', 'symbol', 'length', 'fx', 'fy'].map((scale) => {
65
- const scaleOpts = restOptions[scale] || {};
65
+ const scaleOpts = maybeScaleOptions(restOptions[scale]);
66
66
  const scaleFn = scaleOpts.scale || (scale === 'color' ? autoScaleColor : autoScale);
67
67
  return [scale, { ...scaleOpts, scale: scaleFn }];
68
68
  })
69
69
  )
70
70
  );
71
+
72
+ function maybeScaleOptions(
73
+ scaleOptions: undefined | false | RawValue[] | object
74
+ ): Partial<ScaleOptions> | undefined {
75
+ if (scaleOptions === false) return { axis: false };
76
+ if (Array.isArray(scaleOptions)) return { domain: scaleOptions };
77
+ return scaleOptions || {};
78
+ }
71
79
  </script>
72
80
 
73
81
  {#snippet header()}
@@ -35,7 +35,7 @@
35
35
  scaleFn={facetXScale}
36
36
  scaleType="band"
37
37
  ticks={fxValues}
38
- tickFormat={(d) => d}
38
+ tickFormat={plot.options.fx.tickFormat || ((d) => d)}
39
39
  tickFontSize={11}
40
40
  tickSize={0}
41
41
  tickPadding={5}
@@ -53,7 +53,7 @@
53
53
  scaleFn={facetYScale}
54
54
  scaleType="band"
55
55
  ticks={fyValues}
56
- tickFormat={(d) => d}
56
+ tickFormat={plot.options.fy.tickFormat || ((d) => d)}
57
57
  tickFontSize={11}
58
58
  tickSize={0}
59
59
  tickPadding={5}
@@ -114,7 +114,7 @@
114
114
  class: className = '',
115
115
  css = DEFAULTS.css,
116
116
  width: fixedWidth,
117
- ...initialOpts
117
+ ...initialOptions
118
118
  }: Partial<PlotOptions> = $props();
119
119
 
120
120
  let width = $state(DEFAULTS.initialWidth);
@@ -164,7 +164,7 @@
164
164
  );
165
165
 
166
166
  const explicitDomains = $derived(
167
- new Set(SCALES.filter((scale) => !!initialOpts[scale]?.domain))
167
+ new Set(SCALES.filter((scale) => !!initialOptions[scale]?.domain))
168
168
  );
169
169
 
170
170
  // one-dimensional plots have different automatic margins and heights
@@ -173,12 +173,12 @@
173
173
  // construct the plot options from the user-defined options (top-level props) as well
174
174
  // as extending them from smart context-aware defaults
175
175
  const plotOptions = $derived(
176
- extendPlotOptions(initialOpts, {
176
+ extendPlotOptions(initialOptions, {
177
177
  explicitScales,
178
178
  explicitDomains,
179
- hasProjection: !!initialOpts.projection,
180
- margin: initialOpts.margin,
181
- inset: initialOpts.inset
179
+ hasProjection: !!initialOptions.projection,
180
+ margin: initialOptions.margin,
181
+ inset: initialOptions.inset
182
182
  })
183
183
  );
184
184
 
@@ -369,7 +369,7 @@
369
369
  {},
370
370
  { sortOrdinalDomains: DEFAULTS.sortOrdinalDomains },
371
371
  smartDefaultPlotOptions(opts),
372
- initialOpts
372
+ initialOptions
373
373
  );
374
374
  }
375
375
 
@@ -614,10 +614,6 @@
614
614
  border: 0 !important;
615
615
  }
616
616
 
617
- .plot-header :global(h3) {
618
- font-weight: 500;
619
- }
620
-
621
617
  .plot-footer {
622
618
  margin-bottom: 2rem;
623
619
  }
@@ -5,3 +5,5 @@ export default function (datum: DataRow, props: Partial<Channels>): any;
5
5
  export declare function maybeToPixel(cssKey: string, value: string | number): string | number;
6
6
  export declare function maybeFromPixel(value: string | number): string | number;
7
7
  export declare function maybeFromRem(value: string | number, rootFontSize?: number): string | number;
8
+ export declare function getClipId(): string;
9
+ export declare function getPatternId(): string;
@@ -54,3 +54,11 @@ export function maybeFromRem(value, rootFontSize = 16) {
54
54
  ? +value.slice(0, -3) * rootFontSize
55
55
  : value;
56
56
  }
57
+ let nextClipId = 0;
58
+ let nextPatternId = 0;
59
+ export function getClipId() {
60
+ return `svp-clip-${++nextClipId}`;
61
+ }
62
+ export function getPatternId() {
63
+ return `svp-pattern-${++nextPatternId}`;
64
+ }
@@ -12,10 +12,11 @@ export default function removeIdenticalLines(input) {
12
12
  text: []
13
13
  });
14
14
  }
15
- for (let l = 0; l < input[0].text.length; l++) {
15
+ const maxLines = Math.max(...input.map((t) => t.text.length));
16
+ for (let l = 0; l < maxLines; l++) {
16
17
  const isIdentical = input.length > 1 && input.every((tick) => input[0].text[l] === tick.text[l]);
17
18
  for (let c = 0; c < input.length; c++) {
18
- if (!isIdentical && input[c].text[l])
19
+ if (!isIdentical && input[c].text[l] != null)
19
20
  uniqueTicks[c].text.push(input[c].text[l]);
20
21
  }
21
22
  }
@@ -218,8 +218,8 @@ function domainFromInterval(domain, interval, name) {
218
218
  return name === 'y' ? out.toReversed() : out;
219
219
  }
220
220
  const markTypesWithBandDefault = {
221
- x: new Set(['barY', 'cell', 'tickY']),
222
- y: new Set(['barX', 'cell', 'tickX'])
221
+ x: new Set(['barY', 'cell', 'tickY', 'waffleY']),
222
+ y: new Set(['barX', 'cell', 'tickX', 'waffleX'])
223
223
  };
224
224
  /**
225
225
  * Infer a scale type based on the scale name, the data values mapped to it and
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Greedy word-wrapping that approximates visual width by character widths.
3
+ *
4
+ * - Splits input into words, additionally breaking on `-` to avoid very long segments.
5
+ * - Uses a rough character width table to approximate line widths.
6
+ * - Wraps once the maximum visual width is exceeded, but only after a minimum.
7
+ */
8
+ export default function wordwrap(line: string, { maxCharactersPerLine, maxLineWidth }: {
9
+ maxCharactersPerLine?: number;
10
+ maxLineWidth?: number;
11
+ }, { minCharactersPerLine, minLineWidth }: {
12
+ minCharactersPerLine?: number;
13
+ minLineWidth?: number;
14
+ }, fontSize?: number, monospace?: boolean): string[];
@@ -0,0 +1,129 @@
1
+ import { sum } from 'd3-array';
2
+ // Per-character width table for a typical proportional font.
3
+ // This is not perfect for all fonts, but is better than treating
4
+ // all characters as equal width. Set `monospace = true` to bypass.
5
+ const CHAR_W = {
6
+ A: 7,
7
+ a: 7,
8
+ B: 8,
9
+ b: 7,
10
+ C: 8,
11
+ c: 6,
12
+ D: 9,
13
+ d: 7,
14
+ E: 7,
15
+ e: 7,
16
+ F: 7,
17
+ f: 4,
18
+ G: 9,
19
+ g: 7,
20
+ H: 9,
21
+ h: 7,
22
+ I: 3,
23
+ i: 3,
24
+ J: 5,
25
+ j: 3,
26
+ K: 8,
27
+ k: 6,
28
+ L: 7,
29
+ l: 3,
30
+ M: 11,
31
+ m: 11,
32
+ N: 9,
33
+ n: 7,
34
+ O: 9,
35
+ o: 7,
36
+ P: 8,
37
+ p: 7,
38
+ Q: 9,
39
+ q: 7,
40
+ R: 8,
41
+ r: 4,
42
+ S: 8,
43
+ s: 6,
44
+ T: 7,
45
+ t: 4,
46
+ U: 9,
47
+ u: 7,
48
+ V: 7,
49
+ v: 6,
50
+ W: 11,
51
+ w: 9,
52
+ X: 7,
53
+ x: 6,
54
+ Y: 7,
55
+ y: 6,
56
+ Z: 7,
57
+ z: 5,
58
+ '.': 2,
59
+ ',': 2,
60
+ ':': 2,
61
+ ';': 2
62
+ };
63
+ /**
64
+ * Greedy word-wrapping that approximates visual width by character widths.
65
+ *
66
+ * - Splits input into words, additionally breaking on `-` to avoid very long segments.
67
+ * - Uses a rough character width table to approximate line widths.
68
+ * - Wraps once the maximum visual width is exceeded, but only after a minimum.
69
+ */
70
+ export default function wordwrap(line, { maxCharactersPerLine, maxLineWidth }, { minCharactersPerLine, minLineWidth }, fontSize = 12, monospace = false) {
71
+ // Tokenized words (with hyphen-splitting applied) including trailing spaces/hyphens.
72
+ const tokens = [];
73
+ // First split by spaces, then further split by hyphens so we can
74
+ // wrap inside hyphenated words if necessary.
75
+ const spaceSeparated = line.split(' ');
76
+ spaceSeparated.forEach((word, wordIndex) => {
77
+ const hyphenParts = word.split('-');
78
+ const trailingWhitespace = wordIndex < spaceSeparated.length - 1 ? ' ' : '';
79
+ if (hyphenParts.length > 1) {
80
+ hyphenParts.forEach((part, partIndex) => {
81
+ const suffix = partIndex < hyphenParts.length - 1 ? '-' : trailingWhitespace;
82
+ tokens.push(part + suffix);
83
+ });
84
+ }
85
+ else {
86
+ tokens.push(word + trailingWhitespace);
87
+ }
88
+ });
89
+ const maxChars = maxCharactersPerLine || 40;
90
+ if (!maxLineWidth) {
91
+ // Fallback for max characters per line if not provided / falsy.
92
+ // Convert character counts into approximate visual widths.
93
+ maxLineWidth = maxChars * CHAR_W.a;
94
+ }
95
+ if (!minLineWidth) {
96
+ // Estimate a good minimum line length:
97
+ // - start from either a provided value or
98
+ // - clamp a scaled median word length between 3 and half of maxChars.
99
+ const sortedWordLengths = tokens.map((t) => t.length).sort((a, b) => a - b);
100
+ const medianIndex = Math.round(tokens.length / 2);
101
+ const medianWordLength = sortedWordLengths[medianIndex] ?? maxChars;
102
+ const minChars = minCharactersPerLine || Math.max(3, Math.min(maxChars * 0.5, 0.75 * medianWordLength));
103
+ minLineWidth = minChars * CHAR_W.a;
104
+ }
105
+ const lines = [];
106
+ const currentWords = [];
107
+ let currentWidth = 0;
108
+ // Helper to look up a character width, falling back to "a" if unknown
109
+ // or when monospace mode is enabled.
110
+ const charWidth = (char) => (fontSize / 12) * (!monospace ? CHAR_W[char] : CHAR_W.a);
111
+ // Greedy line construction: append tokens until the next one would exceed
112
+ // max visual width, but only break if the line has passed the minimum width.
113
+ tokens.forEach((token) => {
114
+ const tokenWidth = sum(token.split('').map(charWidth));
115
+ if (currentWidth + tokenWidth > maxLineWidth && currentWidth > minLineWidth) {
116
+ lines.push(currentWords.join(''));
117
+ currentWords.length = 0;
118
+ currentWidth = 0;
119
+ }
120
+ currentWidth += tokenWidth;
121
+ currentWords.push(token);
122
+ });
123
+ // Flush trailing tokens into the last line.
124
+ if (currentWords.length > 0) {
125
+ lines.push(currentWords.join(''));
126
+ }
127
+ // Filter out any empty lines that may have been created.
128
+ return lines.filter((d) => d !== '');
129
+ }
@@ -51,6 +51,7 @@
51
51
  tickSpacing?: number;
52
52
  /** text anchor for axis labels */
53
53
  textAnchor?: ConstantAccessor<CSS.Property.TextAnchor | 'auto', Datum>;
54
+ removeDuplicateTicks: boolean;
54
55
  }
55
56
 
56
57
  let markProps: AxisXMarkProps = $props();
@@ -223,7 +224,7 @@
223
224
  {anchor}
224
225
  {className}
225
226
  {labelAnchor}
226
- {options}
227
+ options={{ ...options, ...plot.options.x }}
227
228
  {plot}
228
229
  {text}
229
230
  {tickClass}
@@ -88,6 +88,7 @@ declare class __sveltets_Render<Datum extends RawValue> {
88
88
  tickSpacing?: number;
89
89
  /** text anchor for axis labels */
90
90
  textAnchor?: ConstantAccessor<"auto" | CSS.Property.TextAnchor, Datum>;
91
+ removeDuplicateTicks: boolean;
91
92
  };
92
93
  events(): {};
93
94
  slots(): {};
@@ -139,7 +139,7 @@
139
139
  {...args}>
140
140
  {#snippet children({ mark, usedScales, scaledData })}
141
141
  {#if scaledData.length > 0}
142
- {@const groupedLineData = groupIndex(scaledData, groupByKey)};
142
+ {@const groupedLineData = groupIndex(scaledData, groupByKey)}
143
143
  {#if canvas}
144
144
  <LineCanvas {groupedLineData} {mark} {usedScales} {linePath} {groupByKey} />
145
145
  {:else}
@@ -0,0 +1,115 @@
1
+ <!--
2
+ @component
3
+ The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values.
4
+ -->
5
+ <script lang="ts" generics="Datum extends DataRecord">
6
+ import type {
7
+ DataRecord,
8
+ BaseMarkProps,
9
+ ChannelAccessor,
10
+ LinkableMarkProps,
11
+ BorderRadius
12
+ } from '../types';
13
+ import { wafflePolygon, type WaffleOptions } from './helpers/waffle';
14
+ import { getPlotDefaults } from '../hooks/plotDefaults';
15
+ import { intervalX, recordizeX, sort, stackX } from '../transforms';
16
+ import type { StackOptions } from '../transforms/stack';
17
+ import Mark from '../Mark.svelte';
18
+ import { getContext } from 'svelte';
19
+ import { resolveProp, resolveStyles } from '../helpers/resolve';
20
+ import { roundedRect } from '../helpers/roundedRect';
21
+
22
+ interface WaffleXMarkProps
23
+ extends BaseMarkProps<Datum>,
24
+ LinkableMarkProps<Datum>,
25
+ WaffleOptions<Datum> {
26
+ data?: Datum[];
27
+ /**
28
+ * bound to a quantitative scale
29
+ */
30
+ x?: ChannelAccessor<Datum>;
31
+ /**
32
+ * bound to a quantitative scale
33
+ */
34
+ x1?: ChannelAccessor<Datum>;
35
+ /**
36
+ * bound to a quantitative scale
37
+ */
38
+ x2?: ChannelAccessor<Datum>;
39
+ /**
40
+ * bound to a band scale
41
+ */
42
+ y?: ChannelAccessor<Datum>;
43
+ stack?: StackOptions;
44
+ }
45
+
46
+ const DEFAULTS = {
47
+ fill: 'currentColor',
48
+ ...getPlotDefaults().waffle,
49
+ ...getPlotDefaults().waffleX
50
+ };
51
+
52
+ let markProps: WaffleXMarkProps = $props();
53
+
54
+ const {
55
+ data = [{} as Datum],
56
+ class: className = null,
57
+ stack,
58
+ symbol = null,
59
+ unit,
60
+ ...options
61
+ }: WaffleXMarkProps = $derived({ ...DEFAULTS, ...markProps });
62
+
63
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
64
+ const plot = $derived(getPlotState());
65
+
66
+ const args = $derived(
67
+ stackX(
68
+ intervalX(
69
+ // by default, sort by y channel (the ordinal labels)
70
+ sort(recordizeX({ data, ...options })),
71
+ { plot }
72
+ ),
73
+ stack
74
+ )
75
+ );
76
+ </script>
77
+
78
+ <Mark
79
+ type="waffleX"
80
+ requiredScales={{ y: ['band'] }}
81
+ channels={['x1', 'x2', 'y', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
82
+ {...args}>
83
+ {#snippet children({ mark, usedScales, scaledData })}
84
+ {@const wafflePoly = wafflePolygon('x', args, plot.scales)}
85
+ {#each scaledData as d, i (i)}
86
+ {@const borderRadius = resolveProp(args.borderRadius, d?.datum, 0) as BorderRadius}
87
+ {@const hasBorderRadius =
88
+ (typeof borderRadius === 'number' && borderRadius > 0) ||
89
+ (typeof borderRadius === 'object' &&
90
+ Math.max(
91
+ borderRadius.topRight ?? 0,
92
+ borderRadius.bottomRight ?? 0,
93
+ borderRadius.topLeft ?? 0,
94
+ borderRadius.bottomLeft ?? 0
95
+ ) > 0)}
96
+ {@const [style, styleClass] = resolveStyles(plot, d, options, 'fill', usedScales)}
97
+ {@const { pattern, rect, path } = wafflePoly(d)}
98
+ <g class={['waffle-x', className]}>
99
+ <pattern {...pattern}>
100
+ {#if symbol}
101
+ {@render symbol(rect)}
102
+ {:else if hasBorderRadius}
103
+ <path
104
+ d={roundedRect(rect.x, rect.y, rect.width, rect.height, borderRadius)}
105
+ {style}
106
+ class={styleClass} />
107
+ {:else}
108
+ <rect {style} class={styleClass} {...rect} />
109
+ {/if}
110
+ </pattern>
111
+ <path {...path} />
112
+ </g>
113
+ {/each}
114
+ {/snippet}
115
+ </Mark>
@@ -0,0 +1,19 @@
1
+ import type { DataRecord } from '../types';
2
+ declare class __sveltets_Render<Datum extends DataRecord> {
3
+ props(): any;
4
+ events(): {};
5
+ slots(): {};
6
+ bindings(): "";
7
+ exports(): {};
8
+ }
9
+ interface $$IsomorphicComponent {
10
+ new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
11
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
12
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
13
+ <Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
14
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
15
+ }
16
+ /** The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values. */
17
+ declare const WaffleX: $$IsomorphicComponent;
18
+ type WaffleX<Datum extends DataRecord> = InstanceType<typeof WaffleX<Datum>>;
19
+ export default WaffleX;
@@ -0,0 +1,119 @@
1
+ <!--
2
+ @component
3
+ The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values.
4
+ -->
5
+ <script lang="ts" generics="Datum extends DataRecord">
6
+ import type {
7
+ DataRecord,
8
+ ChannelAccessor,
9
+ BaseMarkProps,
10
+ LinkableMarkProps,
11
+ PlotContext,
12
+ BorderRadius
13
+ } from '../types';
14
+ import { wafflePolygon, type WaffleOptions } from './helpers/waffle';
15
+ import { getPlotDefaults } from '../hooks/plotDefaults';
16
+ import { getContext } from 'svelte';
17
+ import { intervalY, recordizeY, sort, stackY } from '../transforms';
18
+ import Mark from '../Mark.svelte';
19
+ import { resolveProp, resolveStyles } from '../helpers/resolve';
20
+ import { roundedRect } from '../helpers/roundedRect';
21
+ import GroupMultiple from './helpers/GroupMultiple.svelte';
22
+
23
+ interface WaffleYMarkProps
24
+ extends BaseMarkProps<Datum>,
25
+ LinkableMarkProps<Datum>,
26
+ WaffleOptions<Datum> {
27
+ data?: Datum[];
28
+ /**
29
+ * bound to a babd scale
30
+ */
31
+ x?: ChannelAccessor<Datum>;
32
+ /**
33
+ * bound to a quantitative scale
34
+ */
35
+ y?: ChannelAccessor<Datum>;
36
+ /**
37
+ * bound to a quantitative scale
38
+ */
39
+ y1?: ChannelAccessor<Datum>;
40
+ /**
41
+ * bound to a quantitative scale
42
+ */
43
+ y2?: ChannelAccessor<Datum>;
44
+ }
45
+
46
+ const DEFAULTS = {
47
+ ...getPlotDefaults().waffle,
48
+ ...getPlotDefaults().waffleY
49
+ };
50
+
51
+ let markProps: WaffleYMarkProps = $props();
52
+
53
+ const {
54
+ data = [{} as Datum],
55
+ class: className = null,
56
+ stack,
57
+ symbol = null,
58
+ ...options
59
+ }: WaffleYMarkProps = $derived({ ...DEFAULTS, ...markProps });
60
+
61
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
62
+ const plot = $derived(getPlotState());
63
+
64
+ const args = $derived(
65
+ stackY(
66
+ intervalY(
67
+ // by default, sort by y channel (the ordinal labels)
68
+ sort(recordizeY({ data, ...options })),
69
+ { plot }
70
+ ),
71
+ stack
72
+ )
73
+ );
74
+ </script>
75
+
76
+ <Mark
77
+ type="waffleY"
78
+ requiredScales={{ x: ['band'] }}
79
+ channels={['y1', 'y2', 'x', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
80
+ {...args}>
81
+ {#snippet children({ mark, usedScales, scaledData })}
82
+ {@const wafflePoly = wafflePolygon('y', args, plot.scales)}
83
+ {#each scaledData as d, i (i)}
84
+ {@const [style, styleClass] = resolveStyles(
85
+ plot,
86
+ d,
87
+ args,
88
+ args.stroke && !args.fill ? 'stroke' : 'fill',
89
+ usedScales
90
+ )}
91
+ {@const borderRadius = resolveProp(args.borderRadius, d?.datum, 0) as BorderRadius}
92
+ {@const hasBorderRadius =
93
+ (typeof borderRadius === 'number' && borderRadius > 0) ||
94
+ (typeof borderRadius === 'object' &&
95
+ Math.max(
96
+ borderRadius.topRight ?? 0,
97
+ borderRadius.bottomRight ?? 0,
98
+ borderRadius.topLeft ?? 0,
99
+ borderRadius.bottomLeft ?? 0
100
+ ) > 0)}
101
+ {@const { pattern, rect, path } = wafflePoly(d)}
102
+ <g class={['waffle-y', className]}>
103
+ <pattern {...pattern}>
104
+ {#if symbol}
105
+ {@render symbol({ ...rect, style, styleClass, datum: d.datum })}
106
+ {:else if hasBorderRadius}
107
+ <path
108
+ d={roundedRect(rect.x, rect.y, rect.width, rect.height, borderRadius)}
109
+ {style}
110
+ class={styleClass} />
111
+ {:else}
112
+ <rect {style} class={styleClass} {...rect} />
113
+ {/if}
114
+ </pattern>
115
+ <path {...path} />
116
+ </g>
117
+ {/each}
118
+ {/snippet}
119
+ </Mark>
@@ -0,0 +1,19 @@
1
+ import type { DataRecord } from '../types';
2
+ declare class __sveltets_Render<Datum extends DataRecord> {
3
+ props(): any;
4
+ events(): {};
5
+ slots(): {};
6
+ bindings(): "";
7
+ exports(): {};
8
+ }
9
+ interface $$IsomorphicComponent {
10
+ new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
11
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
12
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
13
+ <Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
14
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
15
+ }
16
+ /** The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values. */
17
+ declare const WaffleY: $$IsomorphicComponent;
18
+ type WaffleY<Datum extends DataRecord> = InstanceType<typeof WaffleY<Datum>>;
19
+ export default WaffleY;
@@ -17,6 +17,7 @@
17
17
  import { randomId, testFilter } from '../../helpers/index.js';
18
18
  import { INDEX } from '../../constants';
19
19
  import { RAW_VALUE } from '../../transforms/recordize';
20
+ import wordwrap from '../../helpers/wordwrap';
20
21
 
21
22
  type BaseAxisXProps = {
22
23
  scaleFn: (d: RawValue) => number;
@@ -35,7 +36,9 @@
35
36
  dx: ConstantAccessor<number>;
36
37
  dy: ConstantAccessor<number>;
37
38
  filter: ChannelAccessor;
39
+ wordwrap: boolean;
38
40
  textAnchor: ConstantAccessor<'start' | 'middle' | 'end'> | 'auto';
41
+ removeDuplicateTicks: boolean;
39
42
  };
40
43
  text: boolean;
41
44
  plot: PlotState;
@@ -59,8 +62,21 @@
59
62
  text = true
60
63
  }: BaseAxisXProps = $props();
61
64
 
65
+ const isBandScale = $derived(scaleType === 'band');
66
+ const bandWidth = $derived(isBandScale ? scaleFn.bandwidth() : 0);
67
+
62
68
  function splitTick(tick: string | string[]) {
63
- return Array.isArray(tick) ? tick : [tick];
69
+ return Array.isArray(tick)
70
+ ? tick
71
+ : typeof tick === 'string' && isBandScale && options.wordwrap !== false
72
+ ? wordwrap(
73
+ tick,
74
+ { maxLineWidth: bandWidth * 0.9 },
75
+ { minCharactersPerLine: 4 },
76
+ +resolveProp(tickFontSize, {}, 11),
77
+ false
78
+ )
79
+ : [tick];
64
80
  }
65
81
 
66
82
  let tickRotate = $derived(plot.options.x.tickRotate || 0);
@@ -72,7 +88,7 @@
72
88
  // generate id used for registering margins
73
89
  const id = randomId();
74
90
 
75
- const { autoMarginTop, autoMarginBottom } =
91
+ const { autoMarginTop, autoMarginBottom, autoMarginLeft, autoMarginRight } =
76
92
  getContext<AutoMarginStores>('svelteplot/autoMargins');
77
93
 
78
94
  let tickTextElements = $state([] as SVGTextElement[]);
@@ -109,6 +125,14 @@
109
125
  return tickObjects as ScaledDataRecord[];
110
126
  });
111
127
 
128
+ $effect(() => {
129
+ // just add some minimal horizontal margins for axis ticks
130
+ untrack(() => $autoMarginLeft);
131
+ untrack(() => $autoMarginRight);
132
+ $autoMarginLeft.set(id, 5);
133
+ $autoMarginRight.set(id, 10);
134
+ });
135
+
112
136
  $effect(() => {
113
137
  untrack(() => [$autoMarginTop, $autoMarginBottom]);
114
138
  if (!text) return;
@@ -147,6 +171,8 @@
147
171
  return () => {
148
172
  if ($autoMarginBottom.has(id)) $autoMarginBottom.delete(id);
149
173
  if ($autoMarginTop.has(id)) $autoMarginTop.delete(id);
174
+ if ($autoMarginLeft.has(id)) $autoMarginLeft.delete(id);
175
+ if ($autoMarginRight.has(id)) $autoMarginRight.delete(id);
150
176
  };
151
177
  });
152
178
  </script>
@@ -222,7 +248,9 @@
222
248
  {:else}
223
249
  {#each textLines as line, i (i)}
224
250
  <tspan x="0" dy={i ? 12 : 0}
225
- >{!prevTextLines || prevTextLines[i] !== line
251
+ >{!prevTextLines ||
252
+ prevTextLines[i] !== line ||
253
+ options.removeDuplicateTicks === false
226
254
  ? line
227
255
  : ''}</tspan>
228
256
  {/each}
@@ -16,7 +16,9 @@ type BaseAxisXProps = {
16
16
  dx: ConstantAccessor<number>;
17
17
  dy: ConstantAccessor<number>;
18
18
  filter: ChannelAccessor;
19
+ wordwrap: boolean;
19
20
  textAnchor: ConstantAccessor<'start' | 'middle' | 'end'> | 'auto';
21
+ removeDuplicateTicks: boolean;
20
22
  };
21
23
  text: boolean;
22
24
  plot: PlotState;
@@ -0,0 +1,58 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { StackOptions } from '../../transforms/stack';
3
+ import type { BorderRadius, ConstantAccessor, PlotScales, ScaledDataRecord } from '../../types';
4
+ type Point = [number, number];
5
+ export type WaffleOptions<T> = {
6
+ /**
7
+ * the quantity represented by each square in the waffle chart, defaults to 1
8
+ */
9
+ unit?: number;
10
+ /**
11
+ * the number of cells per row (or column); defaults to undefined
12
+ */
13
+ multiple?: number;
14
+ /**
15
+ * the separation between adjacent cells, in pixels; defaults to 1
16
+ */
17
+ gap?: number;
18
+ /**
19
+ * whether to round values to avoid partial cells; defaults to false
20
+ */
21
+ round?: boolean;
22
+ stack?: StackOptions;
23
+ borderRadius?: ConstantAccessor<BorderRadius, T>;
24
+ symbol?: Snippet<[
25
+ {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ style: string | null;
31
+ styleClass: string | null;
32
+ datum: T;
33
+ }
34
+ ]>;
35
+ };
36
+ type WaffleProps = {
37
+ pattern: {
38
+ id: string;
39
+ patternUnits: 'userSpaceOnUse';
40
+ width: number;
41
+ height: number;
42
+ };
43
+ rect: {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ height: number;
48
+ };
49
+ path: {
50
+ fill: string;
51
+ transform: string;
52
+ d: string;
53
+ };
54
+ };
55
+ export declare function wafflePolygon(y: 'x' | 'y', options: WaffleOptions, scales: PlotScales): (d: ScaledDataRecord) => WaffleProps;
56
+ export declare function wafflePoints(i1: number, i2: number, columns: number): Point[];
57
+ export declare function maybeRound(round: boolean | ((x: number) => number) | undefined): (x: number) => number;
58
+ export {};
@@ -0,0 +1,194 @@
1
+ // A waffle is approximately a rectangular shape, but may have one or two corner
2
+ // cuts if the starting or ending value is not an even multiple of the number of
3
+ // columns (the width of the waffle in cells). We can represent any waffle by
4
+ // 8 points; below is a waffle of five columns representing the interval 2–11:
5
+ //
6
+ // 1-0
7
+ // |•7-------6
8
+ // |• • • • •|
9
+ // 2---3• • •|
10
+ // 4-----5
11
+ //
12
+ // Note that points 0 and 1 always have the same y-value, points 1 and 2 have
13
+ // the same x-value, and so on, so we don’t need to materialize the x- and y-
14
+ // values of all points. Also note that we can’t use the already-projected y-
15
+ // values because these assume that y-values are distributed linearly along y
16
+ // rather than wrapping around in columns.
17
+ //
18
+ // The corner points may be coincident. If the ending value is an even multiple
19
+ // of the number of columns, say representing the interval 2–10, then points 6,
20
+ // 7, and 0 are the same.
21
+ //
22
+ // 1-----0/7/6
23
+ // |• • • • •|
24
+ // 2---3• • •|
25
+ // 4-----5
26
+ //
27
+ // Likewise if the starting value is an even multiple, say representing the
28
+ // interval 0–10, points 2–4 are coincident.
29
+ //
30
+ // 1-----0/7/6
31
+ // |• • • • •|
32
+ // |• • • • •|
33
+ // 4/3/2-----5
34
+ //
35
+ // Waffles can also represent fractional intervals (e.g., 2.4–10.1). These
36
+ // require additional corner cuts, so the implementation below generates a few
37
+ // more points.
38
+ //
39
+ import { getPatternId } from '../../helpers/getBaseStyles';
40
+ export function wafflePolygon(y, options, scales) {
41
+ const x = y === 'y' ? 'x' : 'y';
42
+ const y1 = `${y}1`;
43
+ const y2 = `${y}2`;
44
+ const xScale = scales[x];
45
+ const yScale = scales[y];
46
+ const barwidth = xScale.fn.bandwidth();
47
+ const { unit = 1, gap = 1 } = options;
48
+ const round = maybeRound(options.round);
49
+ // The length of a unit along y in pixels.
50
+ const scale = Math.abs(yScale.fn(unit) - yScale.fn(0));
51
+ // The number of cells on each row (or column) of the waffle.
52
+ const multiple = options.multiple ?? Math.max(1, Math.floor(Math.sqrt(barwidth / scale)));
53
+ // The outer size of each square cell, in pixels, including the gap.
54
+ const cx = Math.min(barwidth / multiple, scale * multiple);
55
+ const cy = scale * multiple;
56
+ // The reference position.
57
+ const tx = (barwidth - multiple * cx) / 2;
58
+ const transform = y === 'y' ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx];
59
+ // const mx = typeof x0 === 'function' ? (i) => x0(i) - barwidth / 2 : () => x0;
60
+ const [ix, iy] = y === 'y' ? [0, 1] : [1, 0];
61
+ const y0 = yScale.fn(0);
62
+ const mx = -barwidth / 2;
63
+ return (d) => {
64
+ const y1val = d.resolved[y1];
65
+ const y2val = d.resolved[y2];
66
+ const P = wafflePoints(round(y1val / unit), round(y2val / unit), multiple).map(transform);
67
+ P.pop();
68
+ const id = getPatternId();
69
+ const pos = [d[x] + tx + mx, y0];
70
+ return {
71
+ pattern: {
72
+ id,
73
+ patternUnits: 'userSpaceOnUse',
74
+ width: cx,
75
+ height: cy
76
+ },
77
+ rect: {
78
+ x: gap / 2,
79
+ y: gap / 2,
80
+ width: cx - gap,
81
+ height: cy - gap
82
+ },
83
+ path: {
84
+ fill: `url(#${id})`,
85
+ transform: `translate(${pos[ix]},${pos[iy]})`,
86
+ d: `M${P.join('L')}Z`
87
+ }
88
+ };
89
+ // return `M${P.join('L')}Z`;
90
+ };
91
+ // const points = wafflePoints(i1, i2, columns);
92
+ // return dimension === 'x' ? points : points.map(([x, y]: Point): Point => [y, x]);
93
+ }
94
+ export function wafflePoints(i1, i2, columns) {
95
+ if (i2 < i1)
96
+ return wafflePoints(i2, i1, columns); // ensure i1 <= i2
97
+ if (i1 < 0)
98
+ return wafflePointsOffset(i1, i2, columns, Math.ceil(-Math.min(i1, i2) / columns)); // ensure i1 >= 0
99
+ const x1f = Math.floor(i1 % columns);
100
+ const x1c = Math.ceil(i1 % columns);
101
+ const x2f = Math.floor(i2 % columns);
102
+ const x2c = Math.ceil(i2 % columns);
103
+ const y1f = Math.floor(i1 / columns);
104
+ const y1c = Math.ceil(i1 / columns);
105
+ const y2f = Math.floor(i2 / columns);
106
+ const y2c = Math.ceil(i2 / columns);
107
+ const points = [];
108
+ if (y2c > y1c)
109
+ points.push([0, y1c]);
110
+ points.push([x1f, y1c], [x1f, y1f + (i1 % 1)], [x1c, y1f + (i1 % 1)]);
111
+ if (!(i1 % columns > columns - 1)) {
112
+ points.push([x1c, y1f]);
113
+ if (y2f > y1f)
114
+ points.push([columns, y1f]);
115
+ }
116
+ if (y2f > y1f)
117
+ points.push([columns, y2f]);
118
+ points.push([x2c, y2f], [x2c, y2f + (i2 % 1)], [x2f, y2f + (i2 % 1)]);
119
+ if (!(i2 % columns < 1)) {
120
+ points.push([x2f, y2c]);
121
+ if (y2c > y1c)
122
+ points.push([0, y2c]);
123
+ }
124
+ points.push(waffleCentroid(i1, i2, columns));
125
+ return points;
126
+ }
127
+ /**
128
+ * Compute waffle points when indices start in negative rows by applying a row offset.
129
+ * - Shifts both indices down by `k` rows (adding `k * columns`) so they are non-negative,
130
+ * delegates to `wafflePoints`, then translates the resulting points back up by `k` on y.
131
+ * - `k` is the number of rows of vertical offset applied.
132
+ */
133
+ function wafflePointsOffset(i1, i2, columns, k) {
134
+ return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]);
135
+ }
136
+ /**
137
+ * Centroid of the waffle region representing the interval [i1, i2).
138
+ * Chooses a strategy based on how many rows the interval spans:
139
+ * - Single row: delegate to `waffleRowCentroid`.
140
+ * - Two rows: if the projected columns overlap, return the midpoint of the overlap;
141
+ * otherwise, return the centroid of the larger partial row.
142
+ * - >= 3 rows: return the center column and halfway between the middle rows.
143
+ */
144
+ function waffleCentroid(i1, i2, columns) {
145
+ const r = Math.floor(i2 / columns) - Math.floor(i1 / columns);
146
+ return r === 0
147
+ ? // Single row
148
+ waffleRowCentroid(i1, i2, columns)
149
+ : r === 1
150
+ ? // Two incomplete rows; use the midpoint of their overlap if any, otherwise the larger row
151
+ Math.floor(i2 % columns) > Math.ceil(i1 % columns)
152
+ ? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)]
153
+ : i2 % columns > columns - (i1 % columns)
154
+ ? waffleRowCentroid(i2 - (i2 % columns), i2, columns)
155
+ : waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns)
156
+ : // At least one full row; take the midpoint of all the rows that include the middle
157
+ [columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2];
158
+ }
159
+ /**
160
+ * Centroid of a waffle segment constrained to a single row.
161
+ * Cases:
162
+ * - c === 0: both endpoints fall into the same cell; center on x, average fractional y.
163
+ * - c === 1: two adjacent partial cells; use the overlap center if > 0.5 cell,
164
+ * otherwise the center of the larger partial cell.
165
+ * - c >= 2: at least one full cell between; x is the midpoint of full cells,
166
+ * y is the row center (0.5) if there’s a full cell spanned, otherwise average fractional y.
167
+ */
168
+ function waffleRowCentroid(i1, i2, columns) {
169
+ const c = Math.floor(i2) - Math.floor(i1);
170
+ return c === 0
171
+ ? // Single cell
172
+ [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)]
173
+ : c === 1
174
+ ? // Two incomplete cells; use the overlap if large enough, otherwise use the largest
175
+ (i2 % 1) - (i1 % 1) > 0.5
176
+ ? [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2]
177
+ : i2 % 1 > 1 - (i1 % 1)
178
+ ? [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2]
179
+ : [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2]
180
+ : // At least one full cell; take the midpoint
181
+ [
182
+ Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2,
183
+ Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1)
184
+ ];
185
+ }
186
+ export function maybeRound(round) {
187
+ if (round === undefined || round === false)
188
+ return Number;
189
+ if (round === true)
190
+ return Math.round;
191
+ if (typeof round !== 'function')
192
+ throw new Error(`invalid round: ${round}`);
193
+ return round;
194
+ }
@@ -33,7 +33,6 @@ export { default as LineX } from './LineX.svelte';
33
33
  export { default as LineY } from './LineY.svelte';
34
34
  export { default as Link } from './Link.svelte';
35
35
  export { default as Pointer } from './Pointer.svelte';
36
- export { default as Vector } from './Vector.svelte';
37
36
  export { default as Rect } from './Rect.svelte';
38
37
  export { default as RectX } from './RectX.svelte';
39
38
  export { default as RectY } from './RectY.svelte';
@@ -46,6 +45,9 @@ export { default as Spike } from './Spike.svelte';
46
45
  export { default as Text } from './Text.svelte';
47
46
  export { default as TickX } from './TickX.svelte';
48
47
  export { default as TickY } from './TickY.svelte';
48
+ export { default as Vector } from './Vector.svelte';
49
+ export { default as WaffleX } from './WaffleX.svelte';
50
+ export { default as WaffleY } from './WaffleY.svelte';
49
51
  export { default as ColorLegend } from './ColorLegend.svelte';
50
52
  export { default as HTMLTooltip } from './HTMLTooltip.svelte';
51
53
  export { default as SymbolLegend } from './SymbolLegend.svelte';
@@ -33,7 +33,6 @@ export { default as LineX } from './LineX.svelte';
33
33
  export { default as LineY } from './LineY.svelte';
34
34
  export { default as Link } from './Link.svelte';
35
35
  export { default as Pointer } from './Pointer.svelte';
36
- export { default as Vector } from './Vector.svelte';
37
36
  export { default as Rect } from './Rect.svelte';
38
37
  export { default as RectX } from './RectX.svelte';
39
38
  export { default as RectY } from './RectY.svelte';
@@ -46,6 +45,9 @@ export { default as Spike } from './Spike.svelte';
46
45
  export { default as Text } from './Text.svelte';
47
46
  export { default as TickX } from './TickX.svelte';
48
47
  export { default as TickY } from './TickY.svelte';
48
+ export { default as Vector } from './Vector.svelte';
49
+ export { default as WaffleX } from './WaffleX.svelte';
50
+ export { default as WaffleY } from './WaffleY.svelte';
49
51
  // HTML marks
50
52
  export { default as ColorLegend } from './ColorLegend.svelte';
51
53
  export { default as HTMLTooltip } from './HTMLTooltip.svelte';
@@ -1,5 +1,5 @@
1
1
  import { groupFacetsAndZ } from '../helpers/group.js';
2
- import { testFilter } from '../helpers/index.js';
2
+ import { isValid, testFilter } from '../helpers/index.js';
3
3
  import { reduceOutputs } from '../helpers/reduce.js';
4
4
  import { resolveChannel } from '../helpers/resolve.js';
5
5
  import { groups as d3Groups } from 'd3-array';
@@ -58,8 +58,12 @@ export function groupY(input, options = {}) {
58
58
  export function groupZ(input, options = {}) {
59
59
  return groupXYZ('z', input, options);
60
60
  }
61
+ const groupDimRaw = Symbol('groupDimRaw');
61
62
  function groupXYZ(dim, { data, ...channels }, options = {}) {
62
- if ((dim === 'z' ? channels.z || channels.fill || channels.stroke : channels[dim]) == null)
63
+ // console.log({ dim, data, channels, options });
64
+ if ((dim === 'z'
65
+ ? channels.z || channels.fill || channels.stroke || channels.fx || channels.fy
66
+ : channels[dim]) == null)
63
67
  throw new Error('you must provide a channel to group on ' + dim);
64
68
  const propName = options[`${dim}PropName`] != null
65
69
  ? options[`${dim}PropName`]
@@ -70,9 +74,11 @@ function groupXYZ(dim, { data, ...channels }, options = {}) {
70
74
  // group by x or y
71
75
  const groups = dim === 'z'
72
76
  ? [[null, data]]
73
- : d3Groups(data.filter((d) => testFilter(d, channels)), (d) => {
74
- const v = resolveChannel(dim, d, channels);
75
- return interval ? interval.round(v) : v;
77
+ : d3Groups(data
78
+ .filter((d) => testFilter(d, channels))
79
+ .map((d) => ({ ...d, [groupDimRaw]: resolveChannel(dim, d, channels) }))
80
+ .filter((d) => isValid(d[groupDimRaw])), (d) => {
81
+ return interval ? interval.floor(d[groupDimRaw]) : d[groupDimRaw];
76
82
  });
77
83
  const newData = [];
78
84
  let newChannels = omit({ ...channels }, 'filter');
@@ -11,6 +11,7 @@ export type ScaledDataRecord<T = Record<string | symbol, RawValue>> = Partial<{
11
11
  dx: number;
12
12
  dy: number;
13
13
  datum: DataRecord<T>;
14
+ resolved: ResolvedDataRecord<T>;
14
15
  valid: Boolean;
15
16
  index: number;
16
17
  };
@@ -6,7 +6,7 @@ export type Mark<T> = {
6
6
  data: DataRecord<T>[];
7
7
  options: T;
8
8
  };
9
- export type MarkType = 'area' | 'arrow' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'line' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY';
9
+ export type MarkType = 'area' | 'arrow' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'line' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'waffleX' | 'waffleY';
10
10
  export type MarkStyleProps = 'strokeDasharray' | 'strokeLinejoin' | 'strokeLinecap' | 'opacity' | 'cursor' | 'pointerEvents' | 'blend' | 'fill' | 'fillOpacity' | 'fontFamily' | 'fontWeight' | 'fontVariant' | 'fontSize' | 'fontStyle' | 'letterSpacing' | 'wordSpacing' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'x' | 'y' | 'clipPath' | 'mask' | 'filter' | 'angle' | 'radius' | 'symbol' | 'textAnchor' | 'textTransform' | 'textDecoration' | 'width';
11
11
  import type { MouseEventHandler } from 'svelte/elements';
12
12
  import type { ChannelAccessor, ConstantAccessor, DataRecord, RawValue } from './index.js';
@@ -1,9 +1,11 @@
1
1
  import type { ComponentProps } from 'svelte';
2
2
  import type { ColorScheme } from './colorScheme.js';
3
3
  import type { GeoProjection } from 'd3-geo';
4
- import type { ChannelAccessor, ChannelName, ColorScaleOptions, DataRecord, LegendScaleOptions, PlotScales, ScaleOptions, XScaleOptions, YScaleOptions } from './index.js';
4
+ import type { ChannelAccessor, ChannelName, ColorScaleOptions, DataRecord, LegendScaleOptions, PlotScales, RawValue, ScaleOptions, XScaleOptions, YScaleOptions } from './index.js';
5
5
  import type { Snippet } from 'svelte';
6
6
  import type { Area, AreaX, AreaY, Arrow, AxisX, AxisY, BarX, BarY, BoxX, BoxY, Brush, BrushX, BrushY, Cell, DifferenceY, Dot, Frame, Geo, Graticule, GridX, GridY, Image, Line, Link, Pointer, Rect, RectX, RectY, RuleX, RuleY, Sphere, Spike, Text, TickX, TickY, Vector } from '../marks/index.js';
7
+ import type WaffleX from '../marks/WaffleX.svelte';
8
+ import type WaffleY from '../marks/WaffleY.svelte';
7
9
  export type PlotState = {
8
10
  width: number;
9
11
  height: number;
@@ -298,6 +300,18 @@ export type PlotDefaults = {
298
300
  * default props for vector marks
299
301
  */
300
302
  vector: Partial<Omit<ComponentProps<typeof Vector>, IgnoreDefaults>>;
303
+ /**
304
+ * default props for waffle marks, applied to both waffleX and waffleY marks
305
+ */
306
+ waffle: Partial<Omit<ComponentProps<typeof WaffleX>, IgnoreDefaults>>;
307
+ /**
308
+ * default props for waffleX marks
309
+ */
310
+ waffleX: Partial<Omit<ComponentProps<typeof WaffleX>, IgnoreDefaults>>;
311
+ /**
312
+ * default props for waffleY marks
313
+ */
314
+ waffleY: Partial<Omit<ComponentProps<typeof WaffleY>, IgnoreDefaults>>;
301
315
  };
302
316
  export type PlotOptions = {
303
317
  /**
@@ -409,11 +423,11 @@ export type PlotOptions = {
409
423
  /**
410
424
  * Options for the shared x scale.
411
425
  */
412
- x: Partial<XScaleOptions>;
426
+ x: Partial<XScaleOptions> | false | RawValue[];
413
427
  /**
414
428
  * Options for the shared y scale
415
429
  */
416
- y: Partial<YScaleOptions>;
430
+ y: Partial<YScaleOptions> | false | RawValue[];
417
431
  /**
418
432
  * Options for the shared radius scale
419
433
  */
@@ -422,8 +436,8 @@ export type PlotOptions = {
422
436
  opacity: Partial<ScaleOptions>;
423
437
  symbol: Partial<LegendScaleOptions>;
424
438
  length: Partial<ScaleOptions>;
425
- fx: Partial<ScaleOptions>;
426
- fy: Partial<ScaleOptions>;
439
+ fx: Partial<XScaleOptions> | false | RawValue[];
440
+ fy: Partial<YScaleOptions> | false | RawValue[];
427
441
  children: Snippet<[
428
442
  {
429
443
  width: number;
@@ -106,6 +106,14 @@ export type XScaleOptions = ScaleOptions & {
106
106
  tickRotate: number;
107
107
  labelAnchor: 'auto' | 'left' | 'center' | 'right';
108
108
  tickFormat: false | Intl.NumberFormatOptions | ((d: RawValue) => string);
109
+ /**
110
+ * Enable word wrapping for axis tick labels, default true
111
+ */
112
+ wordWrap: boolean;
113
+ /**
114
+ * Remove duplicate ticks from axis, default true
115
+ */
116
+ removeDuplicateTicks: boolean;
109
117
  };
110
118
  export type YScaleOptions = ScaleOptions & {
111
119
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.6.0-pr-266.0",
3
+ "version": "0.7.0-pr-269.0",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",
@@ -54,7 +54,7 @@
54
54
  "devDependencies": {
55
55
  "@aitodotai/json-stringify-pretty-compact": "^1.3.0",
56
56
  "@emotion/css": "^11.13.5",
57
- "@sveltejs/adapter-auto": "^6.1.1",
57
+ "@sveltejs/adapter-auto": "^7.0.0",
58
58
  "@sveltejs/adapter-static": "^3.0.10",
59
59
  "@sveltejs/eslint-config": "^8.3.4",
60
60
  "@sveltejs/kit": "^2.48.5",
@@ -81,14 +81,14 @@
81
81
  "@types/topojson-client": "^3.1.5",
82
82
  "@typescript-eslint/eslint-plugin": "^8.46.4",
83
83
  "@typescript-eslint/parser": "^8.46.4",
84
- "csstype": "^3.2.0",
84
+ "csstype": "^3.2.1",
85
85
  "d3-dsv": "^3.0.1",
86
86
  "d3-fetch": "^3.0.1",
87
87
  "d3-force": "^3.0.0",
88
88
  "eslint": "^9.39.1",
89
89
  "eslint-config-prettier": "^10.1.8",
90
90
  "eslint-plugin-svelte": "3.13.0",
91
- "jsdom": "^26.1.0",
91
+ "jsdom": "^27.2.0",
92
92
  "prettier": "^3.6.2",
93
93
  "prettier-plugin-svelte": "^3.4.0",
94
94
  "puppeteer": "^24.30.0",