svelteplot 0.4.4-pr-205.0 → 0.4.4-pr-105.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.
@@ -69,6 +69,7 @@
69
69
  numberFormat: {
70
70
  style: 'decimal',
71
71
  // notation: 'compact',
72
+ useGrouping: 'min2',
72
73
  compactDisplay: 'short'
73
74
  },
74
75
  markerDotRadius: 3,
@@ -10,9 +10,10 @@
10
10
  import Area from './Area.svelte';
11
11
  import { renameChannels } from '../transforms/rename.js';
12
12
  import { stackY } from '../transforms/stack.js';
13
- import { recordizeY } from '../transforms/recordize.js';
13
+ import { RAW_VALUE, recordizeY } from '../transforms/recordize.js';
14
14
  import type { ChannelAccessor, DataRow, PlotDefaults } from '../types/index.js';
15
15
  import { getContext, type Component, type ComponentProps } from 'svelte';
16
+ import { area } from 'd3-shape';
16
17
 
17
18
  let markProps: AreaYMarkProps = $props();
18
19
 
@@ -26,7 +26,7 @@
26
26
  ? plot.options.color.tickFormat
27
27
  : Intl.NumberFormat(
28
28
  plot.options.locale,
29
- plot.options.color.tickFormat || DEFAULTS.numberFormat
29
+ plot.options.color.tickFormat || { ...DEFAULTS.numberFormat, notation: 'compact' }
30
30
  ).format
31
31
  );
32
32
  const randId = Math.round(Math.random() * 1e6).toFixed(32);
@@ -13,12 +13,15 @@ export function indexData(data) {
13
13
  export function recordizeX({ data, ...channels }, { withIndex } = { withIndex: true }) {
14
14
  const dataIsRawValueArray = !isDataRecord(data[0]) && !Array.isArray(data[0]) && channels.x == null;
15
15
  if (dataIsRawValueArray) {
16
+ // we remove x, x1 and x2 from the channels since they make no sense when
17
+ // the data is a raw value array
18
+ const { x, x1, x2, ...nonXChannels } = channels;
16
19
  return {
17
20
  data: data.map((value, index) => ({
18
21
  [RAW_VALUE]: value,
19
22
  [INDEX]: index
20
23
  })),
21
- ...channels,
24
+ ...nonXChannels,
22
25
  x: RAW_VALUE,
23
26
  ...(withIndex ? { y: INDEX } : {})
24
27
  };
@@ -34,12 +37,15 @@ export function recordizeY({ data, ...channels }, { withIndex } = { withIndex: t
34
37
  return { data, ...channels };
35
38
  const dataIsRawValueArray = !isDataRecord(data[0]) && !Array.isArray(data[0]) && channels.y == null;
36
39
  if (dataIsRawValueArray) {
40
+ // we remove y, y1 and y2 from the channels since they make no sense when
41
+ // the data is a raw value array
42
+ const { y, y1, y2, ...nonYChannels } = channels;
37
43
  return {
38
44
  data: Array.from(data).map((value, index) => ({
39
45
  [INDEX]: index,
40
46
  [RAW_VALUE]: value
41
47
  })),
42
- ...channels,
48
+ ...nonYChannels,
43
49
  ...(withIndex ? { x: INDEX } : {}),
44
50
  y: RAW_VALUE
45
51
  };
@@ -27,7 +27,19 @@ export function sort({ data, ...channels }, options = {}) {
27
27
  ...d,
28
28
  [SORT_KEY]: resolveChannel('sort', d, { ...channels, sort })
29
29
  }))
30
- .toSorted((a, b) => (a[SORT_KEY] > b[SORT_KEY] ? 1 : a[SORT_KEY] < b[SORT_KEY] ? -1 : 0) *
30
+ .map((d) => ({
31
+ ...d,
32
+ [SORT_KEY]: typeof d[SORT_KEY] === 'number' && !Number.isFinite(d[SORT_KEY])
33
+ ? Number.POSITIVE_INFINITY
34
+ : d[SORT_KEY]
35
+ }))
36
+ .toSorted((a, b) => (typeof a[SORT_KEY] === 'string' && typeof b[SORT_KEY] === 'string'
37
+ ? a[SORT_KEY].localeCompare(b[SORT_KEY])
38
+ : a[SORT_KEY] > b[SORT_KEY]
39
+ ? 1
40
+ : a[SORT_KEY] < b[SORT_KEY]
41
+ ? -1
42
+ : 0) *
31
43
  (options.reverse ||
32
44
  (isDataRecord(sort) && sort?.order === 'descending')
33
45
  ? -1
@@ -6,7 +6,7 @@ export type StackOptions = {
6
6
  order: null | StackOrder;
7
7
  reverse: boolean;
8
8
  };
9
- export declare function stackY<T>({ data, ...channels }: T, opts?: Partial<StackOptions>): T;
10
- export declare function stackX({ data, ...channels }: TransformArg, opts?: Partial<StackOptions>): TransformArg;
9
+ export declare function stackY<T>({ data, ...channels }: TransformArg<T>, opts?: Partial<StackOptions>): TransformArg<T>;
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): any;
12
12
  export declare function stackMosaicY<T>(args: any, opts: any): any;
@@ -1,12 +1,22 @@
1
1
  import isDataRecord from '../helpers/isDataRecord.js';
2
2
  import { resolveChannel, resolveProp } from '../helpers/resolve.js';
3
3
  import { stack, stackOffsetExpand, stackOffsetSilhouette, stackOffsetWiggle, stackOrderAppearance, stackOrderAscending, stackOrderInsideOut, stackOrderNone, stackOffsetDiverging } from 'd3-shape';
4
- import { index, union, sum, groups as d3Groups, extent, min } from 'd3-array';
4
+ 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';
9
+ import { indexData, RAW_VALUE } from './recordize.js';
10
+ const S = {
11
+ x: Symbol('x'),
12
+ x1: Symbol('x1'),
13
+ x2: Symbol('x2'),
14
+ y: Symbol('y'),
15
+ y1: Symbol('y1'),
16
+ y2: Symbol('y2')
17
+ };
8
18
  const GROUP = Symbol('group');
9
- const FACET = Symbol('group');
19
+ const FACET = Symbol('facet');
10
20
  const DEFAULT_STACK_OPTIONS = {
11
21
  order: null,
12
22
  offset: null,
@@ -41,16 +51,16 @@ function stackXY(byDim, data, channels, options) {
41
51
  channels[`${byLow}`] === undefined &&
42
52
  channels[`${byHigh}`] === undefined) {
43
53
  // resolve all channels for easier computation below
44
- const resolvedData = data.map((d) => ({
45
- ...(isDataRecord(d) ? d : { __orig: d }),
46
- [`__${secondDim}`]: resolveChannel(secondDim, d, channels),
54
+ const resolvedData = indexData(data).map((d, i) => ({
55
+ ...(isDataRecord(d) ? d : { [RAW_VALUE]: d }),
56
+ [S[secondDim]]: resolveChannel(secondDim, d, channels),
47
57
  [GROUP]: groupBy === true ? 'G' : resolveChannel(groupBy, d, channels),
48
58
  [FACET]: groupFacetsBy.length > 0
49
59
  ? groupFacetsBy
50
60
  .map((channel) => String(resolveChannel(channel, d, channels)))
51
61
  .join('---')
52
62
  : 'F',
53
- [`__${byDim}`]: resolveChannel(byDim, d, channels)
63
+ [S[byDim]]: resolveChannel(byDim, d, channels)
54
64
  }));
55
65
  // the final data ends up here
56
66
  const out = [];
@@ -58,9 +68,53 @@ function stackXY(byDim, data, channels, options) {
58
68
  // in separate panels
59
69
  const groups = d3Groups(resolvedData, (d) => d[FACET]);
60
70
  for (const [, facetData] of groups) {
61
- // now we index the data on the second dimension, e.g. over x
62
- // when stacking over y
63
- const indexed = index(facetData, (d) => d[`__${secondDim}`], (d) => d[GROUP]);
71
+ // create a temporary dataset for stacking
72
+ // If we have a grouping channel (fill/stroke/z), build objects keyed by group value
73
+ // so that series identities remain consistent across the secondary dimension.
74
+ // This is required for offsets like 'wiggle' and 'inside-out'.
75
+ let keys;
76
+ const groupedBySecondDim = d3Groups(facetData, (d) => d[S[secondDim]]);
77
+ let stackData;
78
+ const hasUniqueGroups = groupBy !== true &&
79
+ groupedBySecondDim.every(([, items]) => {
80
+ const groupSet = new Set(items.map((d) => d[GROUP]));
81
+ return groupSet.size === items.length;
82
+ });
83
+ if (groupBy === true || !hasUniqueGroups) {
84
+ // Unit stacking: map each secondary-dimension bucket to an array of values.
85
+ // Series are positional (0..N-1) within each bucket.
86
+ let maxKeys = 0;
87
+ stackData = groupedBySecondDim.map(([k, items]) => {
88
+ const values = items
89
+ // keep original order within bucket; no stable series identity across buckets
90
+ .map((d) => ({ i: d[INDEX], v: d[S[byDim]] }));
91
+ if (values.length > maxKeys)
92
+ maxKeys = values.length;
93
+ return values;
94
+ });
95
+ keys = range(maxKeys);
96
+ }
97
+ else {
98
+ // Grouped stacking: keep consistent series identities using the group key
99
+ const keySet = new Set(facetData.map((d) => d[GROUP]));
100
+ stackData = groupedBySecondDim.map(([k, items]) => {
101
+ const obj = {};
102
+ items.forEach((d) => {
103
+ const key = d[GROUP];
104
+ // If duplicates exist for the same (secondDim, group) pair, sum values
105
+ // and keep the latest index for back-reference.
106
+ if (obj[key] == null)
107
+ obj[key] = { i: d[INDEX], v: d[S[byDim]] };
108
+ else
109
+ obj[key] = {
110
+ i: d[INDEX],
111
+ v: obj[key].v + d[S[byDim]]
112
+ };
113
+ });
114
+ return obj;
115
+ });
116
+ keys = Array.from(keySet);
117
+ }
64
118
  const stackOrder = (series) => {
65
119
  const f = STACK_ORDER[options.order || 'none'];
66
120
  return options.reverse ? f(series).reverse() : f(series);
@@ -68,35 +122,31 @@ function stackXY(byDim, data, channels, options) {
68
122
  // now stack the values for each index
69
123
  const series = stack()
70
124
  .order(stackOrder)
71
- .offset(STACK_OFFSET[options.offset])
72
- .keys(union(facetData.map((d) => d[GROUP])))
73
- .value(([, group], key) => (group.get(key) ? group.get(key)[`__${byDim}`] : 0))(indexed);
125
+ // Wiggle requires consistent series identities; fall back to 'center' for unit stacking
126
+ .offset(groupBy === true && options.offset === 'wiggle'
127
+ ? STACK_OFFSET['center']
128
+ : STACK_OFFSET[options.offset])
129
+ .keys(keys)
130
+ .value((d, key, i, data) => {
131
+ return d[key]?.v;
132
+ })(stackData);
74
133
  // and combine it all back into a flat array
75
134
  const newData = series
76
- .map((values) => {
77
- const groupKey = values.key;
78
- return values
79
- .filter((d) => d.data[1].get(groupKey))
80
- .map((d) => {
81
- const datum = d.data[1].get(groupKey);
82
- // cleanup our internal keys
83
- delete datum[GROUP];
84
- delete datum[FACET];
85
- return { ...datum, [`__${byLow}`]: d[0], [`__${byHigh}`]: d[1] };
86
- });
87
- })
88
- .flat(1);
135
+ .flatMap((s) => s.map((d) => [d[0], d[1], d.data[s.key]?.i]))
136
+ .filter((d) => d[2] !== undefined)
137
+ .map((d) => ({ [S[byLow]]: d[0], [S[byHigh]]: d[1], ...resolvedData[d[2]] }));
138
+ out.push(...newData);
89
139
  // which we then add to the output data
90
- out.push(newData);
140
+ // out.push(...newData);
91
141
  }
92
142
  return {
93
- data: out.flat(1),
143
+ data: out,
94
144
  ...channels,
95
145
  [byDim]: undefined,
96
146
  ...(typeof channels[byDim] === 'string' && !channels[`__${byDim}_origField`]
97
147
  ? { [`__${byDim}_origField`]: channels[byDim] }
98
148
  : {}),
99
- ...{ [byLow]: `__${byLow}`, [byHigh]: `__${byHigh}` }
149
+ ...{ [byLow]: S[byLow], [byHigh]: S[byHigh] }
100
150
  };
101
151
  }
102
152
  return { data, ...channels };
@@ -113,12 +163,6 @@ function applyDefaults(opts) {
113
163
  }
114
164
  return { ...DEFAULT_STACK_OPTIONS, ...opts };
115
165
  }
116
- const X = Symbol('x');
117
- const X1 = Symbol('x1');
118
- const X2 = Symbol('x2');
119
- const Y = Symbol('y');
120
- const Y1 = Symbol('y1');
121
- const Y2 = Symbol('y2');
122
166
  function stackMosaic({ data, x, y, value, fx, fy, ...rest }, { outer, inner }, { x: xOpt, y: yOpt } = {}) {
123
167
  const out = [];
124
168
  const { data: filtered, ...restArgs } = sort(filter({ data, x, y, value, fx, fy, ...rest }));
@@ -137,10 +181,10 @@ function stackMosaic({ data, x, y, value, fx, fy, ...rest }, { outer, inner }, {
137
181
  let outerPos = 0;
138
182
  const outerChannel = outer === 'x' ? x : y;
139
183
  const innerChannel = inner === 'x' ? x : y;
140
- const outerSym1 = outer === 'x' ? X1 : Y1;
141
- const outerSym2 = outer === 'x' ? X2 : Y2;
142
- const innerSym1 = inner === 'x' ? X1 : Y1;
143
- const innerSym2 = inner === 'x' ? X2 : Y2;
184
+ const outerSym1 = outer === 'x' ? S.x1 : S.y1;
185
+ const outerSym2 = outer === 'x' ? S.x2 : S.y2;
186
+ const innerSym1 = inner === 'x' ? S.x1 : S.y1;
187
+ const innerSym2 = inner === 'x' ? S.x2 : S.y2;
144
188
  const outerOpt = outer === 'x' ? xOpt : yOpt;
145
189
  const innerOpt = inner === 'x' ? xOpt : yOpt;
146
190
  const grouped = d3Groups(data, (d) => resolveProp(d[outerChannel], d));
@@ -165,13 +209,24 @@ function stackMosaic({ data, x, y, value, fx, fy, ...rest }, { outer, inner }, {
165
209
  result[outerSym2] = normO2;
166
210
  result[innerSym1] = normI1;
167
211
  result[innerSym2] = normI2;
168
- result[X] = (result[X1] + result[X2]) / 2;
169
- result[Y] = (result[Y1] + result[Y2]) / 2;
212
+ result[S.x] = (result[S.x1] + result[S.x2]) / 2;
213
+ result[S.y] = (result[S.y1] + result[S.y2]) / 2;
170
214
  out.push(result);
171
215
  });
172
216
  });
173
217
  });
174
- return { ...rest, fx, fy, data: out, x: X, x1: X1, x2: X2, y: Y, y1: Y1, y2: Y2 };
218
+ return {
219
+ ...rest,
220
+ fx,
221
+ fy,
222
+ data: out,
223
+ x: S.x,
224
+ x1: S.x1,
225
+ x2: S.x2,
226
+ y: S.y,
227
+ y1: S.y1,
228
+ y2: S.y2
229
+ };
175
230
  }
176
231
  export function stackMosaicX(args, opts) {
177
232
  return stackMosaic(args, { outer: 'x', inner: 'y' }, opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.4.4-pr-205.0",
3
+ "version": "0.4.4-pr-105.1",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",