svelteplot 0.5.1 → 0.5.2

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.
@@ -1,5 +1,6 @@
1
- import type { ScaleName, ScaleType, ScaledChannelName } from './types/index.js';
1
+ import type { ChannelName, ScaleName, ScaleType, ScaledChannelName } from './types/index.js';
2
2
  export declare const SCALE_TYPES: Record<ScaleName, symbol>;
3
+ export declare const ORIGINAL_NAME_KEYS: Record<ChannelName, symbol>;
3
4
  export declare const SCALES: ScaleName[];
4
5
  export declare const VALID_SCALE_TYPES: Record<ScaleName, Set<ScaleType>>;
5
6
  /**
package/dist/constants.js CHANGED
@@ -10,6 +10,28 @@ export const SCALE_TYPES = {
10
10
  fy: Symbol('fy'),
11
11
  projection: Symbol('projection')
12
12
  };
13
+ export const ORIGINAL_NAME_KEYS = {
14
+ x: Symbol('origName_x'),
15
+ x1: Symbol('origName_x1'),
16
+ x2: Symbol('origName_x2'),
17
+ y: Symbol('origName_y'),
18
+ y1: Symbol('origName_y1'),
19
+ y2: Symbol('origName_y2'),
20
+ fill: Symbol('origName_color'),
21
+ stroke: Symbol('origName_color'),
22
+ opacity: Symbol('origName_opacity'),
23
+ symbol: Symbol('origName_symbol'),
24
+ r: Symbol('origName_r'),
25
+ z: Symbol('origName_z'),
26
+ sort: Symbol('origName_sort'),
27
+ filter: Symbol('origName_filter'),
28
+ interval: Symbol('origName_interval'),
29
+ length: Symbol('origName_length'),
30
+ fx: Symbol('origName_fx'),
31
+ fy: Symbol('origName_fy'),
32
+ fillOpacity: Symbol('origName_opacity'),
33
+ strokeOpacity: Symbol('origName_opacity')
34
+ };
13
35
  export const SCALES = [
14
36
  'x',
15
37
  'y',
@@ -65,7 +65,7 @@
65
65
  margin: 'auto',
66
66
  colorScheme: 'turbo',
67
67
  unknown: '#cccccc99',
68
-
68
+ sortOrdinalDomains: true,
69
69
  categoricalColorScheme: 'observable10',
70
70
  pointScaleHeight: 18,
71
71
  bandScaleHeight: 30,
@@ -365,7 +365,12 @@
365
365
  initialOpts: Partial<PlotOptions>,
366
366
  opts: PlotOptionsParameters
367
367
  ): PlotOptions {
368
- return mergeDeep<PlotOptions>({}, smartDefaultPlotOptions(opts), initialOpts);
368
+ return mergeDeep<PlotOptions>(
369
+ {},
370
+ { sortOrdinalDomains: DEFAULTS.sortOrdinalDomains },
371
+ smartDefaultPlotOptions(opts),
372
+ initialOpts
373
+ );
369
374
  }
370
375
 
371
376
  function maybeMargin(
@@ -79,7 +79,16 @@ export function autoScale({ name, type, domain, scaleOptions, plotOptions, plotW
79
79
  ...(type === 'band' || type === 'point'
80
80
  ? {
81
81
  align: scaleOptions.align,
82
- padding: maybeNumber(coalesce(scaleOptions.padding, plotOptions.padding, 0.15))
82
+ ...(type === 'point'
83
+ ? {
84
+ // point scales don't have paddingInner/Outer, only padding
85
+ padding: maybeNumber(coalesce(scaleOptions.padding, plotOptions.padding, 0.15))
86
+ }
87
+ : {
88
+ // padding: maybeNumber(coalesce(scaleOptions.padding, plotOptions.padding, 0.15)),
89
+ paddingInner: maybeNumber(coalesce(scaleOptions.paddingInner, scaleOptions.padding, plotOptions.padding, 0.15)),
90
+ paddingOuter: maybeNumber(coalesce(scaleOptions.paddingOuter, scaleOptions.padding, plotOptions.padding, 0.15))
91
+ })
83
92
  }
84
93
  : {})
85
94
  };
@@ -3,7 +3,7 @@ import type { Snippet } from 'svelte';
3
3
  /**
4
4
  * Returns first argument that is not null or undefined
5
5
  */
6
- export declare function coalesce(...args: (RawValue | undefined | null)[]): RawValue | null;
6
+ export declare function coalesce(...args: (RawValue | undefined | null)[]): string | number | boolean | symbol | object | null;
7
7
  export declare function testFilter<T>(datum: T, options: Channels<T>): true | T | null;
8
8
  export declare function randomId(): string;
9
9
  export declare function isSnippet(value: unknown): value is Snippet;
@@ -1,5 +1,10 @@
1
1
  import { isDate } from './typeChecks.js';
2
2
  export default function (value) {
3
3
  const t = typeof value;
4
- return t === 'string' || t === 'number' || t === 'boolean' || isDate(value) || t === null;
4
+ return (t === 'string' ||
5
+ t === 'number' ||
6
+ t === 'boolean' ||
7
+ t === 'object' ||
8
+ isDate(value) ||
9
+ t === null);
5
10
  }
@@ -1,6 +1,7 @@
1
1
  import { min, max, mode, sum, mean, median, variance, deviation, quantile } from 'd3-array';
2
2
  import { resolveChannel } from './resolve.js';
3
3
  import { POSITION_CHANNELS } from './index.js';
4
+ import { ORIGINAL_NAME_KEYS } from '../constants.js';
4
5
  const niceReduceNames = {
5
6
  count: 'Frequency',
6
7
  deviation: 'Standard Deviation',
@@ -72,10 +73,10 @@ export function reduceOutputs(newDatum, data, options, outputs, channels, newCha
72
73
  if (typeof channels[k] === 'string') {
73
74
  // the named reducer is applied to a column name, so we can use a combination
74
75
  // of both as axis labels, e.g. MEAN(weight)
75
- newChannels[`__${k}_origField`] = `${reducerName} ( ${channels[k]} )`;
76
+ newChannels[ORIGINAL_NAME_KEYS[k]] = `${reducerName} ( ${channels[k]} )`;
76
77
  }
77
78
  else {
78
- newChannels[`__${k}_origField`] = reducerName;
79
+ newChannels[ORIGINAL_NAME_KEYS[k]] = reducerName;
79
80
  }
80
81
  }
81
82
  }
@@ -40,3 +40,4 @@ export declare function looksLikeANumber(input: string | number): boolean;
40
40
  export declare function projectXY(scales: PlotScales, x: RawValue, y: RawValue, useXScale?: boolean, useYScale?: boolean): [number, number];
41
41
  export declare function projectX(channel: 'x' | 'x1' | 'x2', scales: PlotScales, value: RawValue): number;
42
42
  export declare function projectY(channel: 'y' | 'y1' | 'y2', scales: PlotScales, value: RawValue): number;
43
+ export declare function isOrdinalScale(scaleType: ScaleType): scaleType is "ordinal" | "point" | "band" | "categorical" | "threshold";
@@ -1,6 +1,6 @@
1
1
  import { extent, ascending } from 'd3-array';
2
2
  import { isColorOrNull, isDate, isDateOrNull, isNumberOrNull, isNumberOrNullOrNaN, isStringOrNull } from './typeChecks.js';
3
- import { CHANNEL_SCALE, VALID_SCALE_TYPES } from '../constants.js';
3
+ import { CHANNEL_SCALE, ORIGINAL_NAME_KEYS, VALID_SCALE_TYPES } from '../constants.js';
4
4
  import { isSymbolOrNull } from './typeChecks.js';
5
5
  import { resolveProp, toChannelOption } from './resolve.js';
6
6
  import isDataRecord from './isDataRecord.js';
@@ -48,7 +48,7 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
48
48
  let manualActiveMarks = 0;
49
49
  const propNames = new Set();
50
50
  const uniqueScaleProps = new Set();
51
- let sortOrdinalDomain = true;
51
+ let sortOrdinalDomain = plotOptions.sortOrdinalDomains ?? true;
52
52
  for (const mark of marks) {
53
53
  // we only sort the scale domain alphabetically, if none of the
54
54
  // marks that map to it are using the `sort` transform. Note that
@@ -126,9 +126,9 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
126
126
  }
127
127
  // special handling of marks using the stackX/stackY transform
128
128
  if ((name === 'x' || name === 'y') &&
129
- mark.options[`__${name}_origField`] &&
130
- !mark.options[`__${name}_origField`].startsWith('__')) {
131
- propNames.add(mark.options[`__${name}_origField`]);
129
+ mark.options[ORIGINAL_NAME_KEYS[name]] &&
130
+ !mark.options[ORIGINAL_NAME_KEYS[name]].startsWith('__')) {
131
+ propNames.add(mark.options[ORIGINAL_NAME_KEYS[name]]);
132
132
  }
133
133
  }
134
134
  else {
@@ -152,11 +152,7 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
152
152
  throw new Error(`Invalid scale type ${type} for scale
153
153
  ${name}. Valid types are ${[...VALID_SCALE_TYPES[name]].join(', ')}`);
154
154
  }
155
- const isOrdinal = type === 'band' ||
156
- type === 'point' ||
157
- type === 'ordinal' ||
158
- type === 'categorical' ||
159
- type === 'threshold';
155
+ const isOrdinal = isOrdinalScale(type);
160
156
  if (isOrdinal && sortOrdinalDomain) {
161
157
  valueArr.sort(ascending);
162
158
  }
@@ -351,3 +347,10 @@ export function projectY(channel, scales, value) {
351
347
  ? scales.y.fn.bandwidth()
352
348
  : 0));
353
349
  }
350
+ export function isOrdinalScale(scaleType) {
351
+ return (scaleType === 'band' ||
352
+ scaleType === 'point' ||
353
+ scaleType === 'ordinal' ||
354
+ scaleType === 'categorical' ||
355
+ scaleType === 'threshold');
356
+ }
@@ -14,6 +14,7 @@
14
14
  sort?: ConstantAccessor<RawValue> | { channel: 'stroke' | 'fill' };
15
15
  stack?: Partial<StackOptions>;
16
16
  canvas?: boolean;
17
+ areaClass?: ConstantAccessor<string, Datum>;
17
18
  }
18
19
 
19
20
  import Mark from '../Mark.svelte';
@@ -37,7 +38,6 @@
37
38
  ChannelAccessor,
38
39
  ScaledDataRecord,
39
40
  LinkableMarkProps,
40
- PlotDefaults,
41
41
  RawValue
42
42
  } from '../types/index.js';
43
43
  import type { StackOptions } from '../transforms/stack.js';
@@ -59,6 +59,7 @@
59
59
  curve = 'linear' as CurveName,
60
60
  tension = 0,
61
61
  class: className = '',
62
+ areaClass,
62
63
  canvas = false,
63
64
  ...options
64
65
  }: AreaMarkProps = $derived({ ...DEFAULTS, ...markProps });
@@ -114,13 +115,14 @@
114
115
  {data}
115
116
  channels={['x1', 'x2', 'y1', 'y2', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
116
117
  required={['x1', 'y1']}
118
+ {...markProps}
117
119
  {...options}>
118
120
  {#snippet children({ mark, usedScales, scaledData })}
119
121
  {@const grouped = groupAndSort(scaledData)}
120
122
  {#if canvas}
121
123
  <AreaCanvas groupedAreaData={grouped} {mark} {usedScales} {areaPath} />
122
124
  {:else}
123
- <GroupMultiple length={grouped.length}>
125
+ <GroupMultiple class={className} length={grouped.length}>
124
126
  {#each grouped as areaData, i (i)}
125
127
  {@const datum = areaData[0]}
126
128
  {#if areaData.length > 0}
@@ -134,7 +136,11 @@
134
136
  usedScales
135
137
  )}
136
138
  <path
137
- class={['svelteplot-area', className, styleClass]}
139
+ class={[
140
+ 'area',
141
+ resolveProp(areaClass, areaData[0].datum),
142
+ styleClass
143
+ ]}
138
144
  clip-path={options.clipPath}
139
145
  d={areaPath(areaData)}
140
146
  {@attach addEventHandlers({
@@ -78,6 +78,7 @@ declare class __sveltets_Render<Datum extends DataRecord> {
78
78
  };
79
79
  stack?: Partial<StackOptions>;
80
80
  canvas?: boolean;
81
+ areaClass?: ConstantAccessor<string, Datum>;
81
82
  };
82
83
  events(): {};
83
84
  slots(): {};
@@ -76,6 +76,7 @@ declare class __sveltets_Render<Datum extends DataRow> {
76
76
  };
77
77
  stack?: Partial<import("../transforms/stack.js").StackOptions>;
78
78
  canvas?: boolean;
79
+ areaClass?: import("../types/index.js").ConstantAccessor<string, Record<string | symbol, import("../types/data").RawValue>>;
79
80
  }, "y1" | "y2"> & {
80
81
  x?: ChannelAccessor<Datum>;
81
82
  y?: ChannelAccessor<Datum>;
@@ -59,7 +59,7 @@
59
59
  stackX(
60
60
  intervalX(
61
61
  // by default, sort by y channel (the ordinal labels)
62
- sort(recordizeX({ data, sort: { channel: 'y' }, ...options })),
62
+ sort(recordizeX({ data, ...options })),
63
63
  { plot }
64
64
  ),
65
65
  stack
@@ -59,7 +59,7 @@
59
59
  stackY(
60
60
  intervalY(
61
61
  // by default, sort by x channel (the ordinal labels)
62
- sort(recordizeY({ data, sort: { channel: 'x' }, ...options })),
62
+ sort(recordizeY({ data, ...options })),
63
63
  { plot }
64
64
  ),
65
65
  stack
@@ -10,6 +10,8 @@
10
10
  import { type ComponentProps } from 'svelte';
11
11
  import type BoxY from './BoxY.svelte';
12
12
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
13
+ import { IS_SORTED } from '../transforms/sort';
14
+ import { sort } from 'd3-array';
13
15
 
14
16
  let markProps: BoxXMarkProps = $props();
15
17
 
@@ -70,6 +72,8 @@
70
72
  ...(fy != null && { fy: FY })
71
73
  });
72
74
 
75
+ const sortProps = { [IS_SORTED]: true };
76
+
73
77
  const boxData = $derived(
74
78
  grouped
75
79
  .map((row) => {
@@ -107,7 +111,15 @@
107
111
  </script>
108
112
 
109
113
  <GroupMultiple class="box-x {className || ''}" length={className ? 2 : grouped.length}>
110
- <RuleY data={boxData} y={Y} x1={MIN} x2={P25} {stroke} {...rule || {}} {...facets} />
114
+ <RuleY
115
+ data={boxData}
116
+ y={Y}
117
+ x1={MIN}
118
+ x2={P25}
119
+ {stroke}
120
+ {...rule || {}}
121
+ {...facets}
122
+ {...sortProps} />
111
123
  <RuleY data={boxData} y={Y} x1={P75} x2={MAX} {stroke} {...rule || {}} {...facets} />
112
124
  <BarX data={boxData} y={Y} x1={P25} x2={P75} {fill} {stroke} {...facets} {...bar || {}} />
113
125
  {#if tickMedian}
@@ -33,6 +33,7 @@
33
33
  import { resolveChannel } from '../helpers/resolve.js';
34
34
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
35
35
  import type { BaseMarkProps, ChannelAccessor, DataRecord } from '../types';
36
+ import { IS_SORTED } from '../transforms/sort';
36
37
 
37
38
  let markProps: BoxYMarkProps = $props();
38
39
 
@@ -93,6 +94,8 @@
93
94
  ...(fy != null && { fy: FY })
94
95
  });
95
96
 
97
+ const sortProps = { [IS_SORTED]: true };
98
+
96
99
  const boxData = $derived(
97
100
  grouped
98
101
  .map((row) => {
@@ -129,7 +132,15 @@
129
132
  </script>
130
133
 
131
134
  <GroupMultiple class="box-y {className || ''}" length={className ? 2 : grouped.length}>
132
- <RuleX data={boxData} x={X} y1={MIN} y2={P25} {stroke} {...rule || {}} {...facets} />
135
+ <RuleX
136
+ data={boxData}
137
+ x={X}
138
+ y1={MIN}
139
+ y2={P25}
140
+ {stroke}
141
+ {...sortProps}
142
+ {...rule || {}}
143
+ {...facets} />
133
144
  <RuleX data={boxData} x={X} y1={P75} y2={MAX} {stroke} {...rule || {}} {...facets} />
134
145
  <BarY data={boxData} x={X} y1={P25} y2={P75} {fill} {stroke} {...facets} {...bar || {}} />
135
146
  {#if tickMedian}
@@ -32,6 +32,7 @@
32
32
  import { addEventHandlers } from './helpers/events.js';
33
33
  import Anchor from './helpers/Anchor.svelte';
34
34
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
35
+ import { isOrdinalScale } from '../helpers/scales.js';
35
36
 
36
37
  const DEFAULTS = {
37
38
  ...getPlotDefaults().dot
@@ -60,7 +61,11 @@
60
61
  recordizeXY({
61
62
  data,
62
63
  // sort by descending radius by default
63
- ...(options.r ? { sort: { channel: '-r' } } : {}),
64
+ ...(options.r &&
65
+ !isOrdinalScale(plot.scales.x.type) &&
66
+ !isOrdinalScale(plot.scales.y.type)
67
+ ? { sort: { channel: '-r' } }
68
+ : {}),
64
69
  ...options,
65
70
  ...(options.fill === true ? { fill: 'currentColor' } : {})
66
71
  })
@@ -31,6 +31,7 @@
31
31
  import GroupMultiple from './helpers/GroupMultiple.svelte';
32
32
  import RectPath from './helpers/RectPath.svelte';
33
33
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
34
+ import { IS_SORTED } from '../transforms/sort';
34
35
 
35
36
  let markProps: RectMarkProps = $props();
36
37
 
@@ -59,6 +60,7 @@
59
60
  type="rect"
60
61
  required={[]}
61
62
  channels={['x1', 'x2', 'y1', 'y2', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
63
+ {...markProps}
62
64
  {...args}>
63
65
  {#snippet children({ usedScales, scaledData })}
64
66
  <GroupMultiple class={scaledData.length > 1 ? className : null} length={scaledData.length}>
@@ -45,7 +45,11 @@
45
45
  const args = $derived(recordizeX({ data, ...options }, { withIndex: false }));
46
46
  </script>
47
47
 
48
- <Mark type="ruleX" channels={['x', 'y1', 'y2', 'stroke', 'opacity', 'strokeOpacity']} {...args}>
48
+ <Mark
49
+ type="ruleX"
50
+ channels={['x', 'y1', 'y2', 'stroke', 'opacity', 'strokeOpacity']}
51
+ {...markProps}
52
+ {...args}>
49
53
  {#snippet children({ mark, scaledData, usedScales })}
50
54
  <GroupMultiple class="rule-x {className || ''}" length={className ? 2 : scaledData.length}>
51
55
  {#each scaledData as d, i (i)}
@@ -24,6 +24,7 @@
24
24
  ChannelAccessor
25
25
  } from '../types/index.js';
26
26
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
27
+ import { IS_SORTED } from '../transforms/sort';
27
28
 
28
29
  let markProps: RuleYMarkProps = $props();
29
30
  const DEFAULTS = {
@@ -44,7 +45,11 @@
44
45
  const args = $derived(recordizeY({ data, ...options }, { withIndex: false }));
45
46
  </script>
46
47
 
47
- <Mark type="ruleY" channels={['y', 'x1', 'x2', 'stroke', 'opacity', 'strokeOpacity']} {...args}>
48
+ <Mark
49
+ type="ruleY"
50
+ channels={['y', 'x1', 'x2', 'stroke', 'opacity', 'strokeOpacity']}
51
+ {...markProps}
52
+ {...args}>
48
53
  {#snippet children({ scaledData, usedScales })}
49
54
  <GroupMultiple class="rule-y {className || ''}" length={className ? 2 : args.data.length}>
50
55
  {#each scaledData as d, i (i)}
@@ -61,7 +61,11 @@
61
61
  let testFacet = $derived(getTestFacet());
62
62
  </script>
63
63
 
64
- <Mark type="tickX" channels={['x', 'y', 'stroke', 'opacity', 'strokeOpacity']} {...args}>
64
+ <Mark
65
+ type="tickX"
66
+ channels={['x', 'y', 'stroke', 'opacity', 'strokeOpacity']}
67
+ {...markProps}
68
+ {...args}>
65
69
  {#snippet children({ mark, usedScales })}
66
70
  <g class="tick-x">
67
71
  {#each args.data as datum, i (i)}
@@ -60,7 +60,11 @@
60
60
  let testFacet = $derived(getTestFacet());
61
61
  </script>
62
62
 
63
- <Mark type="tickY" channels={['x', 'y', 'stroke', 'opacity', 'strokeOpacity']} {...args}>
63
+ <Mark
64
+ type="tickY"
65
+ channels={['x', 'y', 'stroke', 'opacity', 'strokeOpacity']}
66
+ {...markProps}
67
+ {...args}>
64
68
  {#snippet children({ mark, usedScales })}
65
69
  <g class="tick-y">
66
70
  {#each args.data as datum, i (i)}
@@ -81,7 +81,9 @@ Helper component for rendering rectangular marks in SVG
81
81
  useInsetAsFallbackVertically ? inset : 0
82
82
  ) as number)
83
83
  );
84
- const borderRadius = $derived((options.borderRadius ?? 0) as BorderRadius);
84
+ const borderRadius = $derived(
85
+ resolveProp(options.borderRadius, datum?.datum, 0) as BorderRadius
86
+ );
85
87
  const hasBorderRadius = $derived(
86
88
  (typeof borderRadius === 'number' && borderRadius > 0) ||
87
89
  (typeof borderRadius === 'object' &&
@@ -4,6 +4,15 @@ import { bin as d3Bin, extent, thresholdFreedmanDiaconis, thresholdScott, thresh
4
4
  import { reduceOutputs } from '../helpers/reduce.js';
5
5
  import { groupFacetsAndZ } from '../helpers/group.js';
6
6
  import { isDate } from '../helpers/typeChecks.js';
7
+ import { ORIGINAL_NAME_KEYS } from '../constants';
8
+ const CHANNELS = {
9
+ x: Symbol('x'),
10
+ x1: Symbol('x1'),
11
+ x2: Symbol('x2'),
12
+ y: Symbol('y'),
13
+ y1: Symbol('y1'),
14
+ y2: Symbol('y2')
15
+ };
7
16
  const ThresholdGenerators = {
8
17
  auto: thresholdScott,
9
18
  scott: thresholdScott,
@@ -41,19 +50,19 @@ function binBy(byDim, { data, ...channels }, options) {
41
50
  [byDim === 'x' ? 'insetLeft' : 'insetTop']: 0.5,
42
51
  [byDim === 'x' ? 'insetRight' : 'insetBottom']: 0.5,
43
52
  ...channels,
44
- [`${byDim}`]: `__${byDim}`,
45
- [`${byDim}1`]: `__${byDim}1`,
46
- [`${byDim}2`]: `__${byDim}2`,
47
- [`__${byDim}_origField`]: typeof channels[byDim] === 'string' ? channels[byDim] : null
53
+ [`${byDim}`]: CHANNELS[byDim], // `__${byDim}`,
54
+ [`${byDim}1`]: CHANNELS[`${byDim}1`],
55
+ [`${byDim}2`]: CHANNELS[`${byDim}2`],
56
+ [ORIGINAL_NAME_KEYS[byDim]]: typeof channels[byDim] === 'string' ? channels[byDim] : null
48
57
  };
49
58
  const newData = [];
50
59
  let passedGroups = [];
51
60
  const bins = bin(data);
52
61
  (options.cumulative < 0 ? bins.toReversed() : bins).forEach((group) => {
53
62
  const itemBinProps = {
54
- [`__${byDim}1`]: group.x0,
55
- [`__${byDim}2`]: group.x1,
56
- [`__${byDim}`]: isDate(group.x0)
63
+ [CHANNELS[`${byDim}1`]]: group.x0,
64
+ [CHANNELS[`${byDim}2`]]: group.x1,
65
+ [CHANNELS[`${byDim}`]]: isDate(group.x0)
57
66
  ? new Date(Math.round((group.x0.getTime() + group.x1.getTime()) * 0.5))
58
67
  : (group.x0 + group.x1) * 0.5
59
68
  };
@@ -101,11 +110,12 @@ export function bin({ data, ...channels }, options = { thresholds: 'auto', cumul
101
110
  // channels.x is the input
102
111
  binX.value((d) => resolveChannel('x', d, channels));
103
112
  binY.value((d) => resolveChannel('y', d, channels));
113
+ let yThresholds = [];
104
114
  if (interval) {
105
115
  const [xlo, xhi] = extent(data.map((d) => resolveChannel('x', d, channels)));
106
116
  const [ylo, yhi] = extent(data.map((d) => resolveChannel('y', d, channels)));
107
117
  binX.thresholds(maybeInterval(interval).range(xlo, xhi));
108
- binY.thresholds(maybeInterval(interval).range(ylo, yhi));
118
+ binY.thresholds((yThresholds = maybeInterval(interval).range(ylo, yhi)));
109
119
  }
110
120
  else if (thresholds) {
111
121
  // when binning in x and y, we need to ensure we are using consistent thresholds
@@ -114,7 +124,7 @@ export function bin({ data, ...channels }, options = { thresholds: 'auto', cumul
114
124
  : thresholds;
115
125
  binX.thresholds(t);
116
126
  binY.thresholds(t);
117
- const yThresholds = binY(data)
127
+ yThresholds = binY(data)
118
128
  .slice(1)
119
129
  .map((g) => g.x0);
120
130
  binY.thresholds(yThresholds);
@@ -124,14 +134,14 @@ export function bin({ data, ...channels }, options = { thresholds: 'auto', cumul
124
134
  let newChannels = {
125
135
  inset: 0.5,
126
136
  ...channels,
127
- x: '__x',
128
- x1: '__x1',
129
- x2: '__x2',
130
- y: '__y',
131
- y1: '__y1',
132
- y2: '__y2',
133
- __x_origField: typeof channels.x === 'string' ? channels.x : null,
134
- __y_origField: typeof channels.y === 'string' ? channels.y : null
137
+ x: CHANNELS.x,
138
+ x1: CHANNELS.x1,
139
+ x2: CHANNELS.x2,
140
+ y: CHANNELS.y,
141
+ y1: CHANNELS.y1,
142
+ y2: CHANNELS.y2,
143
+ [ORIGINAL_NAME_KEYS.x]: typeof channels.x === 'string' ? channels.x : null,
144
+ [ORIGINAL_NAME_KEYS.y]: typeof channels.y === 'string' ? channels.y : null
135
145
  };
136
146
  const groupBy = channels.z ? 'z' : channels.fill ? 'fill' : channels.stroke ? 'stroke' : true;
137
147
  const groupByPropName = groupBy !== true && typeof channels[groupBy] === 'string' ? channels[groupBy] : '__group';
@@ -141,20 +151,30 @@ export function bin({ data, ...channels }, options = { thresholds: 'auto', cumul
141
151
  const newData = [];
142
152
  binX(data).forEach((groupX) => {
143
153
  const newRecordBaseX = {
144
- __x1: groupX.x0,
145
- __x2: groupX.x1,
146
- __x: isDate(groupX.x0)
154
+ [CHANNELS.x1]: groupX.x0,
155
+ [CHANNELS.x2]: groupX.x1,
156
+ [CHANNELS.x]: isDate(groupX.x0)
147
157
  ? new Date(Math.round((groupX.x0.getTime() + groupX.x1.getTime()) * 0.5))
148
158
  : (groupX.x0 + groupX.x1) * 0.5
149
159
  };
150
- binY(groupX).forEach((groupY) => {
160
+ const [ylo, yhi] = extent(groupX.map((d) => resolveChannel('y', d, channels)));
161
+ const tExtentLo = yThresholds.filter((d) => d < ylo).at(-1);
162
+ const tExtentHi = yThresholds.filter((d) => d > yhi).at(0);
163
+ binY(groupX).forEach((groupY, i) => {
164
+ if (groupY.length === 0)
165
+ return;
166
+ // The first bin.x0 is always equal to the minimum domain value,
167
+ // and the last bin.x1 is always equal to the maximum domain value,
168
+ // therefore we need to align with the thresholds
169
+ const y1 = groupY.x0 === ylo ? tExtentLo : groupY.x0;
170
+ const y2 = groupY.x1 === yhi ? tExtentHi : groupY.x1;
151
171
  const newRecordBaseY = {
152
172
  ...newRecordBaseX,
153
- __y1: groupY.x0,
154
- __y2: groupY.x1,
155
- __y: isDate(groupY.x0)
156
- ? new Date(Math.round((groupY.x0.getTime() + groupY.x1.getTime()) * 0.5))
157
- : (groupY.x0 + groupY.x1) * 0.5
173
+ [CHANNELS.y1]: y1,
174
+ [CHANNELS.y2]: y2,
175
+ [CHANNELS.y]: isDate(y1)
176
+ ? new Date(Math.round((y1.getTime() + y2.getTime()) * 0.5))
177
+ : (y1 + y2) * 0.5
158
178
  };
159
179
  const newGroupChannels = groupFacetsAndZ(groupY, channels, (items, itemGroupProps) => {
160
180
  const newRecord = {
@@ -4,10 +4,7 @@ export declare const IS_SORTED: unique symbol;
4
4
  export declare function sort<T>({ data, ...channels }: TransformArg<T>, options?: {
5
5
  reverse?: boolean;
6
6
  }): {
7
- [IS_SORTED]: string | number | true | symbol | Date | ((a: import("../types/index.js").RawValue, b: import("../types/index.js").RawValue) => number) | {
8
- channel: string;
9
- order?: "ascending" | "descending";
10
- } | ((d: T, index: number) => import("../types/index.js").RawValue);
7
+ [IS_SORTED]: string | number | true | symbol | object;
11
8
  sort: null;
12
9
  filter?: import("../types/index.js").ConstantAccessor<boolean, T>;
13
10
  facet?: "auto" | "include" | "exclude" | undefined;
@@ -5,7 +5,7 @@ export type StackOptions = {
5
5
  offset: null | StackOffset;
6
6
  order: null | StackOrder;
7
7
  reverse: boolean;
8
- };
8
+ } | false;
9
9
  export declare function stackY<T>({ data, ...channels }: TransformArg<T>, opts?: Partial<StackOptions>): TransformArg<T>;
10
10
  export declare function stackX<T>({ data, ...channels }: TransformArg<T>, opts?: Partial<StackOptions>): TransformArg<T>;
11
11
  export declare function stackMosaicX<T>(args: any, opts: any): {
@@ -5,7 +5,7 @@ import { sum, groups as d3Groups, min, range } from 'd3-array';
5
5
  import { groupFacetsAndZ } from '../helpers/group';
6
6
  import { filter } from './filter.js';
7
7
  import { sort } from './sort.js';
8
- import { INDEX } from '../constants.js';
8
+ import { INDEX, ORIGINAL_NAME_KEYS } from '../constants.js';
9
9
  import { indexData, RAW_VALUE } from './recordize.js';
10
10
  const S = {
11
11
  x: Symbol('x'),
@@ -38,6 +38,10 @@ const STACK_OFFSET = {
38
38
  normalize: stackOffsetExpand
39
39
  };
40
40
  function stackXY(byDim, data, channels, options) {
41
+ if (options === false) {
42
+ // no stacking
43
+ return { data, ...channels };
44
+ }
41
45
  // we need to stack the data for each facet separately
42
46
  const groupFacetsBy = [
43
47
  channels.fx != null ? 'fx' : null,
@@ -143,8 +147,8 @@ function stackXY(byDim, data, channels, options) {
143
147
  data: out,
144
148
  ...channels,
145
149
  [byDim]: undefined,
146
- ...(typeof channels[byDim] === 'string' && !channels[`__${byDim}_origField`]
147
- ? { [`__${byDim}_origField`]: channels[byDim] }
150
+ ...(typeof channels[byDim] === 'string' && !channels[ORIGINAL_NAME_KEYS[byDim]]
151
+ ? { [ORIGINAL_NAME_KEYS[byDim]]: channels[byDim] }
148
152
  : {}),
149
153
  ...{ [byLow]: S[byLow], [byHigh]: S[byHigh] }
150
154
  };
@@ -158,6 +162,8 @@ export function stackX({ data, ...channels }, opts = {}) {
158
162
  return stackXY('x', data, channels, applyDefaults(opts));
159
163
  }
160
164
  function applyDefaults(opts) {
165
+ if (opts === false)
166
+ return false;
161
167
  if (opts.offset === 'wiggle' && opts.order === undefined) {
162
168
  return { ...DEFAULT_STACK_OPTIONS, order: 'inside-out', ...opts };
163
169
  }
@@ -1,5 +1,5 @@
1
1
  import type { ConstantAccessor, RawValue } from './index.js';
2
- export type Channels<T> = Record<string, ChannelAccessor<T> | ConstantAccessor<T, string | number | boolean | symbol>>;
2
+ export type Channels<T> = Record<string | symbol, ChannelAccessor<T> | ConstantAccessor<T, string | number | boolean | symbol>>;
3
3
  export type ChannelAccessor<T = Record<string | symbol, RawValue>> = ChannelValue<T> | {
4
4
  /** the channel value */
5
5
  value: ChannelValue<T>;
@@ -1,5 +1,5 @@
1
1
  import type { ScaledChannelName, ScaledChannelType } from './channel.js';
2
- export type RawValue = number | Date | boolean | string | symbol;
2
+ export type RawValue = number | Date | boolean | string | symbol | object;
3
3
  export type DataRecord<T = Record<string | symbol, RawValue>> = T;
4
4
  export type ResolvedDataRecord<T = Record<string | symbol, RawValue>> = Partial<Record<ScaledChannelName, any>> & {
5
5
  datum: DataRecord<T>;
@@ -126,5 +126,5 @@ export type BaseRectMarkProps<T> = {
126
126
  insetTop?: ConstantAccessor<number, T>;
127
127
  insetRight?: ConstantAccessor<number, T>;
128
128
  insetBottom?: ConstantAccessor<number, T>;
129
- borderRadius?: BorderRadius;
129
+ borderRadius?: ConstantAccessor<BorderRadius, T>;
130
130
  };
@@ -112,6 +112,10 @@ export type PlotDefaults = {
112
112
  * default dot radius for line markers, used in dot, circle, and circle-stroke markers
113
113
  */
114
114
  markerDotRadius: number;
115
+ /**
116
+ * if set to true, ordinal domains will be sorted alphabetically
117
+ */
118
+ sortOrdinalDomains: boolean;
115
119
  /**
116
120
  * default props for area marks, applied to area, areaX, and areaY marks
117
121
  */
@@ -467,5 +471,9 @@ export type PlotOptions = {
467
471
  * pass a @emotion/css function to style plot using dynamic classes
468
472
  */
469
473
  css: (d: string) => string | undefined;
474
+ /**
475
+ * if set to true, ordinal domains will be sorted alphabetically
476
+ */
477
+ sortOrdinalDomains: boolean;
470
478
  };
471
479
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",
@@ -112,7 +112,7 @@
112
112
  "fast-equals": "^5.3.2",
113
113
  "interval-tree-1d": "^1.0.4",
114
114
  "merge-deep": "^3.0.3",
115
- "svelte": "5.41.3"
115
+ "svelte": "5.43.0"
116
116
  },
117
117
  "scripts": {
118
118
  "dev": "vite dev",