From ba0918ae63fc5e15b9af42365b524a5fb72a2404 Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 19 May 2025 09:27:33 +0200 Subject: [PATCH 01/24] feat: add jitterX and jitterY --- src/lib/helpers/time.ts | 12 +- src/lib/helpers/typeChecks.ts | 2 +- src/lib/transforms/jitter.ts | 65 +++++++++++ src/routes/features/transforms/+page.md | 19 +++ src/routes/transforms/bolliner/+page.md | 5 + src/routes/transforms/interval/+page.md | 1 - src/routes/transforms/jitter/+page.md | 147 ++++++++++++++++++++++++ src/routes/transforms/jitter/+page.ts | 8 ++ 8 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 src/routes/transforms/bolliner/+page.md create mode 100644 src/routes/transforms/jitter/+page.md create mode 100644 src/routes/transforms/jitter/+page.ts diff --git a/src/lib/helpers/time.ts b/src/lib/helpers/time.ts index 6b9a68d5..0307187a 100644 --- a/src/lib/helpers/time.ts +++ b/src/lib/helpers/time.ts @@ -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], @@ -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; @@ -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); @@ -245,7 +245,7 @@ export function generalizeTimeInterval(interval, n) { if (duration % durationDay === 0 && durationDay < duration && duration < durationMonth) return; // not generalizable const [i] = tickIntervals[ - bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n)) + bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n)) ]; return (interval[intervalType] === 'time' ? maybeTimeInterval : maybeUtcInterval)(i); } diff --git a/src/lib/helpers/typeChecks.ts b/src/lib/helpers/typeChecks.ts index 04d8ff22..8d2f985c 100644 --- a/src/lib/helpers/typeChecks.ts +++ b/src/lib/helpers/typeChecks.ts @@ -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()); } diff --git a/src/lib/transforms/jitter.ts b/src/lib/transforms/jitter.ts index e69de29b..b0f9bedb 100644 --- a/src/lib/transforms/jitter.ts +++ b/src/lib/transforms/jitter.ts @@ -0,0 +1,65 @@ +import type { Channels, DataRecord, TransformArg } from '$lib/types.js'; +import { resolveChannel } from 'svelteplot/helpers/resolve'; +import { randomUniform, randomNormal } from 'd3-random'; +import { isDate } from 'svelteplot/helpers/typeChecks'; +import { durations, maybeTimeInterval, parseTimeInterval } from 'svelteplot/helpers/time'; + +const JITTER_X = Symbol('jitterX'); +const JITTER_Y = Symbol('jitterY'); + +type JitterOptions = { + type: 'uniform' | 'normal'; + /** width for uniform jittering */ + width: number; + /** standard deviation for normal jittering */ + std: number; +} + +export function jitterX({ data, ...channels }: TransformArg, options: JitterOptions): TransformArg { + return jitter('x', data, channels, options); +} + +export function jitterY({ data, ...channels }: TransformArg, options: JitterOptions): TransformArg { + return jitter('y', data, channels, options); +} + +export function jitter(channel: 'x' | 'y', data: DataRecord[], channels: Channels, options: JitterOptions): TransformArg { + if (channels[channel]) { + const type = options?.type ?? 'uniform'; + const width = parseNumber(options?.width ?? 0.35); + const std = parseNumber(options?.std ?? 0.15); + // @todo support time interval strings as width/std parameters + + const random = type === 'uniform' ? randomUniform(-width, width) : randomNormal(0, std); + const accKey = channel === 'x' ? JITTER_X : JITTER_Y; + return { + data: data.map(row => { + const value = resolveChannel(channel, row, channels); + return { + ...row, + [accKey]: typeof value === 'number' ? value + random() : isDate(value) ? new Date(value.getTime() + random()) : value + } + }), + ...channels, + // point channel to new accessor symbol + [channel]: accKey + } + } + return { + data, + ...channels, + }; +} + +function parseNumber(value: number | string): number { + if (typeof value === 'number') return value; + if (typeof value === 'string') { + try { + const [name, period] = parseTimeInterval(value); + return durations.get(name) * period; + } catch (err) { + return 0; + } + } + return 0; +} \ No newline at end of file diff --git a/src/routes/features/transforms/+page.md b/src/routes/features/transforms/+page.md index 684408af..57d3313d 100644 --- a/src/routes/features/transforms/+page.md +++ b/src/routes/features/transforms/+page.md @@ -90,3 +90,22 @@ In case you want to use SveltePlot transforms outside of a Svelte project you ca ```js import { binX } from 'svelteplot/transforms'; ``` +## Available Transforms + +- [bin](/transforms/bin) - Groups data into discrete bins +- [bollinger](/transforms/bollinger) - Creates Bollinger bands for time series data +- [centroid](/transforms/centroid) - Calculates the geometric center of a set of points +- [facet](/transforms/facet) - Splits data into multiple subplots +- [filter](/transforms/filter) - Filters data based on a condition +- [group](/transforms/group) - Groups data by specified dimensions +- [interval](/transforms/interval) - Creates time or numeric intervals +- [jitter](/transforms/jitter) - Adds random noise to prevent overplotting +- [map](/transforms/map) - Applies a mapping function to data +- [normalize](/transforms/normalize) - Scales data to a common range +- [recordize](/transforms/recordize) - Converts raw data to record format +- [rename](/transforms/rename) - Renames channels in a dataset +- [select](/transforms/select) - Selects specific channels or data points +- [shift](/transforms/shift) - Shifts data values by a specified amount +- [sort](/transforms/sort) - Sorts data based on specified criteria +- [stack](/transforms/stack) - Stacks data series on top of each other +- [window](/transforms/window) - Creates a moving window over data diff --git a/src/routes/transforms/bolliner/+page.md b/src/routes/transforms/bolliner/+page.md new file mode 100644 index 00000000..4c73cae0 --- /dev/null +++ b/src/routes/transforms/bolliner/+page.md @@ -0,0 +1,5 @@ +--- +title: Bollinger transform +--- + +TODO: show how to use the Bollinger transform directly diff --git a/src/routes/transforms/interval/+page.md b/src/routes/transforms/interval/+page.md index fac7b033..9914781f 100644 --- a/src/routes/transforms/interval/+page.md +++ b/src/routes/transforms/interval/+page.md @@ -9,7 +9,6 @@ The interval transform is often used for time-series bar charts. For example, co ```svelte live + + +{#if type === 'uniform'} + +{:else} + +{/if} + + + +``` + +This example shows how jittering can be applied to date values in the x-axis, which can be useful when multiple events occur at the same date and would otherwise overlap. diff --git a/src/routes/transforms/jitter/+page.ts b/src/routes/transforms/jitter/+page.ts new file mode 100644 index 00000000..b89209f7 --- /dev/null +++ b/src/routes/transforms/jitter/+page.ts @@ -0,0 +1,8 @@ +import { loadDatasets } from '$lib/helpers/data.js'; +import type { PageLoad } from './$types.js'; + +export const load: PageLoad = async ({ fetch }) => { + return { + data: await loadDatasets(['cars'], fetch) + }; +}; From b8423405e48aef594a404c0a6373e05de7df063a Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 18:43:42 +0200 Subject: [PATCH 02/24] export jitter transform + link to page --- config/sidebar.ts | 1 + src/lib/transforms/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/config/sidebar.ts b/config/sidebar.ts index b6f2f6a4..69018e6f 100644 --- a/config/sidebar.ts +++ b/config/sidebar.ts @@ -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' }, diff --git a/src/lib/transforms/index.ts b/src/lib/transforms/index.ts index 96845e76..0f350816 100644 --- a/src/lib/transforms/index.ts +++ b/src/lib/transforms/index.ts @@ -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 { From 64c87655fad7f3b3a2f99662fff8ff5f0b4e4c78 Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 18:45:55 +0200 Subject: [PATCH 03/24] format --- src/lib/helpers/time.ts | 2 +- src/lib/transforms/jitter.ts | 36 ++++++++++++++++++------- src/routes/features/transforms/+page.md | 1 + 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/lib/helpers/time.ts b/src/lib/helpers/time.ts index 0307187a..60387869 100644 --- a/src/lib/helpers/time.ts +++ b/src/lib/helpers/time.ts @@ -245,7 +245,7 @@ export function generalizeTimeInterval(interval, n) { if (duration % durationDay === 0 && durationDay < duration && duration < durationMonth) return; // not generalizable const [i] = tickIntervals[ - bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n)) + bisector(([, step]) => Math.log(step)).center(tickIntervals, Math.log(duration * n)) ]; return (interval[intervalType] === 'time' ? maybeTimeInterval : maybeUtcInterval)(i); } diff --git a/src/lib/transforms/jitter.ts b/src/lib/transforms/jitter.ts index b0f9bedb..e986dc64 100644 --- a/src/lib/transforms/jitter.ts +++ b/src/lib/transforms/jitter.ts @@ -13,17 +13,28 @@ type JitterOptions = { width: number; /** standard deviation for normal jittering */ std: number; -} +}; -export function jitterX({ data, ...channels }: TransformArg, options: JitterOptions): TransformArg { +export function jitterX( + { data, ...channels }: TransformArg, + options: JitterOptions +): TransformArg { return jitter('x', data, channels, options); } -export function jitterY({ data, ...channels }: TransformArg, options: JitterOptions): TransformArg { +export function jitterY( + { data, ...channels }: TransformArg, + options: JitterOptions +): TransformArg { return jitter('y', data, channels, options); } -export function jitter(channel: 'x' | 'y', data: DataRecord[], channels: Channels, options: JitterOptions): TransformArg { +export function jitter( + channel: 'x' | 'y', + data: DataRecord[], + channels: Channels, + options: JitterOptions +): TransformArg { if (channels[channel]) { const type = options?.type ?? 'uniform'; const width = parseNumber(options?.width ?? 0.35); @@ -33,21 +44,26 @@ export function jitter(channel: 'x' | 'y', data: DataRecord[], channels: Channel const random = type === 'uniform' ? randomUniform(-width, width) : randomNormal(0, std); const accKey = channel === 'x' ? JITTER_X : JITTER_Y; return { - data: data.map(row => { + data: data.map((row) => { const value = resolveChannel(channel, row, channels); return { ...row, - [accKey]: typeof value === 'number' ? value + random() : isDate(value) ? new Date(value.getTime() + random()) : value - } + [accKey]: + typeof value === 'number' + ? value + random() + : isDate(value) + ? new Date(value.getTime() + random()) + : value + }; }), ...channels, // point channel to new accessor symbol [channel]: accKey - } + }; } return { data, - ...channels, + ...channels }; } @@ -62,4 +78,4 @@ function parseNumber(value: number | string): number { } } return 0; -} \ No newline at end of file +} diff --git a/src/routes/features/transforms/+page.md b/src/routes/features/transforms/+page.md index 57d3313d..4075f5f6 100644 --- a/src/routes/features/transforms/+page.md +++ b/src/routes/features/transforms/+page.md @@ -90,6 +90,7 @@ In case you want to use SveltePlot transforms outside of a Svelte project you ca ```js import { binX } from 'svelteplot/transforms'; ``` + ## Available Transforms - [bin](/transforms/bin) - Groups data into discrete bins From a32f843344ce726f88edb8a933061aebeaefc396 Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 21:29:34 +0200 Subject: [PATCH 04/24] add tests for jitter transform --- src/lib/transforms/index.ts | 1 + src/lib/transforms/jitter.test.ts | 175 ++++++++++++++++++++++++++++++ src/lib/transforms/jitter.ts | 16 ++- 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/lib/transforms/jitter.test.ts diff --git a/src/lib/transforms/index.ts b/src/lib/transforms/index.ts index 0f350816..d780fa20 100644 --- a/src/lib/transforms/index.ts +++ b/src/lib/transforms/index.ts @@ -19,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'; diff --git a/src/lib/transforms/jitter.test.ts b/src/lib/transforms/jitter.test.ts new file mode 100644 index 00000000..413af3a4 --- /dev/null +++ b/src/lib/transforms/jitter.test.ts @@ -0,0 +1,175 @@ +// @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'); + }); +}); diff --git a/src/lib/transforms/jitter.ts b/src/lib/transforms/jitter.ts index e986dc64..60bf409b 100644 --- a/src/lib/transforms/jitter.ts +++ b/src/lib/transforms/jitter.ts @@ -13,6 +13,11 @@ type JitterOptions = { width: number; /** standard deviation for normal jittering */ std: number; + /** + * optional random number source that produces values in range [0,1) + * useful for testing with a deterministic source + */ + source?: () => number; }; export function jitterX( @@ -41,7 +46,12 @@ export function jitter( const std = parseNumber(options?.std ?? 0.15); // @todo support time interval strings as width/std parameters - const random = type === 'uniform' ? randomUniform(-width, width) : randomNormal(0, std); + // Use the provided source or default to Math.random + const rng = options?.source ?? Math.random; + const random = type === 'uniform' + ? randomUniform.source(rng)(-width, width) + : randomNormal.source(rng)(0, std); + const accKey = channel === 'x' ? JITTER_X : JITTER_Y; return { data: data.map((row) => { @@ -52,8 +62,8 @@ export function jitter( typeof value === 'number' ? value + random() : isDate(value) - ? new Date(value.getTime() + random()) - : value + ? new Date(value.getTime() + random()) + : value }; }), ...channels, From 91d841376df16efd43341eac86f889edc23134b1 Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 21:43:08 +0200 Subject: [PATCH 05/24] add date jittering example --- src/routes/transforms/jitter/+page.md | 38 +++++++++++---------------- src/routes/transforms/jitter/+page.ts | 2 +- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/routes/transforms/jitter/+page.md b/src/routes/transforms/jitter/+page.md index bfd340d3..04a374b6 100644 --- a/src/routes/transforms/jitter/+page.md +++ b/src/routes/transforms/jitter/+page.md @@ -64,6 +64,7 @@ The jitter transform accepts the following options: - **type**: Distribution type, either `'uniform'` (default) or `'normal'` - **width**: Width of the uniform distribution (default: `0.35`); used when `type` is `'uniform'` - **std**: Standard deviation for the normal distribution (default: `0.15`); used when `type` is `'normal'` +- **source**: Optional random number source that produces values in range [0,1). ## jitterX @@ -93,52 +94,43 @@ Jitters along the y dimension Jittering also works for temporal data. When jittering Date objects, random time offsets are added to each date value: -```svelte +```svelte live - From b7fadfc1c886c65e4e08ab7ef9a28b81304d3c7c Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 22:00:32 +0200 Subject: [PATCH 07/24] add github actions workflow that publishes a preview version to npm --- .github/workflows/preview-publish.yml | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/preview-publish.yml diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml new file mode 100644 index 00000000..6711bbe2 --- /dev/null +++ b/.github/workflows/preview-publish.yml @@ -0,0 +1,47 @@ +name: Publish PR Preview Package + +on: + pull_request: + types: [opened, synchronize] + +jobs: + publish-preview: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org/' + + - name: Install dependencies + run: pnpm i + + - name: Generate preview version + run: | + pr_number=${{ github.event.pull_request.number }} + npm version prerelease --preid=pr-${pr_number} --no-git-tag-version + + - name: Publish to npm + environment: + name: npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH }} + run: npm publish --tag pr-${{ github.event.pull_request.number }} --access public + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + 📦 Preview package for this PR is published! + + Install it with: + ```bash + npm install svelteplot@pr-${{ github.event.pull_request.number }} + ``` \ No newline at end of file From 45b220fe0f392781f60330a697ffd8d201f232b2 Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 22:07:57 +0200 Subject: [PATCH 08/24] improve jitter docs --- src/routes/transforms/jitter/+page.md | 61 ++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/routes/transforms/jitter/+page.md b/src/routes/transforms/jitter/+page.md index adb2e55e..2699c370 100644 --- a/src/routes/transforms/jitter/+page.md +++ b/src/routes/transforms/jitter/+page.md @@ -2,9 +2,11 @@ title: Jitter transform --- -The **jitter transform** adds random noise to data points, which is useful for revealing overlapping points in scatter plots and reducing overplotting. This is particularly helpful when working with discrete or categorical data where many points might share the same coordinates. +The **jitter transform** adds random noise to data points, which is useful for revealing overlapping points in scatter plots and reducing overplotting. -> **Note:** The jitter transform works in data coordinates. To jitter in screen coordinates, you can use the `dx` and `dy` mark properties instead. +:::info +**Note:** The jitter transform works in data coordinates. To jitter in screen coordinates, you can use the `dx` and `dy` mark properties instead. +::: The jitter transform spreads out overlapping points by adding random noise. This makes it easier to see the distribution and density of points: @@ -62,13 +64,29 @@ The jitter transform spreads out overlapping points by adding random noise. This The jitter transform accepts the following options: - **type**: Distribution type, either `'uniform'` (default) or `'normal'` + - `uniform`: Evenly distributed points within range [-width, width] + - `normal`: Normal distribution centered at 0 with standard deviation `std` - **width**: Width of the uniform distribution (default: `0.35`); used when `type` is `'uniform'` + - For numeric data: A number representing the range on either side of the original value + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) - **std**: Standard deviation for the normal distribution (default: `0.15`); used when `type` is `'normal'` -- **source**: Optional random number source that produces values in range [0,1). + - For numeric data: A number representing the standard deviation + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) +- **source**: Optional random number source that produces values in range [0,1) + - Useful for deterministic jittering (testing or reproducibility) + - Can be used with d3's random generators: `randomLcg()` from d3-random + +The following time interval strings are supported for temporal jittering: +- `'1 day'`, `'3 days'` +- `'1 week'`, `'2 weeks'`, `'3 weeks'` +- `'1 month'`, `'2 months'` +- `'1 quarter'` +- `'1 year'` + ## jitterX -Jitters along the x dimensio +Jitters along the x dimension: ```svelte ``` -## Jittering with dates + +## Temporal jittering Jittering also works for temporal data. When jittering Date objects, random time offsets are added to each date value: @@ -154,3 +173,33 @@ Jittering also works for temporal data. When jittering Date objects, random time ``` This example shows how jittering can be applied to date values in the x-axis, which can be useful when multiple events occur at the same date and would otherwise overlap. + +## Custom random sources + +For reproducible jittering or specialized random distributions, you can provide a custom random source: + +```svelte + + + + + +``` + +This is particularly useful for: +- Testing and debugging visualizations +- Creating reproducible figures for publications +- Ensuring consistent visual appearance across renders From 7a6ec51f515b7d511ac58ceda08de9e17d41f7da Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 22:10:29 +0200 Subject: [PATCH 09/24] trigger preview publish on commits --- .github/workflows/preview-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml index 6711bbe2..faa81106 100644 --- a/.github/workflows/preview-publish.yml +++ b/.github/workflows/preview-publish.yml @@ -2,8 +2,8 @@ name: Publish PR Preview Package on: pull_request: - types: [opened, synchronize] - + branches: [main] + jobs: publish-preview: runs-on: ubuntu-latest From 9046e7141fb5ee035584ef575e98511adf529d9a Mon Sep 17 00:00:00 2001 From: gka Date: Mon, 26 May 2025 22:10:53 +0200 Subject: [PATCH 10/24] format --- .github/workflows/preview-publish.yml | 86 +++++++++++++-------------- src/routes/transforms/jitter/+page.md | 28 ++++----- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml index faa81106..8155c405 100644 --- a/.github/workflows/preview-publish.yml +++ b/.github/workflows/preview-publish.yml @@ -1,47 +1,47 @@ name: Publish PR Preview Package on: - pull_request: - branches: [main] - + pull_request: + branches: [main] + jobs: - publish-preview: - runs-on: ubuntu-latest - permissions: - contents: write - issues: write - pull-requests: write - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: 'https://registry.npmjs.org/' - - - name: Install dependencies - run: pnpm i - - - name: Generate preview version - run: | - pr_number=${{ github.event.pull_request.number }} - npm version prerelease --preid=pr-${pr_number} --no-git-tag-version - - - name: Publish to npm - environment: - name: npm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH }} - run: npm publish --tag pr-${{ github.event.pull_request.number }} --access public - - - name: Comment on PR - uses: marocchino/sticky-pull-request-comment@v2 - with: - message: | - 📦 Preview package for this PR is published! - - Install it with: - ```bash - npm install svelteplot@pr-${{ github.event.pull_request.number }} - ``` \ No newline at end of file + publish-preview: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org/' + + - name: Install dependencies + run: pnpm i + + - name: Generate preview version + run: | + pr_number=${{ github.event.pull_request.number }} + npm version prerelease --preid=pr-${pr_number} --no-git-tag-version + + - name: Publish to npm + environment: + name: npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH }} + run: npm publish --tag pr-${{ github.event.pull_request.number }} --access public + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + 📦 Preview package for this PR is published! + + Install it with: + ```bash + npm install svelteplot@pr-${{ github.event.pull_request.number }} + ``` diff --git a/src/routes/transforms/jitter/+page.md b/src/routes/transforms/jitter/+page.md index 2699c370..4fe9503b 100644 --- a/src/routes/transforms/jitter/+page.md +++ b/src/routes/transforms/jitter/+page.md @@ -2,7 +2,7 @@ title: Jitter transform --- -The **jitter transform** adds random noise to data points, which is useful for revealing overlapping points in scatter plots and reducing overplotting. +The **jitter transform** adds random noise to data points, which is useful for revealing overlapping points in scatter plots and reducing overplotting. :::info **Note:** The jitter transform works in data coordinates. To jitter in screen coordinates, you can use the `dx` and `dy` mark properties instead. @@ -64,26 +64,26 @@ The jitter transform spreads out overlapping points by adding random noise. This The jitter transform accepts the following options: - **type**: Distribution type, either `'uniform'` (default) or `'normal'` - - `uniform`: Evenly distributed points within range [-width, width] - - `normal`: Normal distribution centered at 0 with standard deviation `std` + - `uniform`: Evenly distributed points within range [-width, width] + - `normal`: Normal distribution centered at 0 with standard deviation `std` - **width**: Width of the uniform distribution (default: `0.35`); used when `type` is `'uniform'` - - For numeric data: A number representing the range on either side of the original value - - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) + - For numeric data: A number representing the range on either side of the original value + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) - **std**: Standard deviation for the normal distribution (default: `0.15`); used when `type` is `'normal'` - - For numeric data: A number representing the standard deviation - - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) + - For numeric data: A number representing the standard deviation + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) - **source**: Optional random number source that produces values in range [0,1) - - Useful for deterministic jittering (testing or reproducibility) - - Can be used with d3's random generators: `randomLcg()` from d3-random + - Useful for deterministic jittering (testing or reproducibility) + - Can be used with d3's random generators: `randomLcg()` from d3-random The following time interval strings are supported for temporal jittering: + - `'1 day'`, `'3 days'` - `'1 week'`, `'2 weeks'`, `'3 weeks'` -- `'1 month'`, `'2 months'` +- `'1 month'`, `'2 months'` - `'1 quarter'` - `'1 year'` - ## jitterX Jitters along the x dimension: @@ -108,7 +108,6 @@ Jitters along the y dimension: )} /> ``` - ## Temporal jittering Jittering also works for temporal data. When jittering Date objects, random time offsets are added to each date value: @@ -182,11 +181,11 @@ For reproducible jittering or specialized random distributions, you can provide