diff --git a/package.json b/package.json index 31f01918..ced5f0d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelteplot", - "version": "0.2.10", + "version": "0.3.0", "license": "ISC", "author": { "name": "Gregor Aisch", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..6b0d7e3d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - puppeteer diff --git a/screenshot-examples.js b/screenshot-examples.js index fe6dc989..593ec016 100644 --- a/screenshot-examples.js +++ b/screenshot-examples.js @@ -94,7 +94,9 @@ const takeScreenshot = async (page, urlPath, outputPath, isDarkMode = false) => const finalOutputPath = outputPath.replace('.png', `${themeSuffix}.png`); // Wait for the Plot component to be rendered - await page.waitForSelector('.content > figure.svelteplot', { timeout: 10000 }); + await page.waitForSelector('.content figure.svelteplot ', { + timeout: 10000 + }); // Toggle dark mode if needed if (isDarkMode) { @@ -112,7 +114,7 @@ const takeScreenshot = async (page, urlPath, outputPath, isDarkMode = false) => // Get the Plot SVG element const elementHandle = await page.evaluateHandle(() => - document.querySelector('.content > figure.svelteplot > .plot-body > svg') + document.querySelector('.content .screenshot') ); // Take a screenshot of the element diff --git a/src/lib/Plot.svelte b/src/lib/Plot.svelte index fb54112b..2166bd10 100644 --- a/src/lib/Plot.svelte +++ b/src/lib/Plot.svelte @@ -12,7 +12,7 @@ @@ -78,7 +95,7 @@ required={['x1', 'x2', 'y1', 'y2']} channels={['x1', 'y1', 'x2', 'y2', 'opacity', 'stroke', 'strokeOpacity']} {...args}> - {#snippet children({ mark, usedScales, scaledData })} + {#snippet children({ usedScales, scaledData })} {@const sweep = maybeSweep(args.sweep)} {#each scaledData as d, i (i)} @@ -86,8 +103,8 @@ {@const inset = resolveProp(args.inset, d.datum, 0)} {@const insetStart = resolveProp(args.insetStart, d.datum)} {@const insetEnd = resolveProp(args.insetEnd, d.datum)} - {@const headAngle = resolveProp(args.headAngle, d.datum, 60)} - {@const headLength = resolveProp(args.headLength, d.datum, 8)} + {@const headAngle = resolveProp(args.headAngle, d.datum)} + {@const headLength = resolveProp(args.headLength, d.datum)} {@const bend = resolveProp(args.bend, d.datum, 0)} {@const strokeWidth = resolveProp(args.strokeWidth, d.datum, 1)} {@const arrPath = arrowPath( diff --git a/src/lib/marks/AxisX.svelte b/src/lib/marks/AxisX.svelte index d1256c48..8767c59b 100644 --- a/src/lib/marks/AxisX.svelte +++ b/src/lib/marks/AxisX.svelte @@ -27,16 +27,11 @@ ticks?: number | string | RawValue[]; /** set to false or null to disable tick labels */ text: boolean | null; - } & XOR< - { - /** approximate number of ticks to be generated */ - tickCount?: number; - }, - { - /** approximate number of pixels between generated ticks */ - tickSpacing?: number; - } - >; + /** approximate number of ticks to be generated */ + tickCount?: number; + /** approximate number of pixels between generated ticks */ + tickSpacing?: number; + }; + - + diff --git a/src/lib/marks/BrushY.svelte b/src/lib/marks/BrushY.svelte index e16e45a5..99e6c3b1 100644 --- a/src/lib/marks/BrushY.svelte +++ b/src/lib/marks/BrushY.svelte @@ -4,8 +4,14 @@ - + diff --git a/src/lib/marks/Cell.svelte b/src/lib/marks/Cell.svelte index dfc9f22d..d8caf2fc 100644 --- a/src/lib/marks/Cell.svelte +++ b/src/lib/marks/Cell.svelte @@ -8,7 +8,8 @@ DataRecord, BaseMarkProps, BaseRectMarkProps, - ChannelAccessor + ChannelAccessor, + PlotDefaults } from '../types.js'; export type CellMarkProps = BaseMarkProps & @@ -29,7 +30,20 @@ import { isValid } from '../helpers/isValid.js'; import RectPath from './helpers/RectPath.svelte'; - let { data = [{}], class: className = null, ...options }: CellMarkProps = $props(); + let markProps: CellMarkProps = $props(); + + const DEFAULTS = { + ...getContext('svelteplot/_defaults').cell + }; + + const { + data = [{}], + class: className = '', + ...options + }: CellMarkProps = $derived({ + ...DEFAULTS, + ...markProps + }); const { getPlotState } = getContext('svelteplot'); const plot = $derived(getPlotState()); diff --git a/src/lib/marks/ColorLegend.svelte b/src/lib/marks/ColorLegend.svelte index a6019388..47c3c257 100644 --- a/src/lib/marks/ColorLegend.svelte +++ b/src/lib/marks/ColorLegend.svelte @@ -11,14 +11,14 @@ import { range as d3Range, extent } from 'd3-array'; import { maybeSymbol } from '$lib/helpers/symbols.js'; - import type { DefaultOptions, PlotContext } from '../types.js'; + import type { PlotDefaults, PlotContext } from '../types.js'; let { class: className = null }: ColorLegendMarkProps = $props(); const { getPlotState } = getContext('svelteplot'); const plot = $derived(getPlotState()); - const DEFAULTS = getContext>('svelteplot/_defaults'); + const DEFAULTS = getContext>('svelteplot/_defaults'); const legendTitle = $derived(plot.options.color.label); const scaleType = $derived(plot.scales.color.type); diff --git a/src/lib/marks/Dot.svelte b/src/lib/marks/Dot.svelte index 2a4019be..c69b95bd 100644 --- a/src/lib/marks/Dot.svelte +++ b/src/lib/marks/Dot.svelte @@ -35,13 +35,19 @@ import { addEventHandlers } from './helpers/events.js'; import Anchor from './helpers/Anchor.svelte'; - let { + const DEFAULTS = { + ...getContext('svelteplot/_defaults').dot + }; + + let markProps: DotMarkProps = $props(); + + const { data = [{}], canvas = false, class: className = '', dotClass = null, ...options - }: DotMarkProps = $props(); + }: DotMarkProps = $derived({ ...DEFAULTS, ...markProps }); const { getPlotState } = getContext('svelteplot'); const plot = $derived(getPlotState()); @@ -50,8 +56,6 @@ return d3Symbol(maybeSymbol(symbolType), size)(); } - const { dotRadius } = getContext('svelteplot/_defaults'); - const args = $derived( // todo: move sorting to Mark sort( @@ -80,7 +84,7 @@ 'fillOpacity', 'strokeOpacity' ]} - defaults={{ r: dotRadius, symbol: 'circle' }} + defaults={{ r: 3, symbol: 'circle' }} {...args}> {#snippet children({ mark, usedScales, scaledData })} diff --git a/src/lib/marks/Frame.svelte b/src/lib/marks/Frame.svelte index 35098650..05bf7d01 100644 --- a/src/lib/marks/Frame.svelte +++ b/src/lib/marks/Frame.svelte @@ -28,19 +28,38 @@ diff --git a/src/lib/marks/GridX.svelte b/src/lib/marks/GridX.svelte index 30940f9c..0a8a7b93 100644 --- a/src/lib/marks/GridX.svelte +++ b/src/lib/marks/GridX.svelte @@ -11,13 +11,27 @@ - + diff --git a/src/lib/marks/Spike.svelte b/src/lib/marks/Spike.svelte index 35218f9f..bceda7de 100644 --- a/src/lib/marks/Spike.svelte +++ b/src/lib/marks/Spike.svelte @@ -8,16 +8,26 @@ - + diff --git a/src/lib/marks/Text.svelte b/src/lib/marks/Text.svelte index 7f84e330..1905e726 100644 --- a/src/lib/marks/Text.svelte +++ b/src/lib/marks/Text.svelte @@ -39,13 +39,30 @@ DataRecord, BaseMarkProps, ConstantAccessor, - ChannelAccessor + ChannelAccessor, + PlotDefaults } from '../types.js'; import { resolveProp, resolveStyles } from '../helpers/resolve.js'; import Mark from '../Mark.svelte'; import { sort } from '$lib/index.js'; - let { data = [{}], class: className = null, ...options }: TextMarkProps = $props(); + const DEFAULTS = { + fontSize: 12, + fontWeight: 500, + strokeWidth: 1.6, + ...getContext('svelteplot/_defaults').text + }; + + let markProps: TextMarkProps = $props(); + + const { + data = [{}], + class: className = '', + ...options + }: TextMarkProps = $derived({ + ...DEFAULTS, + ...markProps + }); const { getPlotState } = getContext('svelteplot'); let plot = $derived(getPlotState()); @@ -54,7 +71,7 @@ bottom: 'auto', middle: 'central', top: 'hanging' - }; + } as const; const args = $derived( sort({ @@ -77,9 +94,10 @@ 'strokeOpacity', 'fillOpacity' ]} + required={['x', 'y']} {...args}> {#snippet children({ mark, scaledData, usedScales })} - + {#each scaledData as d, i (i)} {#if d.valid} {@const title = resolveProp(args.title, d.datum, '')} diff --git a/src/lib/marks/TickX.svelte b/src/lib/marks/TickX.svelte index 0ffdf902..65995799 100644 --- a/src/lib/marks/TickX.svelte +++ b/src/lib/marks/TickX.svelte @@ -31,7 +31,8 @@ BaseMarkProps, ChannelAccessor, DataRow, - FacetContext + FacetContext, + PlotDefaults } from '../types.js'; import { recordizeX } from '$lib/index.js'; import { projectX, projectY } from '../helpers/scales.js'; @@ -41,7 +42,19 @@ const { getPlotState } = getContext('svelteplot'); let plot = $derived(getPlotState()); - let { data = [{}], ...options }: TickXMarkProps = $props(); + let markProps: TickXMarkProps = $props(); + const DEFAULTS = { + ...getContext('svelteplot/_defaults').tick, + ...getContext('svelteplot/_defaults').tickX + }; + const { + data = [{}], + class: className = '', + ...options + }: TickXMarkProps = $derived({ + ...DEFAULTS, + ...markProps + }); let args = $derived(recordizeX({ data, ...options }, { withIndex: false })); diff --git a/src/lib/marks/TickY.svelte b/src/lib/marks/TickY.svelte index 99d503ed..d63b5cb6 100644 --- a/src/lib/marks/TickY.svelte +++ b/src/lib/marks/TickY.svelte @@ -32,7 +32,8 @@ ChannelAccessor, DataRow, FacetContext, - ConstantAccessor + ConstantAccessor, + PlotDefaults } from '../types.js'; import { recordizeY } from '$lib/index.js'; import { projectX, projectY } from '../helpers/scales.js'; @@ -42,7 +43,19 @@ const { getPlotState } = getContext('svelteplot'); let plot = $derived(getPlotState()); - let { data = [{}], ...options }: TickYMarkProps = $props(); + let markProps: TickYMarkProps = $props(); + const DEFAULTS = { + ...getContext('svelteplot/_defaults').tick, + ...getContext('svelteplot/_defaults').tickY + }; + const { + data = [{}], + class: className = '', + ...options + }: TickYMarkProps = $derived({ + ...DEFAULTS, + ...markProps + }); let args = $derived(recordizeY({ data, ...options }, { withIndex: false })); diff --git a/src/lib/marks/Vector.svelte b/src/lib/marks/Vector.svelte index fdeea203..23c8a19a 100644 --- a/src/lib/marks/Vector.svelte +++ b/src/lib/marks/Vector.svelte @@ -9,7 +9,8 @@ BaseMarkProps, ConstantAccessor, ChannelAccessor, - FacetContext + FacetContext, + PlotDefaults } from '../types.js'; type D3Path = ReturnType; @@ -53,19 +54,26 @@ const defaultRadius = 3.5; // The size of the arrowhead is proportional to its length, but we still allow - // the relative size of the head to be controlled via the mark’s width option; + // the relative size of the head to be controlled via the mark's width option; // doubling the default radius will produce an arrowhead that is twice as big. - // That said, we’ll probably want a arrow with a fixed head size, too. + // That said, we'll probably want a arrow with a fixed head size, too. const wingRatio = defaultRadius * 5; - let { + let markProps: VectorMarkProps = $props(); + const DEFAULTS = { + ...getContext('svelteplot/_defaults').vector + }; + const { data = [{}], canvas, shape = 'arrow', anchor = 'middle', r = defaultRadius, ...options - }: VectorMarkProps = $props(); + }: VectorMarkProps = $derived({ + ...DEFAULTS, + ...markProps + }); const { getPlotState } = getContext('svelteplot'); const plot = $derived(getPlotState()); diff --git a/src/lib/types.ts b/src/lib/types.ts index e2a58fb9..8e771b61 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,6 +4,37 @@ import type { MouseEventHandler } from 'svelte/elements'; import type { MarkerShape } from './marks/helpers/Marker.svelte'; import type { Writable } from 'svelte/store'; import type * as CSS from 'csstype'; +import type { AreaMarkProps } from './marks/Area.svelte'; +import type { ArrowMarkProps } from './marks/Arrow.svelte'; +import type { AxisXMarkProps } from './marks/AxisX.svelte'; +import type { AxisYMarkProps } from './marks/AxisY.svelte'; +import type { BarXMarkProps } from './marks/BarX.svelte'; +import type { CellMarkProps } from './marks/Cell.svelte'; +import type { DotMarkProps } from './marks/Dot.svelte'; +import type { FrameMarkProps } from './marks/Frame.svelte'; +import type { GeoMarkProps } from './marks/Geo.svelte'; +import type { GraticuleMarkProps } from './marks/Graticule.svelte'; +import type { LineMarkProps } from './marks/Line.svelte'; +import type { LinkMarkProps } from './marks/Link.svelte'; +import type { RectMarkProps } from './marks/Rect.svelte'; +import type { RuleXMarkProps } from './marks/RuleX.svelte'; +import type { SphereMarkProps } from './marks/Sphere.svelte'; +import type { SpikeMarkProps } from './marks/Spike.svelte'; +import type { TextMarkProps } from './marks/Text.svelte'; +import type { TickXMarkProps } from './marks/TickX.svelte'; +import type { VectorMarkProps } from './marks/Vector.svelte'; +import type { BrushMarkProps } from './marks/Brush.svelte'; +import type { BrushXMarkProps } from './marks/BrushX.svelte'; +import type { BrushYMarkProps } from './marks/BrushY.svelte'; +import type { RectXMarkProps } from './marks/RectX.svelte'; +import type { RectYMarkProps } from './marks/RectY.svelte'; +import type { RuleYMarkProps } from './marks/RuleY.svelte'; +import type { TickYMarkProps } from './marks/TickY.svelte'; +import type { GridYMarkProps } from './marks/GridY.svelte'; +import type { GridXMarkProps } from './marks/GridX.svelte'; +import type { PointerMarkProps } from './marks/Pointer.svelte'; +import type { BoxXMarkProps } from './marks/BoxX.svelte'; +import type { BoxYMarkProps } from './marks/BoxY.svelte'; export type MarkType = | 'area' @@ -399,52 +430,6 @@ export type PlotOptions = { css: (d: string) => string | undefined; }; -export type PlotDefaults = { - axisXAnchor: AxisXAnchor; - axisYAnchor: AxisYAnchor; - xTickSpacing: number; - yTickSpacing: number; - height: number; - inset: number; - colorScheme: ColorScheme | string[]; - categoricalColorScheme: ColorScheme | string[]; - dotRadius: number; - /** - * for computing the automatic height based on the number of - * domain items in a point scale - */ - pointScaleHeight: number; - /** - * for computing the automatic height based on the number of - * domain items in a band scale - */ - bandScaleHeight: number; - /** - * add frame to plots by default - */ - frame: boolean; - grid: boolean; - axes: boolean; - /** - * initial width of the plot before measuring the actual width - */ - initialWidth: number; - /** - * locale, used for automatic axis ticks - */ - locale: string; - /** - * default number format for axis ticks - */ - numberFormat: Intl.NumberFormatOptions; - markerDotRadius: number; - /** - * fallback color to be used for null/NA - */ - unknown: string; - css: (d: string) => string | undefined; -}; - export type GenericMarkOptions = Record; export type DataRecord = Record & { @@ -636,27 +621,27 @@ export type LinkableMarkProps = { /** * if set, the mark element will be wrapped in a link element */ - href: ConstantAccessor; + href?: ConstantAccessor; /** * the relationship of the target object to the link object (e.g. "noopener") * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#rel */ - rel: ConstantAccessor; + rel?: ConstantAccessor; /** * the link target mime type, e.g. "text/csv" * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#type */ - type: ConstantAccessor; + type?: ConstantAccessor; /** * the target of the link, e.g. "_blank" or "_self" * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#target */ - target: ConstantAccessor<'_self' | '_blank' | '_parent' | '_top' | string>; + target?: ConstantAccessor<'_self' | '_blank' | '_parent' | '_top' | string>; /** * if set to true, the link will be downloaded instead of navigating to it * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download */ - download: ConstantAccessor; + download?: ConstantAccessor; // allow data-sveltekit-* attributes on the link element, e.g. data-sveltekit-reload [key: `data-sveltekit-${string}`]: string | boolean; }; @@ -853,11 +838,13 @@ export type AutoMarginStores = { autoMarginBottom: Writable>; }; +type IgnoreDefaults = 'data' | 'facet' | ChannelName | 'title' | 'automatic' | 'children'; + /** * these are the default options for the plot marks that can be set using * the 'svelteplot/defaults' context. */ -export type DefaultOptions = { +export type PlotDefaults = { /** * default plot height */ @@ -867,41 +854,32 @@ export type DefaultOptions = { */ inset: number; /** - * default tick line length - */ - tickSize: number; - /** - * default padding between tick line and tick label - */ - tickPadding: number; - /** - * default font size for tick labels - */ - tickFontSize: number; - /** - * default anchor for x axis + * default color scheme */ - axisXAnchor: 'bottom' | 'top'; + colorScheme: ColorScheme; + categoricalColorScheme: ColorScheme | string[]; /** - * default anchor for y axis + * fallback color to be used for null/NA */ - axisYAnchor: 'left' | 'right'; + unknown: string; /** - * default spacing between ticks in AxisX and GridX + * optional @emotion/css function to style the plot */ - xTickSpacing: number; + css: (d: string) => string | undefined; /** - * default spacing between ticks in AxisY and GridY + * for computing the automatic height based on the number of + * domain items in a point scale */ - yTickSpacing: number; + pointScaleHeight: number; /** - * default color scheme + * for computing the automatic height based on the number of + * domain items in a band scale */ - colorScheme: ColorScheme; + bandScaleHeight: number; /** - * default step for graticule, in degrees + * initial width of the plot before measuring the actual width */ - graticuleStep: number; + initialWidth: number; /** * locale, used for automatic axis ticks */ @@ -914,6 +892,171 @@ export type DefaultOptions = { * default dot radius for line markers, used in dot, circle, and circle-stroke markers */ markerDotRadius: number; + /** + * default props for area marks, applied to area, areaX, and areaY marks + */ + area: Partial>; + /** + * default props for areaX marks + */ + areaX: Partial>; + /** + * default props for areaY marks + */ + areaY: Partial>; + /** + * default props for arrow marks + */ + arrow: Partial>; + /** + * default props for axis marks, applied to both axisX and axisY marks + */ + axis: Partial< + Omit< + AxisXMarkProps, + 'data' | 'facet' | ChannelName | 'facetAnchor' | 'labelAnchor' | 'anchor' + > & { implicit: boolean } + >; + /** + * default props for axisX marks + */ + axisX: Partial & { implicit: boolean }>; + /** + * default props for axisY marks + */ + axisY: Partial & { implicit: boolean }>; + /** + * default props for bar marks, applied to both barX and barY marks + */ + bar: Partial>; + /** + * default props for barX marks + */ + barX: Partial>; + /** + * default props for barY marks + */ + barY: Partial>; + /** + * default props for box marks, applied to boxX and boxY marks + */ + box: Partial>; + /** + * default props for boxX marks + */ + boxX: Partial>; + /** + * default props for boxY marks + */ + boxY: Partial>; + /** + * default props for brush marks, applied to brush, brushX and brushY marks + */ + brush: Partial>; + /** + * default props for brushX marks + */ + brushX: Partial>; + /** + * default props for brushY marks + */ + brushY: Partial>; + /** + * default props for cell marks + */ + cell: Partial>; + /** + * default props for dot marks + */ + dot: Partial>; + /** + * default props for frame marks + */ + frame: Partial; + /** + * default props for geo marks + */ + geo: Partial>; + /** + * default props for graticule marks + */ + graticule: Partial>; + /** + * default props for grid marks, applied to both gridX and gridY marks + */ + grid: Partial & { implicit: boolean }>; + /** + * default props for gridX marks + */ + gridX: Partial & { implicit: boolean }>; + /** + * default props for gridY marks + */ + gridY: Partial & { implicit: boolean }>; + /** + * default props for line marks + */ + line: Partial>; + /** + * default props for link marks + */ + link: Partial>; + /** + * default props for pointer marks + */ + pointer: Partial>; + /** + * default props for rect marks, applied to rect and rectX marks + */ + rect: Partial>; + /** + * default props for rectX marks + */ + rectX: Partial>; + /** + * default props for rectY marks + */ + rectY: Partial>; + /** + * default props for rule marks + */ + rule: Partial>; + /** + * default props for rule marks + */ + ruleX: Partial>; + /** + * default props for rule marks + */ + ruleY: Partial>; + /** + * default props for sphere marks + */ + sphere: Partial; + /** + * default props for spike marks + */ + spike: Partial>; + /** + * default props for text marks + */ + text: Partial>; + /** + * default props for tick marks, applied to tickX and tickY marks + */ + tick: Partial>; + /** + * default props for tickX marks + */ + tickX: Partial>; + /** + * default props for tickY marks + */ + tickY: Partial>; + /** + * default props for vector marks + */ + vector: Partial>; }; export type MapIndexObject = { diff --git a/src/lib/ui/ExamplesGrid.svelte b/src/lib/ui/ExamplesGrid.svelte index 99ab7d33..e16a8484 100644 --- a/src/lib/ui/ExamplesGrid.svelte +++ b/src/lib/ui/ExamplesGrid.svelte @@ -36,7 +36,7 @@ border: 1px solid #88888822; border-radius: 2px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); - padding: 1.5ex; + padding: 1.5ex 1.5ex 0.4ex 1.5ex; } &:hover { diff --git a/src/routes/examples/+layout.ts b/src/routes/examples/+layout.ts index 5fa96da7..4c63766d 100644 --- a/src/routes/examples/+layout.ts +++ b/src/routes/examples/+layout.ts @@ -19,6 +19,7 @@ export const load: PageServerLoad = async ({ fetch }) => { 'co2', 'crimea', 'driving', + 'metros', 'languages', 'penguins', 'riaa', diff --git a/src/routes/examples/+page.svelte b/src/routes/examples/+page.svelte index 160b6564..c22d8bc4 100644 --- a/src/routes/examples/+page.svelte +++ b/src/routes/examples/+page.svelte @@ -15,7 +15,7 @@ ]; -

@@ -87,7 +100,9 @@ ].title}