Skip to content

feature: add jitterX and jitterY transforms #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/npm-prerelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: npm prerelease

on:
pull_request:
branches: [main]

permissions:
contents: write
pull-requests: write

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 10
run_install: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Run Vitest tests
run: pnpm test

publish-preview:
# Prevent this job from running on forks
if: github.repository == 'svelteplot/svelteplot'
if: github.event.pull_request.draft == false
needs: test
runs-on: ubuntu-latest
environment:
name: npm

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 10
run_install: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: pnpm install

- name: Generate preview version
run: |
pr_number=${{ github.event.pull_request.number }}

# Get existing prerelease versions for this PR
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 "")

if [ -n "$existing_versions" ]; then
# Get the highest existing prerelease version
latest_version=$(echo "$existing_versions" | sort -V | tail -n 1)

echo "Found latest existing prerelease version: $latest_version"

# Extract base version (before the `-pr-N.x` part)
base_version=$(echo "$latest_version" | sed -E "s/-pr-${pr_number}\.[0-9]+//")

# Extract current prerelease number (the `.x` part)
prerelease_num=$(echo "$latest_version" | sed -E "s/.*-pr-${pr_number}\.([0-9]+)$/\1/")

# Increment prerelease number
next_prerelease=$((prerelease_num + 1))

next_version="${base_version}-pr-${pr_number}.${next_prerelease}"

echo "Bumping to next prerelease version: $next_version"

npm version "$next_version" --no-git-tag-version
else
# No existing prerelease, start fresh from current base version
base_version=$(node -p "require('./package.json').version")
next_version="${base_version}-pr-${pr_number}.0"

echo "Starting fresh prerelease version: $next_version"

npm version "$next_version" --no-git-tag-version
fi

echo "Generated version: $(node -p "require('./package.json').version")"



- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH }}
run: npm publish --tag pr-${{ github.event.pull_request.number }} --access public

# Save version for use in PR comment
- name: Save version
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV

- name: Comment on PR
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
📦 Preview package for this PR is published!

Version: `${{ env.PACKAGE_VERSION }}`

Install it with:
```bash
npm install svelteplot@pr-${{ github.event.pull_request.number }}
# or install the specific version
npm install svelteplot@${{ env.PACKAGE_VERSION }}
```
reactions: '+1, rocket'
edit-mode: replace
1 change: 1 addition & 0 deletions config/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default {
{ title: 'Filter', to: '/transforms/filter' },
{ title: 'Group', to: '/transforms/group' },
{ title: 'Interval', to: '/transforms/interval' },
{ title: 'Jitter', to: '/transforms/jitter' },
{ title: 'Map', to: '/transforms/map' },
{ title: 'Normalize', to: '/transforms/normalize' },
{ title: 'Select', to: '/transforms/select' },
Expand Down
10 changes: 5 additions & 5 deletions src/lib/helpers/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const tickIntervals = [
['100 years', 100 * durationYear] // TODO generalize to longer time scales
];

const durations = new Map([
export const durations = new Map([
['second', durationSecond],
['minute', durationMinute],
['hour', durationHour],
Expand Down Expand Up @@ -193,7 +193,7 @@ const formatIntervals = [
...utcFormatIntervals.slice(3)
];

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

export function maybeTimeInterval(input) {
export function maybeTimeInterval(input: string) {
return asInterval(parseTimeInterval(input), 'time');
}

export function maybeUtcInterval(input) {
export function maybeUtcInterval(input: string) {
return asInterval(parseTimeInterval(input), 'utc');
}

function asInterval([name, period], type) {
function asInterval([name, period]: [string, number], type: 'time' | 'utc') {
let interval = (type === 'time' ? timeIntervals : utcIntervals).get(name);
if (period > 1) {
interval = interval.every(period);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/helpers/typeChecks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function isBooleanOrNull(v: RawValue) {
return v == null || typeof v === 'boolean';
}

export function isDate(v: RawValue) {
export function isDate(v: RawValue): v is Date {
return v instanceof Date && !isNaN(v.getTime());
}

Expand Down
2 changes: 2 additions & 0 deletions src/lib/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { map, mapX, mapY } from './map.js';
export { normalizeX, normalizeY } from './normalize.js';
export { group, groupX, groupY, groupZ } from './group.js';
export { intervalX, intervalY } from './interval.js';
export { jitterX, jitterY } from './jitter.js';
export { recordizeX, recordizeY } from './recordize.js';
export { renameChannels, replaceChannels } from './rename.js';
export {
Expand All @@ -18,6 +19,7 @@ export {
selectMinY
} from './select.js';
export { shiftX, shiftY } from './shift.js';

export { sort, shuffle, reverse } from './sort.js';
export { stackX, stackY } from './stack.js';
export { windowX, windowY } from './window.js';
165 changes: 165 additions & 0 deletions src/lib/transforms/jitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// @ts-nocheck
import { describe, it, expect } from 'vitest';
import { jitterX, jitterY } from './jitter.js';
import { randomLcg } from 'd3-random';

// Tests for the jitter transforms
describe('jitterX', () => {
it('should add uniform jitter to x values with default options', () => {
// Create a deterministic random source that returns exactly what we need
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]

const data = [{ x: 5 }, { x: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterX({ data, x: 'x' }, { source: mockRandom });
// The result should add the jitter values to the original x values
const { x } = result;
// Check approximate values
expect(result.data[0][x]).toBe(5.35, 2);
expect(result.data[1][x]).toBe(10.35, 2);
});

it('should add uniform jitter to x values with custom width', () => {
// Create a deterministic random source that returns exactly what we need
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]

const data = [{ x: 5 }, { x: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterX({ data, x: 'x' }, { width: 0.5, source: mockRandom });
// The result should add the jitter values to the original x values
const { x } = result;
// Check approximate values
expect(result.data[0][x]).toBe(5.5, 2);
expect(result.data[1][x]).toBe(10.5, 2);
});

it('should add normal jitter to x values', () => {
// We'll simplify this test by not trying to mock d3-random directly
// Instead, we'll provide a source function that controls the output values
const data = [{ x: 5 }, { x: 10 }];

// Custom source function that controls the exact jitter values
let values = [0.05, -0.1]; // The exact jitter values we want
let index = 0;

const mockSource = randomLcg(42);

// @ts-ignore - Bypassing type checking for tests
const result = jitterX(
{ data, x: 'x' },
{
type: 'normal',
std: 0.2,
// Use our custom function as the source
// This effectively hijacks the normal distribution calculation
source: mockSource
}
);

// The result should add the jitter values to the original x values
const { x } = result;
expect(result.data[0][x]).toBeCloseTo(4.9318, 3);
expect(result.data[1][x]).toBeCloseTo(9.9589, 3);
});

// // Note: Date jittering is not yet supported, test will be added when implemented

it('should not modify data if x channel is not provided', () => {
const mockRandom = () => 0.5;

const data = [{ y: 5 }, { y: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterX({ data, y: 'y' }, { source: mockRandom });

// The result should be the same as the input
expect(result.data).toEqual(data);
expect(result.y).toBe('y');
});

it('should parse time interval strings for width/std', () => {
// This isn't fully implemented in the jitter.ts but mentioned in a TODO comment
const mockRandom = () => 0.75;

const data = [{ x: new Date(Date.UTC(2020, 0, 1)) }, { x: new Date(Date.UTC(2021, 0, 1)) }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterX({ data, x: 'x' }, { source: mockRandom, width: '1 month' });

const { x } = result;
expect(result.data[0][x]).toBeTypeOf('object');
expect(result.data[0][x].getTime).toBeTypeOf('function');
expect(result.data[0][x]).toStrictEqual(new Date(Date.UTC(2020, 0, 16)));
});
});

describe('jitterY', () => {
it('should add uniform jitter to x values with default options', () => {
// Create a deterministic random source that returns exactly what we need
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]

const data = [{ x: 5 }, { x: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterY({ data, y: 'x' }, { source: mockRandom });
// The result should add the jitter values to the original x values
const { y } = result;
// Check approximate values
expect(result.data[0][y]).toBe(5.35, 2);
expect(result.data[1][y]).toBe(10.35, 2);
});

it('should add uniform jitter to x values with custom width', () => {
// Create a deterministic random source that returns exactly what we need
const mockRandom = () => 1; // This will produce exactly +0.1 in range [-0.35, 0.35]

const data = [{ x: 5 }, { x: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterY({ data, y: 'x' }, { width: 0.5, source: mockRandom });
// The result should add the jitter values to the original x values
const { y } = result;
// Check approximate values
expect(result.data[0][y]).toBe(5.5, 2);
expect(result.data[1][y]).toBe(10.5, 2);
});

it('should add normal jitter to x values', () => {
// We'll simplify this test by not trying to mock d3-random directly
// Instead, we'll provide a source function that controls the output values
const data = [{ x: 5 }, { x: 10 }];

// Custom source function that controls the exact jitter values
let values = [0.05, -0.1]; // The exact jitter values we want
let index = 0;

const mockSource = randomLcg(42);

// @ts-ignore - Bypassing type checking for tests
const result = jitterY(
{ data, y: 'x' },
{
type: 'normal',
std: 0.2,
// Use our custom function as the source
// This effectively hijacks the normal distribution calculation
source: mockSource
}
);

// The result should add the jitter values to the original x values
const { y } = result;
expect(result.data[0][y]).toBeCloseTo(4.9318, 3);
expect(result.data[1][y]).toBeCloseTo(9.9589, 3);
});

// // Note: Date jittering is not yet supported, test will be added when implemented

it('should not modify data if y channel is not provided', () => {
const mockRandom = () => 0.5;

const data = [{ x: 5 }, { x: 10 }];
// @ts-ignore - Bypassing type checking for tests
const result = jitterY({ data, x: 'x' }, { source: mockRandom });

// The result should be the same as the input
expect(result.data).toEqual(data);
expect(result.x).toBe('x');
});
});
Loading
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