layerchart 2.0.0-next.32 → 2.0.0-next.34

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.
@@ -125,8 +125,17 @@
125
125
 
126
126
  import { extent } from 'd3-array';
127
127
  import { pointRadial } from 'd3-shape';
128
-
129
- import { type FormatType, type FormatConfig } from '@layerstack/utils';
128
+ import {
129
+ timeDay,
130
+ timeHour,
131
+ timeMillisecond,
132
+ timeMinute,
133
+ timeMonth,
134
+ timeSecond,
135
+ timeYear,
136
+ } from 'd3-time';
137
+
138
+ import { type FormatType, type FormatConfig, unique, PeriodType } from '@layerstack/utils';
130
139
  import { cls } from '@layerstack/tailwind';
131
140
 
132
141
  import Group, { type GroupProps } from './Group.svelte';
@@ -138,7 +147,7 @@
138
147
  import { getChartContext } from './Chart.svelte';
139
148
  import { extractLayerProps, layerClass } from '../utils/attributes.js';
140
149
  import { type MotionProp } from '../utils/motion.svelte.js';
141
- import { resolveTickFormat, resolveTickVals, type TicksConfig } from '../utils/ticks.js';
150
+ import { autoTickVals, autoTickFormat, type TicksConfig } from '../utils/ticks.js';
142
151
 
143
152
  let {
144
153
  placement,
@@ -209,9 +218,46 @@
209
218
  ? Math.round(ctxSize / tickSpacing)
210
219
  : undefined
211
220
  );
212
- const tickVals = $derived(resolveTickVals(scale, ticks, tickCount, interval));
221
+ const tickVals = $derived.by(() => {
222
+ let tickVals = autoTickVals(scale, ticks, tickCount);
223
+
224
+ if (interval != null) {
225
+ // Remove last tick when interval is provided (such as for bar charts with center aligned (offset) ticks)
226
+ tickVals.pop();
227
+ }
228
+
229
+ // Use format to filter ticks (helpful to keep ticks above a threshold for wide charts or short durations)
230
+ const formatType = typeof format === 'object' ? format?.type : format;
231
+
232
+ if (formatType === 'integer') {
233
+ tickVals = tickVals.filter(Number.isInteger);
234
+ } else if (formatType === 'year' || formatType === PeriodType.CalendarYear) {
235
+ tickVals = tickVals.filter((val) => +timeYear.floor(val) === +val);
236
+ } else if (
237
+ formatType === 'month' ||
238
+ formatType === PeriodType.Month ||
239
+ formatType === PeriodType.MonthYear
240
+ ) {
241
+ // tickVals = tickVals.filter((val) => +timeMonth.floor(val) === +val);
242
+ tickVals = tickVals.filter((val) => val.getDate() < 7); // first week of the month
243
+ } else if (formatType === 'day' || formatType === PeriodType.Day) {
244
+ tickVals = tickVals.filter((val) => +timeDay.floor(val) === +val);
245
+ } else if (formatType === 'hour' || formatType === PeriodType.Hour) {
246
+ tickVals = tickVals.filter((val) => +timeHour.floor(val) === +val);
247
+ } else if (formatType === 'minute' || formatType === PeriodType.Minute) {
248
+ tickVals = tickVals.filter((val) => +timeMinute.floor(val) === +val);
249
+ } else if (formatType === 'second' || formatType === PeriodType.Second) {
250
+ tickVals = tickVals.filter((val) => +timeSecond.floor(val) === +val);
251
+ } else if (formatType === 'millisecond' || formatType === PeriodType.Millisecond) {
252
+ tickVals = tickVals.filter((val) => +timeMillisecond.floor(val) === +val);
253
+ }
254
+
255
+ // Remove any duplicates (manually added)
256
+ return unique(tickVals);
257
+ });
258
+
213
259
  const tickFormat = $derived(
214
- resolveTickFormat({
260
+ autoTickFormat({
215
261
  scale,
216
262
  ticks,
217
263
  count: tickCount,
@@ -393,8 +439,8 @@
393
439
  {#if rule !== false}
394
440
  {@const ruleProps = extractLayerProps(rule, 'axis-rule')}
395
441
  <Rule
396
- x={placement === 'left' || placement === 'right' ? placement : placement === 'angle'}
397
- y={placement === 'top' || placement === 'bottom' ? placement : placement === 'radius'}
442
+ x={placement === 'left' ? '$left' : placement === 'right' ? '$right' : placement === 'angle'}
443
+ y={placement === 'top' ? '$top' : placement === 'bottom' ? '$bottom' : placement === 'radius'}
398
444
  {motion}
399
445
  {...ruleProps}
400
446
  class={cls('stroke-surface-content/50', classes.rule, ruleProps?.class)}
@@ -136,10 +136,10 @@
136
136
  const rounded = $derived(
137
137
  roundedProp === 'edge'
138
138
  ? isVertical
139
- ? resolvedValue >= 0
139
+ ? resolvedValue >= 0 && ctx.yRange[0] > ctx.yRange[1] // not inverted (bottom to top)
140
140
  ? 'top'
141
141
  : 'bottom'
142
- : resolvedValue >= 0
142
+ : resolvedValue >= 0 && ctx.xRange[0] < ctx.xRange[1] // not inverted (left to right)
143
143
  ? 'right'
144
144
  : 'left'
145
145
  : roundedProp
@@ -32,8 +32,8 @@
32
32
  import Bar, { type BarProps, type BarPropsWithoutHTML } from './Bar.svelte';
33
33
  import Group from './Group.svelte';
34
34
 
35
- import { chartDataArray } from '../utils/common.js';
36
35
  import { getChartContext } from './Chart.svelte';
36
+ import { chartDataArray } from '../utils/common.js';
37
37
  import { extractLayerProps, layerClass } from '../utils/attributes.js';
38
38
 
39
39
  let {
@@ -99,7 +99,7 @@
99
99
  import Spline from './Spline.svelte';
100
100
  import { getChartContext } from './Chart.svelte';
101
101
  import { extractLayerProps, layerClass } from '../utils/attributes.js';
102
- import { resolveTickVals, type TicksConfig } from '../utils/ticks.js';
102
+ import { autoTickVals, type TicksConfig } from '../utils/ticks.js';
103
103
 
104
104
  const ctx = getChartContext();
105
105
 
@@ -131,8 +131,8 @@
131
131
 
132
132
  const transitionIn = $derived((transitionInProp ?? tweenConfig?.options) ? fade : () => ({}));
133
133
 
134
- const xTickVals = $derived(resolveTickVals(ctx.xScale, xTicks));
135
- const yTickVals = $derived(resolveTickVals(ctx.yScale, yTicks));
134
+ const xTickVals = $derived(autoTickVals(ctx.xScale, xTicks));
135
+ const yTickVals = $derived(autoTickVals(ctx.yScale, yTicks));
136
136
 
137
137
  const xBandOffset = $derived(
138
138
  isScaleBand(ctx.xScale)
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" module>
2
2
  import type { CommonStyleProps, Without } from '../utils/types.js';
3
- import type { ComponentProps, Snippet } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
4
 
5
5
  export type Point = { x: number; y: number; r: number; xValue: any; yValue: any; data: any };
6
6
  type Offset = number | ((value: number, context: any) => number) | undefined;
@@ -37,13 +37,6 @@
37
37
  */
38
38
  offsetY?: Offset;
39
39
 
40
- /**
41
- * Enable showing links between related points (array x/y accessors)
42
- *
43
- * @default false
44
- */
45
- links?: boolean | Partial<ComponentProps<typeof Link>>;
46
-
47
40
  children?: Snippet<[{ points: Point[] }]>;
48
41
  } & CommonStyleProps;
49
42
 
@@ -71,7 +64,6 @@
71
64
  r = 5,
72
65
  offsetX,
73
66
  offsetY,
74
- links = false,
75
67
  fill,
76
68
  fillOpacity,
77
69
  stroke,
@@ -136,66 +128,11 @@
136
128
  return [];
137
129
  }) as Point[]
138
130
  );
139
-
140
- const _links = $derived(
141
- pointsData.flatMap((d: any) => {
142
- const xValue = xAccessor(d);
143
- const yValue = yAccessor(d);
144
-
145
- if (Array.isArray(xValue)) {
146
- /*
147
- x={["prop1" ,"prop2"]}
148
- y="prop3"
149
- */
150
- const [xMin, xMax] = extent(ctx.xGet(d)) as unknown as [number, number];
151
- const y = ctx.yGet(d) + getOffset(ctx.yGet(d), offsetY, ctx.yScale);
152
- return {
153
- source: {
154
- x: xMin + getOffset(xMin, offsetX, ctx.xScale) + (ctx.config.r ? ctx.rGet(d) : r),
155
- y,
156
- },
157
- target: {
158
- x: xMax + getOffset(xMax, offsetX, ctx.xScale) - (ctx.config.r ? ctx.rGet(d) : r),
159
- y: y,
160
- },
161
- data: d,
162
- };
163
- } else if (Array.isArray(yValue)) {
164
- /*
165
- x="prop1"
166
- y={["prop2" ,"prop3"]}
167
- */
168
- const x = ctx.xGet(d) + getOffset(ctx.xGet(d), offsetX, ctx.xScale);
169
- const [yMin, yMax] = extent(ctx.yGet(d)) as unknown as [number, number];
170
- return {
171
- source: {
172
- x: x,
173
- y: yMin + getOffset(yMin, offsetY, ctx.yScale),
174
- },
175
- target: {
176
- x: x,
177
- y: yMax + getOffset(yMax, offsetY, ctx.yScale),
178
- },
179
- data: d,
180
- };
181
- }
182
- })
183
- );
184
131
  </script>
185
132
 
186
133
  {#if children}
187
134
  {@render children({ points })}
188
135
  {:else}
189
- {#if links}
190
- {#each _links as link}
191
- <Link
192
- data={link}
193
- stroke={fill ?? (ctx.config.c ? ctx.cGet(link.data) : null)}
194
- {...extractLayerProps(links, 'points-link')}
195
- />
196
- {/each}
197
- {/if}
198
-
199
136
  {#each points as point}
200
137
  <Circle
201
138
  cx={point.x}
@@ -1,5 +1,5 @@
1
1
  import type { CommonStyleProps, Without } from '../utils/types.js';
2
- import type { ComponentProps, Snippet } from 'svelte';
2
+ import type { Snippet } from 'svelte';
3
3
  export type Point = {
4
4
  x: number;
5
5
  y: number;
@@ -36,19 +36,12 @@ export type PointsPropsWithoutHTML = {
36
36
  * The offset of the point in the y direction
37
37
  */
38
38
  offsetY?: Offset;
39
- /**
40
- * Enable showing links between related points (array x/y accessors)
41
- *
42
- * @default false
43
- */
44
- links?: boolean | Partial<ComponentProps<typeof Link>>;
45
39
  children?: Snippet<[{
46
40
  points: Point[];
47
41
  }]>;
48
42
  } & CommonStyleProps;
49
43
  export type PointsProps = PointsPropsWithoutHTML & Omit<Without<CircleProps, PointsPropsWithoutHTML>, 'ref'>;
50
44
  import { type CircleProps } from './Circle.svelte';
51
- import Link from './Link.svelte';
52
45
  import { type Accessor } from '../utils/common.js';
53
46
  declare const Points: import("svelte").Component<PointsProps, {}, "">;
54
47
  type Points = ReturnType<typeof Points>;
@@ -3,6 +3,11 @@
3
3
  import type { SVGAttributes } from 'svelte/elements';
4
4
 
5
5
  export type BaseRulePropsWithoutHTML = {
6
+ /**
7
+ * Override the data from the context.
8
+ */
9
+ data?: any;
10
+
6
11
  /**
7
12
  * Create a vertical `x` line
8
13
  * - If true or 'left', will draw at chart left (xRange[0])
@@ -12,7 +17,7 @@
12
17
  *
13
18
  * @default false
14
19
  */
15
- x?: number | Date | boolean | 'left' | 'right';
20
+ x?: number | Date | boolean | '$left' | '$right' | Accessor;
16
21
 
17
22
  /**
18
23
  * Pixel offset to apply to `x` coordinate
@@ -30,7 +35,7 @@
30
35
  *
31
36
  * @default false
32
37
  */
33
- y?: number | Date | boolean | 'top' | 'bottom';
38
+ y?: number | Date | boolean | '$top' | '$bottom' | Accessor;
34
39
 
35
40
  /**
36
41
  * Pixel offset to apply to `y` coordinate
@@ -55,13 +60,17 @@
55
60
  import Group from './Group.svelte';
56
61
  import Line, { type LinePropsWithoutHTML } from './Line.svelte';
57
62
  import { getChartContext } from './Chart.svelte';
63
+ import { accessor, chartDataArray, type Accessor } from '../utils/common.js';
58
64
  import { layerClass } from '../utils/attributes.js';
65
+ import { isScaleBand, isScaleNumeric } from '../utils/scales.svelte.js';
59
66
 
60
67
  let {
68
+ data: dataProp,
61
69
  x = false,
62
70
  xOffset = 0,
63
71
  y = false,
64
72
  yOffset = 0,
73
+ stroke: strokeProp,
65
74
  class: className,
66
75
  children,
67
76
  ...restProps
@@ -69,89 +78,163 @@
69
78
 
70
79
  const ctx = getChartContext();
71
80
 
72
- const xRangeMinMax = $derived(extent<number | Date>(ctx.xRange));
73
- const yRangeMinMax = $derived(extent<number | Date>(ctx.yRange));
74
-
75
- function showRule(value: typeof x | typeof y, axis: 'x' | 'y') {
76
- switch (typeof value) {
77
- case 'boolean':
78
- return value;
79
- case 'string':
80
- return true;
81
- default:
82
- if (axis === 'x') {
83
- return ctx.xScale(value) >= xRangeMinMax[0]! && ctx.xScale(value) <= xRangeMinMax[1]!;
84
- } else {
85
- return ctx.yScale(value) >= yRangeMinMax[0]! && ctx.yScale(value) <= yRangeMinMax[1]!;
86
- }
81
+ const data = $derived(chartDataArray(dataProp ?? ctx.data));
82
+
83
+ const singleX = $derived(
84
+ typeof x === 'number' ||
85
+ x instanceof Date ||
86
+ x === true ||
87
+ x === '$left' ||
88
+ x === '$right' ||
89
+ (isScaleBand(ctx.xScale) && ctx.xDomain.includes(x as any))
90
+ );
91
+ const singleY = $derived(
92
+ typeof y === 'number' ||
93
+ y instanceof Date ||
94
+ y === true ||
95
+ y === '$bottom' ||
96
+ y === '$top' ||
97
+ (isScaleBand(ctx.yScale) && ctx.yDomain.includes(y as any))
98
+ );
99
+
100
+ const xRangeMinMax = $derived(extent<number>(ctx.xRange));
101
+ const yRangeMinMax = $derived(extent<number>(ctx.yRange));
102
+
103
+ const lines = $derived.by(() => {
104
+ const result: {
105
+ x1: number;
106
+ y1: number;
107
+ x2: number;
108
+ y2: number;
109
+ axis: 'x' | 'y';
110
+ stroke?: string;
111
+ }[] = [];
112
+
113
+ // Single x line
114
+ if (singleX) {
115
+ const _x =
116
+ x === true || x === '$left'
117
+ ? xRangeMinMax[0]!
118
+ : x === '$right'
119
+ ? xRangeMinMax[1]!
120
+ : ctx.xScale(x) + xOffset;
121
+
122
+ result.push({
123
+ x1: _x,
124
+ y1: ctx.yRange[0] || 0,
125
+ x2: _x,
126
+ y2: ctx.yRange[1] || 0,
127
+ axis: 'x',
128
+ });
129
+ }
130
+
131
+ // Single y line
132
+ if (singleY) {
133
+ const _y =
134
+ y === true || y === '$bottom'
135
+ ? yRangeMinMax[1]!
136
+ : y === '$top'
137
+ ? yRangeMinMax[0]!
138
+ : ctx.yScale(y) + yOffset;
139
+
140
+ result.push({
141
+ x1: ctx.xRange[0] || 0,
142
+ y1: _y,
143
+ x2: ctx.xRange[1] || 0,
144
+ y2: _y,
145
+ axis: 'y',
146
+ });
87
147
  }
88
- }
148
+
149
+ // Data driven lines
150
+ if (!singleX && !singleY) {
151
+ const xAccessor = x !== false ? accessor(x as Accessor) : ctx.x;
152
+ const yAccessor = y !== false ? accessor(y as Accessor) : ctx.y;
153
+
154
+ const xBandOffset = isScaleBand(ctx.xScale) ? ctx.xScale.bandwidth() / 2 : 0;
155
+ const yBandOffset = isScaleBand(ctx.yScale) ? ctx.yScale.bandwidth() / 2 : 0;
156
+
157
+ for (const d of data) {
158
+ const xValue = xAccessor(d);
159
+ const yValue = yAccessor(d);
160
+
161
+ const x1Value = Array.isArray(xValue) ? xValue[0] : isScaleNumeric(ctx.xScale) ? 0 : xValue;
162
+ const x2Value = Array.isArray(xValue) ? xValue[1] : xValue;
163
+ const y1Value = Array.isArray(yValue) ? yValue[0] : isScaleNumeric(ctx.yScale) ? 0 : yValue;
164
+ const y2Value = Array.isArray(yValue) ? yValue[1] : yValue;
165
+
166
+ result.push({
167
+ x1: ctx.xScale(x1Value) + xBandOffset + xOffset,
168
+ y1: ctx.yScale(y1Value) + yBandOffset + yOffset,
169
+ x2: ctx.xScale(x2Value) + xBandOffset + xOffset,
170
+ y2: ctx.yScale(y2Value) + yBandOffset + yOffset,
171
+ axis: Array.isArray(yValue) || isScaleBand(ctx.xScale) ? 'x' : 'y', // TODO: what about single prop like lollipop?
172
+ stroke: (strokeProp ?? ctx.config.c) ? ctx.cGet(d) : null, // use color scale, if available
173
+ });
174
+ }
175
+ }
176
+
177
+ // Remove lines if out of range of chart (non-0 baseline, brushing, etc)
178
+ return result.filter((line) => {
179
+ return (
180
+ line.x1 >= xRangeMinMax[0]! &&
181
+ line.x2 <= xRangeMinMax[1]! &&
182
+ line.y1 >= yRangeMinMax[0]! &&
183
+ line.y2 <= yRangeMinMax[1]!
184
+ );
185
+ });
186
+ });
187
+
188
+ // $inspect({ lines });
89
189
  </script>
90
190
 
91
191
  <Group class={layerClass('rule-g')}>
92
- {#if showRule(x, 'x')}
93
- {@const xCoord =
94
- x === true || x === 'left'
95
- ? xRangeMinMax[0]
96
- : x === 'right'
97
- ? xRangeMinMax[1]
98
- : ctx.xScale(x) + xOffset}
192
+ {#each lines as line}
193
+ {@const stroke = line.stroke}
99
194
 
100
195
  {#if ctx.radial}
101
- {@const [x1, y1] = pointRadial(xCoord, Number(yRangeMinMax[0]))}
102
- {@const [x2, y2] = pointRadial(xCoord, Number(yRangeMinMax[1]))}
103
-
104
- <Line
105
- {...restProps}
106
- {x1}
107
- {y1}
108
- {x2}
109
- {y2}
110
- class={cls(layerClass('rule-x-radial-line'), 'stroke-surface-content/10', className)}
111
- />
196
+ {#if line.axis === 'x'}
197
+ {@const [x1, y1] = pointRadial(line.x1, line.y1)}
198
+ {@const [x2, y2] = pointRadial(line.x2, line.y2)}
199
+ <Line
200
+ {...restProps}
201
+ {x1}
202
+ {y1}
203
+ {x2}
204
+ {y2}
205
+ {stroke}
206
+ class={cls(
207
+ layerClass('rule-x-radial-line'),
208
+ !stroke && 'stroke-surface-content/10',
209
+ className
210
+ )}
211
+ />
212
+ {:else if line.axis === 'y'}
213
+ <Circle
214
+ r={line.y1}
215
+ {stroke}
216
+ class={cls(
217
+ layerClass('rule-y-radial-circle'),
218
+ !stroke && 'stroke-surface-content/50',
219
+ 'fill-none',
220
+ className
221
+ )}
222
+ />
223
+ {/if}
112
224
  {:else}
113
225
  <Line
114
226
  {...restProps}
115
- x1={xCoord}
116
- x2={xCoord}
117
- y1={ctx.yRange[0] || 0}
118
- y2={ctx.yRange[1] || 0}
119
- class={cls(layerClass('rule-x-line'), 'stroke-surface-content/50', className)}
120
- />
121
- {/if}
122
- {/if}
123
-
124
- {#if showRule(y, 'y')}
125
- {#if ctx.radial}
126
- <Circle
127
- r={y === true || y === 'bottom'
128
- ? yRangeMinMax[1]
129
- : y === 'top'
130
- ? yRangeMinMax[0]
131
- : ctx.yScale(y) + yOffset}
227
+ x1={line.x1}
228
+ y1={line.y1}
229
+ x2={line.x2}
230
+ y2={line.y2}
231
+ {stroke}
132
232
  class={cls(
133
- layerClass('rule-y-radial-circle'),
134
- 'fill-none stroke-surface-content/50',
233
+ layerClass(line.axis === 'x' ? 'rule-x-line' : 'rule-y-line'),
234
+ !stroke && 'stroke-surface-content/50',
135
235
  className
136
236
  )}
137
237
  />
138
- {:else}
139
- <Line
140
- {...restProps}
141
- x1={ctx.xRange[0] || 0}
142
- x2={ctx.xRange[1] || 0}
143
- y1={y === true || y === 'bottom'
144
- ? yRangeMinMax[1]
145
- : y === 'top'
146
- ? yRangeMinMax[0]
147
- : ctx.yScale(y) + yOffset}
148
- y2={y === true || y === 'bottom'
149
- ? yRangeMinMax[1]
150
- : y === 'top'
151
- ? yRangeMinMax[0]
152
- : ctx.yScale(y) + yOffset}
153
- class={cls(layerClass('rule-y-line'), 'stroke-surface-content/50', className)}
154
- />
155
238
  {/if}
156
- {/if}
239
+ {/each}
157
240
  </Group>
@@ -1,6 +1,10 @@
1
1
  import type { Without } from '../utils/types.js';
2
2
  import type { SVGAttributes } from 'svelte/elements';
3
3
  export type BaseRulePropsWithoutHTML = {
4
+ /**
5
+ * Override the data from the context.
6
+ */
7
+ data?: any;
4
8
  /**
5
9
  * Create a vertical `x` line
6
10
  * - If true or 'left', will draw at chart left (xRange[0])
@@ -10,7 +14,7 @@ export type BaseRulePropsWithoutHTML = {
10
14
  *
11
15
  * @default false
12
16
  */
13
- x?: number | Date | boolean | 'left' | 'right';
17
+ x?: number | Date | boolean | '$left' | '$right' | Accessor;
14
18
  /**
15
19
  * Pixel offset to apply to `x` coordinate
16
20
  *
@@ -26,7 +30,7 @@ export type BaseRulePropsWithoutHTML = {
26
30
  *
27
31
  * @default false
28
32
  */
29
- y?: number | Date | boolean | 'top' | 'bottom';
33
+ y?: number | Date | boolean | '$top' | '$bottom' | Accessor;
30
34
  /**
31
35
  * Pixel offset to apply to `y` coordinate
32
36
  * @default 0
@@ -36,6 +40,7 @@ export type BaseRulePropsWithoutHTML = {
36
40
  export type RulePropsWithoutHTML = BaseRulePropsWithoutHTML & Without<Partial<LinePropsWithoutHTML>, BaseRulePropsWithoutHTML>;
37
41
  export type RuleProps = RulePropsWithoutHTML & Without<SVGAttributes<SVGElement>, RulePropsWithoutHTML>;
38
42
  import { type LinePropsWithoutHTML } from './Line.svelte';
43
+ import { type Accessor } from '../utils/common.js';
39
44
  declare const Rule: import("svelte").Component<RuleProps, {}, "">;
40
45
  type Rule = ReturnType<typeof Rule>;
41
46
  export default Rule;
@@ -23,21 +23,21 @@ export declare function createSeries<TKey extends string>(options: {
23
23
  }): ({
24
24
  x: number;
25
25
  } & { [K in TKey]: number; })[];
26
- export declare function createDateSeries<TKey extends string>(options: {
26
+ export declare function createDateSeries<TKey extends string>(options?: {
27
27
  count?: number;
28
- min: number;
29
- max: number;
28
+ min?: number;
29
+ max?: number;
30
30
  keys?: TKey[];
31
31
  value?: 'number' | 'integer';
32
32
  }): ({
33
33
  date: Date;
34
34
  } & { [K in TKey]: number; })[];
35
- export declare function createTimeSeries<TKey extends string>(options: {
35
+ export declare function createTimeSeries<TKey extends string>(options?: {
36
36
  count?: number;
37
- min: number;
38
- max: number;
39
- keys: TKey[];
40
- value: 'number' | 'integer';
37
+ min?: number;
38
+ max?: number;
39
+ keys?: TKey[];
40
+ value?: 'number' | 'integer';
41
41
  }): ({
42
42
  name: string;
43
43
  startDate: Date;
@@ -43,29 +43,31 @@ export function createSeries(options) {
43
43
  };
44
44
  });
45
45
  }
46
- export function createDateSeries(options) {
46
+ export function createDateSeries(options = {}) {
47
47
  const now = timeDay.floor(new Date());
48
48
  const count = options.count ?? 10;
49
- const min = options.min;
50
- const max = options.max;
49
+ const min = options.min ?? 0;
50
+ const max = options.max ?? 100;
51
51
  const keys = options.keys ?? ['value'];
52
+ const valueType = options.value ?? 'number';
52
53
  return Array.from({ length: count }).map((_, i) => {
53
54
  return {
54
55
  date: timeDay.offset(now, -count + i),
55
56
  ...Object.fromEntries(keys.map((key) => {
56
57
  return [
57
58
  key,
58
- options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max),
59
+ valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max),
59
60
  ];
60
61
  })),
61
62
  };
62
63
  });
63
64
  }
64
- export function createTimeSeries(options) {
65
+ export function createTimeSeries(options = {}) {
65
66
  const count = options.count ?? 10;
66
- const min = options.min;
67
- const max = options.max;
67
+ const min = options.min ?? 0;
68
+ const max = options.max ?? 100;
68
69
  const keys = options.keys ?? ['value'];
70
+ const valueType = options.value ?? 'number';
69
71
  let lastStartDate = timeDay.floor(new Date());
70
72
  const timeSeries = Array.from({ length: count }).map((_, i) => {
71
73
  const startDate = timeMinute.offset(lastStartDate, getRandomInteger(0, 60));
@@ -78,7 +80,7 @@ export function createTimeSeries(options) {
78
80
  ...Object.fromEntries(keys.map((key) => {
79
81
  return [
80
82
  key,
81
- options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max),
83
+ valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max),
82
84
  ];
83
85
  })),
84
86
  };
@@ -90,6 +90,10 @@ export function createDimensionGetter(ctx, getOptions) {
90
90
  top = min([0, yDomainMinMax[1]]);
91
91
  bottom = yValue;
92
92
  }
93
+ // If yRange is inverted (drawing from top), swap top and bottom
94
+ if (ctx.yRange[0] < ctx.yRange[1]) {
95
+ [top, bottom] = [bottom, top];
96
+ }
93
97
  const y = ctx.yScale(top) + insets.top;
94
98
  const height = ctx.yScale(bottom) - ctx.yScale(top) - insets.bottom - insets.top;
95
99
  return { x, y, width, height };
@@ -25,6 +25,7 @@ export type AnyScale<TInput extends SingleDomainType = any, TOutput extends Sing
25
25
  };
26
26
  export declare function isScaleBand(scale: AnyScale<any, any>): scale is ScaleBand<any>;
27
27
  export declare function isScaleTime(scale: AnyScale<any, any>): scale is ScaleTime<any, any>;
28
+ export declare function isScaleNumeric(scale: AnyScale<any, any>): scale is ScaleTime<any, any>;
28
29
  export declare function getRange(scale: any): any[];
29
30
  export type SingleDomainType = number | string | Date | null | undefined;
30
31
  export type DomainType = (number | string | Date | null | undefined)[] | null | undefined;
@@ -12,6 +12,10 @@ export function isScaleTime(scale) {
12
12
  const domain = scale.domain();
13
13
  return domain[0] instanceof Date || domain[1] instanceof Date;
14
14
  }
15
+ export function isScaleNumeric(scale) {
16
+ const domain = scale.domain();
17
+ return typeof domain[0] === 'number' || typeof domain[1] === 'number';
18
+ }
15
19
  export function getRange(scale) {
16
20
  if (isAnyScale(scale)) {
17
21
  return scale.range();
@@ -9,8 +9,8 @@ export declare function getDurationFormat(duration: Duration, options?: {
9
9
  export type TicksConfig = number | any[] | ((scale: AnyScale) => any[] | undefined) | {
10
10
  interval: TimeInterval | null;
11
11
  } | null;
12
- export declare function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number, interval?: TimeInterval | null): any[];
13
- export declare function resolveTickFormat(options: {
12
+ export declare function autoTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[];
13
+ export declare function autoTickFormat(options: {
14
14
  scale: AnyScale;
15
15
  ticks?: TicksConfig;
16
16
  count?: number;
@@ -110,7 +110,7 @@ export function getDurationFormat(duration, options = {
110
110
  }
111
111
  };
112
112
  }
113
- export function resolveTickVals(scale, ticks, count, interval) {
113
+ export function autoTickVals(scale, ticks, count) {
114
114
  // Explicit ticks
115
115
  if (Array.isArray(ticks))
116
116
  return ticks;
@@ -132,16 +132,11 @@ export function resolveTickVals(scale, ticks, count, interval) {
132
132
  }
133
133
  // Ticks from scale
134
134
  if (scale.ticks && typeof scale.ticks === 'function') {
135
- const tickVals = scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined));
136
- if (interval) {
137
- // Remove last tick when interval is provided (such as for bar charts with center aligned (offset) ticks)
138
- tickVals.pop();
139
- }
140
- return tickVals;
135
+ return scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined));
141
136
  }
142
137
  return [];
143
138
  }
144
- export function resolveTickFormat(options) {
139
+ export function autoTickFormat(options) {
145
140
  const { scale, ticks, count, formatType, multiline, placement } = options;
146
141
  // Explicit format
147
142
  if (formatType) {
@@ -1,62 +1,57 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { resolveTickVals } from './ticks.js';
2
+ import { autoTickVals } from './ticks.js';
3
3
  // Mock helpers
4
4
  const mockTicksFn = vi.fn();
5
5
  const mockDomain = vi.fn(() => ['a', 'b', 'c', 'd', 'e']);
6
- describe('resolveTickVals', () => {
6
+ describe('autoTickVals', () => {
7
7
  it('returns array ticks directly', () => {
8
8
  const ticks = [1, 2, 3];
9
9
  const scale = { ticks: mockTicksFn };
10
- expect(resolveTickVals(scale, ticks)).toEqual([1, 2, 3]);
10
+ expect(autoTickVals(scale, ticks)).toEqual([1, 2, 3]);
11
11
  });
12
12
  it('calls function ticks with scale', () => {
13
13
  const fnTicks = vi.fn(() => [4, 5, 6]);
14
14
  const scale = { ticks: mockTicksFn };
15
- expect(resolveTickVals(scale, fnTicks)).toEqual([4, 5, 6]);
15
+ expect(autoTickVals(scale, fnTicks)).toEqual([4, 5, 6]);
16
16
  expect(fnTicks).toHaveBeenCalledWith(scale);
17
17
  });
18
18
  it('uses interval when provided', () => {
19
19
  const interval = { every: vi.fn() };
20
20
  const ticksConfig = { interval };
21
21
  const scale = { ticks: vi.fn(() => [7, 8, 9]) };
22
- expect(resolveTickVals(scale, ticksConfig)).toEqual([7, 8, 9]);
22
+ expect(autoTickVals(scale, ticksConfig)).toEqual([7, 8, 9]);
23
23
  expect(scale.ticks).toHaveBeenCalledWith(interval);
24
24
  });
25
25
  it('returns empty array if interval is null', () => {
26
26
  const ticksConfig = { interval: null };
27
27
  const scale = { ticks: mockTicksFn };
28
- expect(resolveTickVals(scale, ticksConfig)).toEqual([]);
28
+ expect(autoTickVals(scale, ticksConfig)).toEqual([]);
29
29
  });
30
30
  it('filters band scale domain with number ticks', () => {
31
31
  const scale = { domain: mockDomain, bandwidth: vi.fn() };
32
- expect(resolveTickVals(scale, 2)).toEqual(['a', 'c', 'e']);
32
+ expect(autoTickVals(scale, 2)).toEqual(['a', 'c', 'e']);
33
33
  });
34
34
  it('returns full domain for band scale without ticks', () => {
35
35
  const scale = { domain: mockDomain, bandwidth: vi.fn() };
36
- expect(resolveTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']);
36
+ expect(autoTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']);
37
37
  });
38
38
  it('uses undefined for non-left/right placement', () => {
39
39
  const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2]) };
40
- expect(resolveTickVals(scale, undefined, undefined)).toEqual([1, 2]);
40
+ expect(autoTickVals(scale, undefined, undefined)).toEqual([1, 2]);
41
41
  expect(scale.ticks).toHaveBeenCalledWith(undefined);
42
42
  });
43
43
  it('passes number ticks to scale.ticks', () => {
44
44
  const scale = { domain: mockDomain, ticks: vi.fn(() => [10, 20]) };
45
- expect(resolveTickVals(scale, 5)).toEqual([10, 20]);
45
+ expect(autoTickVals(scale, 5)).toEqual([10, 20]);
46
46
  expect(scale.ticks).toHaveBeenCalledWith(5);
47
47
  });
48
48
  it('returns empty array for scale without ticks', () => {
49
49
  const scale = { domain: mockDomain };
50
- expect(resolveTickVals(scale, 5)).toEqual([]);
50
+ expect(autoTickVals(scale, 5)).toEqual([]);
51
51
  });
52
52
  it('handles null ticks with placement', () => {
53
53
  const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2, 3]) };
54
- expect(resolveTickVals(scale, null, undefined)).toEqual([1, 2, 3]);
54
+ expect(autoTickVals(scale, null, undefined)).toEqual([1, 2, 3]);
55
55
  expect(scale.ticks).toHaveBeenCalledWith(undefined);
56
56
  });
57
- it('removes last tick when interval is provided', () => {
58
- const interval = { every: vi.fn() };
59
- const scale = { ticks: vi.fn(() => [1, 2, 3, 4]) };
60
- expect(resolveTickVals(scale, undefined, undefined, interval)).toEqual([1, 2, 3]);
61
- });
62
57
  });
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Sean Lynch <techniq35@gmail.com>",
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
- "version": "2.0.0-next.32",
7
+ "version": "2.0.0-next.34",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.29.4",
10
10
  "@iconify-json/lucide": "^1.2.48",
@@ -72,10 +72,10 @@
72
72
  "type": "module",
73
73
  "dependencies": {
74
74
  "@dagrejs/dagre": "^1.1.4",
75
- "@layerstack/svelte-actions": "1.0.1-next.12",
76
- "@layerstack/svelte-state": "0.1.0-next.17",
77
- "@layerstack/tailwind": "2.0.0-next.15",
78
- "@layerstack/utils": "2.0.0-next.12",
75
+ "@layerstack/svelte-actions": "1.0.1-next.14",
76
+ "@layerstack/svelte-state": "0.1.0-next.19",
77
+ "@layerstack/tailwind": "2.0.0-next.17",
78
+ "@layerstack/utils": "2.0.0-next.14",
79
79
  "d3-array": "^3.2.4",
80
80
  "d3-color": "^3.1.0",
81
81
  "d3-delaunay": "^6.0.4",