layerchart 2.0.0-next.31 → 2.0.0-next.33

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,
@@ -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
@@ -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)
@@ -103,9 +103,14 @@
103
103
  const scaledX: number = ctx.xScale(xVal);
104
104
  const scaledY: number = ctx.yScale(yVal);
105
105
 
106
+ const x = scaledX + getOffset(scaledX, offsetX, ctx.xScale);
107
+ const y = scaledY + getOffset(scaledY, offsetY, ctx.yScale);
108
+
109
+ const radialPoint = pointRadial(x, y);
110
+
106
111
  return {
107
- x: scaledX + getOffset(scaledX, offsetX, ctx.xScale),
108
- y: scaledY + getOffset(scaledY, offsetY, ctx.yScale),
112
+ x: ctx.radial ? radialPoint[0] : x,
113
+ y: ctx.radial ? radialPoint[1] : y,
109
114
  r: ctx.config.r ? ctx.rGet(d) : r,
110
115
  xValue: xVal,
111
116
  yValue: yVal,
@@ -192,10 +197,9 @@
192
197
  {/if}
193
198
 
194
199
  {#each points as point}
195
- {@const radialPoint = pointRadial(point.x, point.y)}
196
200
  <Circle
197
- cx={ctx.radial ? radialPoint[0] : point.x}
198
- cy={ctx.radial ? radialPoint[1] : point.y}
201
+ cx={point.x}
202
+ cy={point.y}
199
203
  r={point.r}
200
204
  fill={fill ?? (ctx.config.c ? ctx.cGet(point.data) : null)}
201
205
  {fillOpacity}
@@ -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 };
@@ -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.31",
7
+ "version": "2.0.0-next.33",
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",