Skip to content

Fix/use unknown color #104

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 23 commits into from
May 31, 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
8 changes: 7 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import svelteParser from 'svelte-eslint-parser';
import tsParser from '@typescript-eslint/parser';

export default [
{
rules: {
'no-console': ['error', { allow: ['error'] }]
}
},
...svelte.configs.recommended,
{
files: ['**/*.svelte', '*.svelte'],
rules: {
'svelte/no-object-in-text-mustaches': 'warn'
'svelte/no-object-in-text-mustaches': 'warn',
'svelte/no-inspect': 'warn'
},
ignores: ['dist/*', '.sveltepress/*'],
languageOptions: {
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@
"@types/d3-scale": "^4.0.9",
"@types/d3-scale-chromatic": "^3.1.0",
"@types/d3-shape": "^3.1.7",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.33.0",
"csstype": "^3.1.3",
"d3-dsv": "^3.0.1",
"d3-fetch": "^3.0.1",
"d3-force": "^3.0.0",
"eslint": "^9.27.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "3.9.0",
"jsdom": "^26.1.0",
Expand Down Expand Up @@ -122,6 +122,6 @@
"es-toolkit": "^1.38.0",
"fast-equals": "^5.2.2",
"merge-deep": "^3.0.3",
"svelte": "5.33.2"
"svelte": "5.33.10"
}
}
381 changes: 203 additions & 178 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/lib/Mark.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@

const channelsWithFacets: ScaledChannelName[] = $derived([...channels, 'fx', 'fy']);

const { addMark, updateMark, updatePlotState, removeMark, getTopLevelFacet, getPlotState } =
const { addMark, removeMark, getTopLevelFacet, getPlotState } =
getContext<PlotContext>('svelteplot');

const plot = $derived(getPlotState());
Expand Down Expand Up @@ -258,16 +258,17 @@
if (options?.[channel] != null && out[channel] === undefined) {
// resolve value
const value = row[channel];

const scaled = usedScales[channel]
? scale === 'x'
? projectX(channel as 'x' | 'x1' | 'x2', plot.scales, value)
: scale === 'y'
? projectY(channel as 'y' | 'y1' | 'y1', plot.scales, value)
: plot.scales[scale].fn(value)
: scale === 'color' && !isValid(value)
? plot.options.color.unknown
: plot.scales[scale].fn(value)
: value;

out.valid = out.valid && isValid(value);
out.valid = out.valid && (scale === 'color' || isValid(value));

// apply dx/dy transform
out[channel] =
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Plot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
scales,
...restProps
})}
<svelte:boundary onerror={(err) => console.warn(err)}>
<svelte:boundary onerror={(err) => console.error(err)}>
<!-- implicit axes -->
{#if !hasProjection && !hasExplicitAxisX}
{#if options.axes && (options.x.axis === 'top' || options.x.axis === 'both')}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/Plot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
initialWidth: 500,
inset: 0,
colorScheme: 'turbo',
unknown: '#cccccc',
unknown: '#cccccc99',
dotRadius: 3,
frame: false,
axes: true,
Expand Down Expand Up @@ -450,7 +450,7 @@
padding: 0,
align: 0
},
color: { type: 'auto' },
color: { type: 'auto', unknown: DEFAULTS.unknown },
length: { type: 'linear' },
symbol: { type: 'ordinal' },
fx: { type: 'band', axis: 'top' },
Expand Down
13 changes: 12 additions & 1 deletion src/lib/helpers/scales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import isDataRecord from './isDataRecord.js';

import { createProjection } from './projection.js';
import { maybeInterval } from './autoTicks.js';
import { IS_SORTED } from 'svelteplot/transforms/sort.js';

/**
* compute the plot scales
Expand Down Expand Up @@ -183,7 +184,10 @@ export function createScale<T extends ScaleOptions>(
// we're deliberately checking for !== undefined and not for != null
// since the explicit sort transforms like shuffle will set the
// sort channel to null to we know that there's an explicit order
if (mark.channels.sort !== undefined) sortOrdinalDomain = false;
if ((name === 'x' || name === 'y') && mark.options[IS_SORTED] != undefined) {
sortOrdinalDomain = false;
}

for (const channel of mark.channels) {
// channelOptions can be passed as prop, but most often users will just
// pass the channel accessor or constant value, so we may need to wrap
Expand Down Expand Up @@ -278,6 +282,13 @@ export function createScale<T extends ScaleOptions>(
}
}

if ((name === 'x' || name === 'y') && scaleOptions.sort) {
sortOrdinalDomain = true;
}
if ((name === 'x' || name === 'y') && scaleOptions.sort === false) {
sortOrdinalDomain = false;
}

// construct domain from data values
const valueArr = [...dataValues.values(), ...(scaleOptions.domain || [])].filter(
(d) => d != null
Expand Down
2 changes: 1 addition & 1 deletion src/lib/marks/AxisX.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
const ticks: RawValue[] = $derived(
data.length > 0
? // use custom tick values if user passed any as prop
data
Array.from(new Set(data))
: // use custom scale tick values if user passed any as plot scale option
autoTicks(
plot.scales.x.type,
Expand Down
1 change: 1 addition & 0 deletions src/lib/marks/ColorLegend.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
.swatch {
display: inline-flex;
align-items: center;
column-gap: 0.3rem;
}
.item-label {
vertical-align: super;
Expand Down
4 changes: 1 addition & 3 deletions src/lib/marks/Dot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@
return d3Symbol(maybeSymbol(symbolType), size)();
}

const { getTestFacet } = getContext<FacetContext>('svelteplot/facet');
const { dotRadius } = getContext<PlotDefaults>('svelteplot/_defaults');
let testFacet = $derived(getTestFacet());

const args = $derived(
// todo: move sorting to Mark
Expand Down Expand Up @@ -84,7 +82,7 @@
defaults={{ r: dotRadius, symbol: 'circle' }}
{...args}>
{#snippet children({ mark, usedScales, scaledData })}
<g class="dots {className || ''}">
<g class="dot {className || ''}">
{#if canvas}
<DotCanvas data={scaledData} {mark} />
{:else}
Expand Down
55 changes: 55 additions & 0 deletions src/lib/transforms/sort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import { sort, shuffle } from './sort.js';
import type { DataRecord } from '$lib/types.js';

const data: DataRecord[] = [
{ A: 1, B: 7 },
{ A: 5, B: 4 },
{ A: 3, B: 3 }
];

const sortedByA = data.sort((a, b) => a.A - b.A);

describe('sort transform', () => {
it('does not sort if no sort channel is defined', () => {
expect(sort({ data }).data).toStrictEqual(data);
});

it('sort data by string accessor', () => {
expect(sort({ data, sort: 'A' }).data).toStrictEqual(sortedByA);
});

it('sort data by accessor function', () => {
expect(sort({ data, sort: (d) => d.A }).data).toStrictEqual(sortedByA);
});

it('sort data by comperator function', () => {
expect(sort({ data, sort: (a, b) => a.A - b.A }).data).toStrictEqual(sortedByA);
});

it('sort data by channel', () => {
expect(sort({ data, x: 'A', sort: { channel: 'x' } }).data).toStrictEqual(sortedByA);
});

it('sort data by channel descending', () => {
expect(
sort({ data, x: 'A', sort: { channel: 'x', order: 'descending' } }).data
).toStrictEqual(sortedByA.toReversed());
});

it('sort data by channel descending alternative syntax', () => {
expect(sort({ data, y: 'A', sort: { channel: '-y' } }).data).toStrictEqual(
sortedByA.toReversed()
);
});
});

describe('shuffle transform', () => {
it('shuffles the data', () => {
const shuffled = shuffle({ data }, { seed: 1 });
expect(shuffled.sort).toBe(null);
expect(shuffled.data).toHaveLength(data.length);
expect(shuffled.data).not.toStrictEqual(data);
expect(shuffled.data.sort((a, b) => a.A - b.A)).toStrictEqual(sortedByA);
});
});
46 changes: 30 additions & 16 deletions src/lib/transforms/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { shuffler } from 'd3-array';
import { randomLcg } from 'd3-random';

export const SORT_KEY = Symbol('sortKey');
export const IS_SORTED = Symbol('isSorted');

export function sort(
{ data, ...channels }: TransformArg<DataRecord>,
Expand All @@ -21,23 +22,34 @@ export function sort(
sort.channel = sort.channel.substring(1);
sort.order = 'descending';
}
// if sort is a function that does not take exactly one argument, we treat it
// as comparator function, as you would pass to array.sort
const isComparator = typeof channels.sort === 'function' && channels.sort.length !== 1;

// sort data
return {
data: data
.map((d) => ({
...d,
[SORT_KEY]: resolveChannel('sort', d, { ...channels, sort })
}))
.toSorted(
(a, b) =>
(a[SORT_KEY] > b[SORT_KEY] ? 1 : a[SORT_KEY] < b[SORT_KEY] ? -1 : 0) *
(options.reverse || (isDataRecord(sort) && sort?.order === 'descending')
? -1
: 1)
)
.map(({ [SORT_KEY]: a, ...rest }) => rest),
data: isComparator
? data.toSorted(channels.sort as (a: DataRecord, b: DataRecord) => number)
: data
.map((d) => ({
...d,
[SORT_KEY]: resolveChannel('sort', d, { ...channels, sort }) as
| number
| Date
| string
}))
.toSorted(
(a, b) =>
(a[SORT_KEY] > b[SORT_KEY] ? 1 : a[SORT_KEY] < b[SORT_KEY] ? -1 : 0) *
(options.reverse ||
(isDataRecord(sort) && sort?.order === 'descending')
? -1
: 1)
)
.map(({ [SORT_KEY]: a, ...rest }) => rest),

...channels,
[IS_SORTED]: sort,
// set the sort channel to null to disable the implicit alphabetical
// ordering of ordinal domains, and also to avoid double sorting in case
// this transform is used "outside" a mark
Expand All @@ -51,7 +63,7 @@ export function sort(
}

/**
* reverses the data row order
* shuffles the data row order
*/
export function shuffle(
{ data, ...channels }: TransformArg<DataRow[]>,
Expand All @@ -64,7 +76,8 @@ export function shuffle(
...channels,
// set the sort channel to null to disable the implicit
// alphabetical ordering of ordinal domains
sort: null
sort: null,
[IS_SORTED]: true
};
}

Expand All @@ -77,6 +90,7 @@ export function reverse({ data, ...channels }: TransformArg<DataRow[]>) {
...channels,
// set the sort channel to null to disable the implicit
// alphabetical ordering of ordinal domains
sort: null
sort: null,
[IS_SORTED]: true
};
}
14 changes: 12 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export type PlotDefaults = {
css: (d: string) => string | undefined;
};

export type GenericMarkOptions = Record<string, any>;
export type GenericMarkOptions = Record<string | symbol, any>;

export type DataRecord = Record<string | symbol, RawValue> & {
___orig___?: RawValue | [RawValue, RawValue];
Expand Down Expand Up @@ -657,6 +657,16 @@ export type BaseMarkProps = Partial<{
dy: ConstantAccessor<number>;
fill: ConstantAccessor<string>;
fillOpacity: ConstantAccessor<number>;
sort:
| string
| ConstantAccessor<RawValue>
| ((a: RawValue, b: RawValue) => number)
| {
/** sort data using an already defined channel */
channel: string;
/** sort order */
order?: 'ascending' | 'descending';
};
stroke: ConstantAccessor<string>;
strokeWidth: ConstantAccessor<number>;
strokeOpacity: ConstantAccessor<number>;
Expand Down Expand Up @@ -731,7 +741,7 @@ export type Channels = Record<
ChannelAccessor | ConstantAccessor<string | number | boolean | symbol>
>;

export type TransformArg<K> = Channels & { data: K[] };
export type TransformArg<K> = Channels & BaseMarkProps & { data: K[] };
export type MapArg<K> = Channels & { data: K[] };

export type TransformArgsRow = Partial<Channels> & { data: DataRow[] };
Expand Down
7 changes: 5 additions & 2 deletions src/routes/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ heroImage: /logo.png
tagline: A Svelte-native visualization framework based on the layered grammar of graphics principles.
actions:
- label: Getting started
type: flat
type: primary
to: /getting-started
- label: Why SveltePlot?
to: /why-svelteplot
type: primary
type: flat
- label: Examples
to: /examples
type: flat
_features:
- title: Marks
description: SveltePlot comes with a powerful set of built-in marks for building for your visualizations
Expand Down
6 changes: 4 additions & 2 deletions src/routes/examples/+layout.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { loadDatasets, loadJSON } from '$lib/helpers/data.js';
import type { PageLoad } from './$types.js';
import type { PageServerLoad } from '../$types';

export const ssr = true;

export const load: PageLoad = async ({ fetch }) => {
export const load: PageServerLoad = async ({ fetch }) => {
return {
data: {
world: await loadJSON(fetch, 'countries-110m'),
us: await loadJSON(fetch, 'us-counties-10m'),
...(await loadDatasets(
[
'aapl',
'alphabet',
'beagle',
'bls',
'co2',
'crimea',
'driving',
'languages',
'penguins',
'riaa',
'stateage',
Expand Down
5 changes: 5 additions & 0 deletions src/routes/examples/bar/_index.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script module>
export let title = 'Bar';
</script>

<h1>Bar examples</h1>
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