layerchart 2.0.0-next.33 → 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.
@@ -439,8 +439,8 @@
439
439
  {#if rule !== false}
440
440
  {@const ruleProps = extractLayerProps(rule, 'axis-rule')}
441
441
  <Rule
442
- x={placement === 'left' || placement === 'right' ? placement : placement === 'angle'}
443
- 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'}
444
444
  {motion}
445
445
  {...ruleProps}
446
446
  class={cls('stroke-surface-content/50', classes.rule, ruleProps?.class)}
@@ -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 {
@@ -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
  };
@@ -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();
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.33",
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",