Skip to content

Commit 36a337e

Browse files
authored
feature: add jitterX and jitterY transforms
Merge pull request #65 from svelteplot/feat/jitter-transform
2 parents 8166086 + 9521b6a commit 36a337e

File tree

12 files changed

+634
-7
lines changed

12 files changed

+634
-7
lines changed

.github/workflows/npm-prerelease.yml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: npm prerelease
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
permissions:
8+
contents: write
9+
pull-requests: write
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install pnpm
19+
uses: pnpm/action-setup@v3
20+
with:
21+
version: 10
22+
run_install: false
23+
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: '22'
28+
cache: 'pnpm'
29+
30+
- name: Install dependencies
31+
run: pnpm install
32+
33+
- name: Run Vitest tests
34+
run: pnpm test
35+
36+
publish-preview:
37+
# Prevent this job from running on forks
38+
if: github.repository == 'svelteplot/svelteplot'
39+
if: github.event.pull_request.draft == false
40+
needs: test
41+
runs-on: ubuntu-latest
42+
environment:
43+
name: npm
44+
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Install pnpm
49+
uses: pnpm/action-setup@v3
50+
with:
51+
version: 10
52+
run_install: false
53+
54+
- name: Setup Node.js
55+
uses: actions/setup-node@v4
56+
with:
57+
node-version: '22'
58+
cache: 'pnpm'
59+
registry-url: "https://registry.npmjs.org"
60+
61+
- name: Install dependencies
62+
run: pnpm install
63+
64+
- name: Generate preview version
65+
run: |
66+
pr_number=${{ github.event.pull_request.number }}
67+
68+
# Get existing prerelease versions for this PR
69+
existing_versions=$(npm view svelteplot versions --json 2>/dev/null | grep -o "\"[0-9]*\.[0-9]*\.[0-9]*-pr-${pr_number}\.[0-9]*\"" | tr -d '"' || echo "")
70+
71+
if [ -n "$existing_versions" ]; then
72+
# Get the highest existing prerelease version
73+
latest_version=$(echo "$existing_versions" | sort -V | tail -n 1)
74+
75+
echo "Found latest existing prerelease version: $latest_version"
76+
77+
# Extract base version (before the `-pr-N.x` part)
78+
base_version=$(echo "$latest_version" | sed -E "s/-pr-${pr_number}\.[0-9]+//")
79+
80+
# Extract current prerelease number (the `.x` part)
81+
prerelease_num=$(echo "$latest_version" | sed -E "s/.*-pr-${pr_number}\.([0-9]+)$/\1/")
82+
83+
# Increment prerelease number
84+
next_prerelease=$((prerelease_num + 1))
85+
86+
next_version="${base_version}-pr-${pr_number}.${next_prerelease}"
87+
88+
echo "Bumping to next prerelease version: $next_version"
89+
90+
npm version "$next_version" --no-git-tag-version
91+
else
92+
# No existing prerelease, start fresh from current base version
93+
base_version=$(node -p "require('./package.json').version")
94+
next_version="${base_version}-pr-${pr_number}.0"
95+
96+
echo "Starting fresh prerelease version: $next_version"
97+
98+
npm version "$next_version" --no-git-tag-version
99+
fi
100+
101+
echo "Generated version: $(node -p "require('./package.json').version")"
102+
103+
104+
105+
- name: Publish to npm
106+
env:
107+
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH }}
108+
run: npm publish --tag pr-${{ github.event.pull_request.number }} --access public
109+
110+
# Save version for use in PR comment
111+
- name: Save version
112+
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
113+
114+
- name: Comment on PR
115+
uses: peter-evans/create-or-update-comment@v3
116+
with:
117+
issue-number: ${{ github.event.pull_request.number }}
118+
body: |
119+
📦 Preview package for this PR is published!
120+
121+
Version: `${{ env.PACKAGE_VERSION }}`
122+
123+
Install it with:
124+
```bash
125+
npm install svelteplot@pr-${{ github.event.pull_request.number }}
126+
# or install the specific version
127+
npm install svelteplot@${{ env.PACKAGE_VERSION }}
128+
```
129+
reactions: '+1, rocket'
130+
edit-mode: replace

config/sidebar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export default {
8282
{ title: 'Filter', to: '/transforms/filter' },
8383
{ title: 'Group', to: '/transforms/group' },
8484
{ title: 'Interval', to: '/transforms/interval' },
85+
{ title: 'Jitter', to: '/transforms/jitter' },
8586
{ title: 'Map', to: '/transforms/map' },
8687
{ title: 'Normalize', to: '/transforms/normalize' },
8788
{ title: 'Select', to: '/transforms/select' },

src/lib/helpers/time.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const tickIntervals = [
8787
['100 years', 100 * durationYear] // TODO generalize to longer time scales
8888
];
8989

90-
const durations = new Map([
90+
export const durations = new Map([
9191
['second', durationSecond],
9292
['minute', durationMinute],
9393
['hour', durationHour],
@@ -193,7 +193,7 @@ const formatIntervals = [
193193
...utcFormatIntervals.slice(3)
194194
];
195195

196-
export function parseTimeInterval(input) {
196+
export function parseTimeInterval(input: string): [string, number] {
197197
let name = `${input}`.toLowerCase();
198198
if (name.endsWith('s')) name = name.slice(0, -1); // drop plural
199199
let period = 1;
@@ -218,15 +218,15 @@ export function parseTimeInterval(input) {
218218
return [name, period];
219219
}
220220

221-
export function maybeTimeInterval(input) {
221+
export function maybeTimeInterval(input: string) {
222222
return asInterval(parseTimeInterval(input), 'time');
223223
}
224224

225-
export function maybeUtcInterval(input) {
225+
export function maybeUtcInterval(input: string) {
226226
return asInterval(parseTimeInterval(input), 'utc');
227227
}
228228

229-
function asInterval([name, period], type) {
229+
function asInterval([name, period]: [string, number], type: 'time' | 'utc') {
230230
let interval = (type === 'time' ? timeIntervals : utcIntervals).get(name);
231231
if (period > 1) {
232232
interval = interval.every(period);

src/lib/helpers/typeChecks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function isBooleanOrNull(v: RawValue) {
1414
return v == null || typeof v === 'boolean';
1515
}
1616

17-
export function isDate(v: RawValue) {
17+
export function isDate(v: RawValue): v is Date {
1818
return v instanceof Date && !isNaN(v.getTime());
1919
}
2020

src/lib/transforms/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { map, mapX, mapY } from './map.js';
66
export { normalizeX, normalizeY } from './normalize.js';
77
export { group, groupX, groupY, groupZ } from './group.js';
88
export { intervalX, intervalY } from './interval.js';
9+
export { jitterX, jitterY } from './jitter.js';
910
export { recordizeX, recordizeY } from './recordize.js';
1011
export { renameChannels, replaceChannels } from './rename.js';
1112
export {
@@ -18,6 +19,7 @@ export {
1819
selectMinY
1920
} from './select.js';
2021
export { shiftX, shiftY } from './shift.js';
22+
2123
export { sort, shuffle, reverse } from './sort.js';
2224
export { stackX, stackY } from './stack.js';
2325
export { windowX, windowY } from './window.js';

src/lib/transforms/jitter.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// @ts-nocheck
2+
import { describe, it, expect } from 'vitest';
3+
import { jitterX, jitterY } from './jitter.js';
4+
import { randomLcg } from 'd3-random';
5+
6+
// Tests for the jitter transforms
7+
describe('jitterX', () => {
8+
it('should add uniform jitter to x values with default options', () => {
9+
// Create a deterministic random source that returns exactly what we need
10+
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]
11+
12+
const data = [{ x: 5 }, { x: 10 }];
13+
// @ts-ignore - Bypassing type checking for tests
14+
const result = jitterX({ data, x: 'x' }, { source: mockRandom });
15+
// The result should add the jitter values to the original x values
16+
const { x } = result;
17+
// Check approximate values
18+
expect(result.data[0][x]).toBe(5.35, 2);
19+
expect(result.data[1][x]).toBe(10.35, 2);
20+
});
21+
22+
it('should add uniform jitter to x values with custom width', () => {
23+
// Create a deterministic random source that returns exactly what we need
24+
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]
25+
26+
const data = [{ x: 5 }, { x: 10 }];
27+
// @ts-ignore - Bypassing type checking for tests
28+
const result = jitterX({ data, x: 'x' }, { width: 0.5, source: mockRandom });
29+
// The result should add the jitter values to the original x values
30+
const { x } = result;
31+
// Check approximate values
32+
expect(result.data[0][x]).toBe(5.5, 2);
33+
expect(result.data[1][x]).toBe(10.5, 2);
34+
});
35+
36+
it('should add normal jitter to x values', () => {
37+
// We'll simplify this test by not trying to mock d3-random directly
38+
// Instead, we'll provide a source function that controls the output values
39+
const data = [{ x: 5 }, { x: 10 }];
40+
41+
// Custom source function that controls the exact jitter values
42+
let values = [0.05, -0.1]; // The exact jitter values we want
43+
let index = 0;
44+
45+
const mockSource = randomLcg(42);
46+
47+
// @ts-ignore - Bypassing type checking for tests
48+
const result = jitterX(
49+
{ data, x: 'x' },
50+
{
51+
type: 'normal',
52+
std: 0.2,
53+
// Use our custom function as the source
54+
// This effectively hijacks the normal distribution calculation
55+
source: mockSource
56+
}
57+
);
58+
59+
// The result should add the jitter values to the original x values
60+
const { x } = result;
61+
expect(result.data[0][x]).toBeCloseTo(4.9318, 3);
62+
expect(result.data[1][x]).toBeCloseTo(9.9589, 3);
63+
});
64+
65+
// // Note: Date jittering is not yet supported, test will be added when implemented
66+
67+
it('should not modify data if x channel is not provided', () => {
68+
const mockRandom = () => 0.5;
69+
70+
const data = [{ y: 5 }, { y: 10 }];
71+
// @ts-ignore - Bypassing type checking for tests
72+
const result = jitterX({ data, y: 'y' }, { source: mockRandom });
73+
74+
// The result should be the same as the input
75+
expect(result.data).toEqual(data);
76+
expect(result.y).toBe('y');
77+
});
78+
79+
it('should parse time interval strings for width/std', () => {
80+
// This isn't fully implemented in the jitter.ts but mentioned in a TODO comment
81+
const mockRandom = () => 0.75;
82+
83+
const data = [{ x: new Date(Date.UTC(2020, 0, 1)) }, { x: new Date(Date.UTC(2021, 0, 1)) }];
84+
// @ts-ignore - Bypassing type checking for tests
85+
const result = jitterX({ data, x: 'x' }, { source: mockRandom, width: '1 month' });
86+
87+
const { x } = result;
88+
expect(result.data[0][x]).toBeTypeOf('object');
89+
expect(result.data[0][x].getTime).toBeTypeOf('function');
90+
expect(result.data[0][x]).toStrictEqual(new Date(Date.UTC(2020, 0, 16)));
91+
});
92+
});
93+
94+
describe('jitterY', () => {
95+
it('should add uniform jitter to x values with default options', () => {
96+
// Create a deterministic random source that returns exactly what we need
97+
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]
98+
99+
const data = [{ x: 5 }, { x: 10 }];
100+
// @ts-ignore - Bypassing type checking for tests
101+
const result = jitterY({ data, y: 'x' }, { source: mockRandom });
102+
// The result should add the jitter values to the original x values
103+
const { y } = result;
104+
// Check approximate values
105+
expect(result.data[0][y]).toBe(5.35, 2);
106+
expect(result.data[1][y]).toBe(10.35, 2);
107+
});
108+
109+
it('should add uniform jitter to x values with custom width', () => {
110+
// Create a deterministic random source that returns exactly what we need
111+
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]
112+
113+
const data = [{ x: 5 }, { x: 10 }];
114+
// @ts-ignore - Bypassing type checking for tests
115+
const result = jitterY({ data, y: 'x' }, { width: 0.5, source: mockRandom });
116+
// The result should add the jitter values to the original x values
117+
const { y } = result;
118+
// Check approximate values
119+
expect(result.data[0][y]).toBe(5.5, 2);
120+
expect(result.data[1][y]).toBe(10.5, 2);
121+
});
122+
123+
it('should add normal jitter to x values', () => {
124+
// We'll simplify this test by not trying to mock d3-random directly
125+
// Instead, we'll provide a source function that controls the output values
126+
const data = [{ x: 5 }, { x: 10 }];
127+
128+
// Custom source function that controls the exact jitter values
129+
let values = [0.05, -0.1]; // The exact jitter values we want
130+
let index = 0;
131+
132+
const mockSource = randomLcg(42);
133+
134+
// @ts-ignore - Bypassing type checking for tests
135+
const result = jitterY(
136+
{ data, y: 'x' },
137+
{
138+
type: 'normal',
139+
std: 0.2,
140+
// Use our custom function as the source
141+
// This effectively hijacks the normal distribution calculation
142+
source: mockSource
143+
}
144+
);
145+
146+
// The result should add the jitter values to the original x values
147+
const { y } = result;
148+
expect(result.data[0][y]).toBeCloseTo(4.9318, 3);
149+
expect(result.data[1][y]).toBeCloseTo(9.9589, 3);
150+
});
151+
152+
// // Note: Date jittering is not yet supported, test will be added when implemented
153+
154+
it('should not modify data if y channel is not provided', () => {
155+
const mockRandom = () => 0.5;
156+
157+
const data = [{ x: 5 }, { x: 10 }];
158+
// @ts-ignore - Bypassing type checking for tests
159+
const result = jitterY({ data, x: 'x' }, { source: mockRandom });
160+
161+
// The result should be the same as the input
162+
expect(result.data).toEqual(data);
163+
expect(result.x).toBe('x');
164+
});
165+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy