svelteplot 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Mark.svelte +7 -0
- package/dist/Plot.svelte +10 -2
- package/dist/core/FacetAxes.svelte +2 -2
- package/dist/core/Plot.svelte +7 -11
- package/dist/helpers/getBaseStyles.d.ts +2 -0
- package/dist/helpers/getBaseStyles.js +8 -0
- package/dist/helpers/removeIdenticalLines.js +3 -2
- package/dist/helpers/scales.js +2 -2
- package/dist/helpers/wordwrap.d.ts +14 -0
- package/dist/helpers/wordwrap.js +129 -0
- package/dist/marks/AxisX.svelte +2 -1
- package/dist/marks/AxisX.svelte.d.ts +1 -0
- package/dist/marks/Brush.svelte +44 -4
- package/dist/marks/Text.svelte.d.ts +1 -1
- package/dist/marks/WaffleX.svelte +115 -0
- package/dist/marks/WaffleX.svelte.d.ts +102 -0
- package/dist/marks/WaffleY.svelte +119 -0
- package/dist/marks/WaffleY.svelte.d.ts +100 -0
- package/dist/marks/helpers/BaseAxisX.svelte +31 -3
- package/dist/marks/helpers/BaseAxisX.svelte.d.ts +2 -0
- package/dist/marks/helpers/waffle.d.ts +58 -0
- package/dist/marks/helpers/waffle.js +194 -0
- package/dist/marks/index.d.ts +3 -1
- package/dist/marks/index.js +3 -1
- package/dist/transforms/group.js +11 -5
- package/dist/types/data.d.ts +1 -0
- package/dist/types/mark.d.ts +1 -1
- package/dist/types/plot.d.ts +19 -5
- package/dist/types/scale.d.ts +8 -0
- package/package.json +14 -14
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values.
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts" generics="Datum extends DataRecord">
|
|
6
|
+
import type {
|
|
7
|
+
DataRecord,
|
|
8
|
+
ChannelAccessor,
|
|
9
|
+
BaseMarkProps,
|
|
10
|
+
LinkableMarkProps,
|
|
11
|
+
PlotContext,
|
|
12
|
+
BorderRadius
|
|
13
|
+
} from '../types';
|
|
14
|
+
import { wafflePolygon, type WaffleOptions } from './helpers/waffle';
|
|
15
|
+
import { getPlotDefaults } from '../hooks/plotDefaults';
|
|
16
|
+
import { getContext } from 'svelte';
|
|
17
|
+
import { intervalY, recordizeY, sort, stackY } from '../transforms';
|
|
18
|
+
import Mark from '../Mark.svelte';
|
|
19
|
+
import { resolveProp, resolveStyles } from '../helpers/resolve';
|
|
20
|
+
import { roundedRect } from '../helpers/roundedRect';
|
|
21
|
+
import GroupMultiple from './helpers/GroupMultiple.svelte';
|
|
22
|
+
|
|
23
|
+
interface WaffleYMarkProps
|
|
24
|
+
extends BaseMarkProps<Datum>,
|
|
25
|
+
LinkableMarkProps<Datum>,
|
|
26
|
+
WaffleOptions<Datum> {
|
|
27
|
+
data?: Datum[];
|
|
28
|
+
/**
|
|
29
|
+
* bound to a babd scale
|
|
30
|
+
*/
|
|
31
|
+
x?: ChannelAccessor<Datum>;
|
|
32
|
+
/**
|
|
33
|
+
* bound to a quantitative scale
|
|
34
|
+
*/
|
|
35
|
+
y?: ChannelAccessor<Datum>;
|
|
36
|
+
/**
|
|
37
|
+
* bound to a quantitative scale
|
|
38
|
+
*/
|
|
39
|
+
y1?: ChannelAccessor<Datum>;
|
|
40
|
+
/**
|
|
41
|
+
* bound to a quantitative scale
|
|
42
|
+
*/
|
|
43
|
+
y2?: ChannelAccessor<Datum>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULTS = {
|
|
47
|
+
...getPlotDefaults().waffle,
|
|
48
|
+
...getPlotDefaults().waffleY
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let markProps: WaffleYMarkProps = $props();
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
data = [{} as Datum],
|
|
55
|
+
class: className = null,
|
|
56
|
+
stack,
|
|
57
|
+
symbol = null,
|
|
58
|
+
...options
|
|
59
|
+
}: WaffleYMarkProps = $derived({ ...DEFAULTS, ...markProps });
|
|
60
|
+
|
|
61
|
+
const { getPlotState } = getContext<PlotContext>('svelteplot');
|
|
62
|
+
const plot = $derived(getPlotState());
|
|
63
|
+
|
|
64
|
+
const args = $derived(
|
|
65
|
+
stackY(
|
|
66
|
+
intervalY(
|
|
67
|
+
// by default, sort by y channel (the ordinal labels)
|
|
68
|
+
sort(recordizeY({ data, ...options })),
|
|
69
|
+
{ plot }
|
|
70
|
+
),
|
|
71
|
+
stack
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<Mark
|
|
77
|
+
type="waffleY"
|
|
78
|
+
requiredScales={{ x: ['band'] }}
|
|
79
|
+
channels={['y1', 'y2', 'x', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
|
|
80
|
+
{...args}>
|
|
81
|
+
{#snippet children({ mark, usedScales, scaledData })}
|
|
82
|
+
{@const wafflePoly = wafflePolygon('y', args, plot.scales)}
|
|
83
|
+
{#each scaledData as d, i (i)}
|
|
84
|
+
{@const [style, styleClass] = resolveStyles(
|
|
85
|
+
plot,
|
|
86
|
+
d,
|
|
87
|
+
args,
|
|
88
|
+
args.stroke && !args.fill ? 'stroke' : 'fill',
|
|
89
|
+
usedScales
|
|
90
|
+
)}
|
|
91
|
+
{@const borderRadius = resolveProp(args.borderRadius, d?.datum, 0) as BorderRadius}
|
|
92
|
+
{@const hasBorderRadius =
|
|
93
|
+
(typeof borderRadius === 'number' && borderRadius > 0) ||
|
|
94
|
+
(typeof borderRadius === 'object' &&
|
|
95
|
+
Math.max(
|
|
96
|
+
borderRadius.topRight ?? 0,
|
|
97
|
+
borderRadius.bottomRight ?? 0,
|
|
98
|
+
borderRadius.topLeft ?? 0,
|
|
99
|
+
borderRadius.bottomLeft ?? 0
|
|
100
|
+
) > 0)}
|
|
101
|
+
{@const { pattern, rect, path } = wafflePoly(d)}
|
|
102
|
+
<g class={['waffle-y', className]}>
|
|
103
|
+
<pattern {...pattern}>
|
|
104
|
+
{#if symbol}
|
|
105
|
+
{@render symbol({ ...rect, style, styleClass, datum: d.datum })}
|
|
106
|
+
{:else if hasBorderRadius}
|
|
107
|
+
<path
|
|
108
|
+
d={roundedRect(rect.x, rect.y, rect.width, rect.height, borderRadius)}
|
|
109
|
+
{style}
|
|
110
|
+
class={styleClass} />
|
|
111
|
+
{:else}
|
|
112
|
+
<rect {style} class={styleClass} {...rect} />
|
|
113
|
+
{/if}
|
|
114
|
+
</pattern>
|
|
115
|
+
<path {...path} />
|
|
116
|
+
</g>
|
|
117
|
+
{/each}
|
|
118
|
+
{/snippet}
|
|
119
|
+
</Mark>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { DataRecord, ChannelAccessor, LinkableMarkProps } from '../types';
|
|
2
|
+
import { type WaffleOptions } from './helpers/waffle';
|
|
3
|
+
declare class __sveltets_Render<Datum extends DataRecord> {
|
|
4
|
+
props(): Partial<{
|
|
5
|
+
filter: import("../types").ConstantAccessor<boolean, Datum>;
|
|
6
|
+
facet: "auto" | "include" | "exclude";
|
|
7
|
+
fx: ChannelAccessor<Datum>;
|
|
8
|
+
fy: ChannelAccessor<Datum>;
|
|
9
|
+
dx: import("../types").ConstantAccessor<number, Datum>;
|
|
10
|
+
dy: import("../types").ConstantAccessor<number, Datum>;
|
|
11
|
+
dodgeX: import("../transforms/dodge").DodgeXOptions;
|
|
12
|
+
dodgeY: import("../transforms/dodge").DodgeYOptions;
|
|
13
|
+
fill: ChannelAccessor<Datum>;
|
|
14
|
+
fillOpacity: import("../types").ConstantAccessor<number, Datum>;
|
|
15
|
+
sort: ((a: import("../types").RawValue, b: import("../types").RawValue) => number) | {
|
|
16
|
+
channel: string;
|
|
17
|
+
order?: "ascending" | "descending";
|
|
18
|
+
} | import("../types").ConstantAccessor<import("../types").RawValue, Datum>;
|
|
19
|
+
stroke: ChannelAccessor<Datum>;
|
|
20
|
+
strokeWidth: import("../types").ConstantAccessor<number, Datum>;
|
|
21
|
+
strokeOpacity: import("../types").ConstantAccessor<number, Datum>;
|
|
22
|
+
strokeLinejoin: import("../types").ConstantAccessor<import("csstype").Property.StrokeLinejoin, Datum>;
|
|
23
|
+
strokeLinecap: import("../types").ConstantAccessor<import("csstype").Property.StrokeLinecap, Datum>;
|
|
24
|
+
strokeMiterlimit: import("../types").ConstantAccessor<number, Datum>;
|
|
25
|
+
opacity: ChannelAccessor<Datum>;
|
|
26
|
+
strokeDasharray: import("../types").ConstantAccessor<string, Datum>;
|
|
27
|
+
strokeDashoffset: import("../types").ConstantAccessor<number, Datum>;
|
|
28
|
+
mixBlendMode: import("../types").ConstantAccessor<import("csstype").Property.MixBlendMode, Datum>;
|
|
29
|
+
clipPath: string;
|
|
30
|
+
imageFilter: import("../types").ConstantAccessor<string, Datum>;
|
|
31
|
+
shapeRendering: import("../types").ConstantAccessor<import("csstype").Property.ShapeRendering, Datum>;
|
|
32
|
+
paintOrder: import("../types").ConstantAccessor<string, Datum>;
|
|
33
|
+
onclick: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
34
|
+
ondblclick: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
35
|
+
onmouseup: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
36
|
+
onmousedown: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
37
|
+
onmouseenter: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
38
|
+
onmousemove: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
39
|
+
onmouseleave: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
40
|
+
onmouseout: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
41
|
+
onmouseover: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
42
|
+
onpointercancel: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
43
|
+
onpointerdown: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
44
|
+
onpointerup: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
45
|
+
onpointerenter: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
46
|
+
onpointerleave: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
47
|
+
onpointermove: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
48
|
+
onpointerover: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
49
|
+
onpointerout: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
50
|
+
ondrag: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
51
|
+
ondrop: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
52
|
+
ondragstart: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
53
|
+
ondragenter: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
54
|
+
ondragleave: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
55
|
+
ondragover: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
56
|
+
ondragend: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
57
|
+
ontouchstart: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
58
|
+
ontouchmove: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
59
|
+
ontouchend: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
60
|
+
ontouchcancel: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
61
|
+
oncontextmenu: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
62
|
+
onwheel: import("svelte/elements").MouseEventHandler<SVGPathElement>;
|
|
63
|
+
class: string;
|
|
64
|
+
style: string;
|
|
65
|
+
cursor: import("../types").ConstantAccessor<import("csstype").Property.Cursor, Datum>;
|
|
66
|
+
}> & LinkableMarkProps<Datum> & WaffleOptions<Datum> & {
|
|
67
|
+
data?: Datum[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* bound to a babd scale
|
|
70
|
+
*/
|
|
71
|
+
x?: ChannelAccessor<Datum>;
|
|
72
|
+
/**
|
|
73
|
+
* bound to a quantitative scale
|
|
74
|
+
*/
|
|
75
|
+
y?: ChannelAccessor<Datum>;
|
|
76
|
+
/**
|
|
77
|
+
* bound to a quantitative scale
|
|
78
|
+
*/
|
|
79
|
+
y1?: ChannelAccessor<Datum>;
|
|
80
|
+
/**
|
|
81
|
+
* bound to a quantitative scale
|
|
82
|
+
*/
|
|
83
|
+
y2?: ChannelAccessor<Datum>;
|
|
84
|
+
};
|
|
85
|
+
events(): {};
|
|
86
|
+
slots(): {};
|
|
87
|
+
bindings(): "";
|
|
88
|
+
exports(): {};
|
|
89
|
+
}
|
|
90
|
+
interface $$IsomorphicComponent {
|
|
91
|
+
new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
|
|
92
|
+
$$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
|
|
93
|
+
} & ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
94
|
+
<Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
|
|
95
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
96
|
+
}
|
|
97
|
+
/** The waffleX mark lets you create waffle charts by filling a rectangular area with small squares representing data values. */
|
|
98
|
+
declare const WaffleY: $$IsomorphicComponent;
|
|
99
|
+
type WaffleY<Datum extends DataRecord> = InstanceType<typeof WaffleY<Datum>>;
|
|
100
|
+
export default WaffleY;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { randomId, testFilter } from '../../helpers/index.js';
|
|
18
18
|
import { INDEX } from '../../constants';
|
|
19
19
|
import { RAW_VALUE } from '../../transforms/recordize';
|
|
20
|
+
import wordwrap from '../../helpers/wordwrap';
|
|
20
21
|
|
|
21
22
|
type BaseAxisXProps = {
|
|
22
23
|
scaleFn: (d: RawValue) => number;
|
|
@@ -35,7 +36,9 @@
|
|
|
35
36
|
dx: ConstantAccessor<number>;
|
|
36
37
|
dy: ConstantAccessor<number>;
|
|
37
38
|
filter: ChannelAccessor;
|
|
39
|
+
wordwrap: boolean;
|
|
38
40
|
textAnchor: ConstantAccessor<'start' | 'middle' | 'end'> | 'auto';
|
|
41
|
+
removeDuplicateTicks: boolean;
|
|
39
42
|
};
|
|
40
43
|
text: boolean;
|
|
41
44
|
plot: PlotState;
|
|
@@ -59,8 +62,21 @@
|
|
|
59
62
|
text = true
|
|
60
63
|
}: BaseAxisXProps = $props();
|
|
61
64
|
|
|
65
|
+
const isBandScale = $derived(scaleType === 'band');
|
|
66
|
+
const bandWidth = $derived(isBandScale ? scaleFn.bandwidth() : 0);
|
|
67
|
+
|
|
62
68
|
function splitTick(tick: string | string[]) {
|
|
63
|
-
return Array.isArray(tick)
|
|
69
|
+
return Array.isArray(tick)
|
|
70
|
+
? tick
|
|
71
|
+
: typeof tick === 'string' && isBandScale && options.wordwrap !== false
|
|
72
|
+
? wordwrap(
|
|
73
|
+
tick,
|
|
74
|
+
{ maxLineWidth: bandWidth * 0.9 },
|
|
75
|
+
{ minCharactersPerLine: 4 },
|
|
76
|
+
+resolveProp(tickFontSize, {}, 11),
|
|
77
|
+
false
|
|
78
|
+
)
|
|
79
|
+
: [tick];
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
let tickRotate = $derived(plot.options.x.tickRotate || 0);
|
|
@@ -72,7 +88,7 @@
|
|
|
72
88
|
// generate id used for registering margins
|
|
73
89
|
const id = randomId();
|
|
74
90
|
|
|
75
|
-
const { autoMarginTop, autoMarginBottom } =
|
|
91
|
+
const { autoMarginTop, autoMarginBottom, autoMarginLeft, autoMarginRight } =
|
|
76
92
|
getContext<AutoMarginStores>('svelteplot/autoMargins');
|
|
77
93
|
|
|
78
94
|
let tickTextElements = $state([] as SVGTextElement[]);
|
|
@@ -109,6 +125,14 @@
|
|
|
109
125
|
return tickObjects as ScaledDataRecord[];
|
|
110
126
|
});
|
|
111
127
|
|
|
128
|
+
$effect(() => {
|
|
129
|
+
// just add some minimal horizontal margins for axis ticks
|
|
130
|
+
untrack(() => $autoMarginLeft);
|
|
131
|
+
untrack(() => $autoMarginRight);
|
|
132
|
+
$autoMarginLeft.set(id, 5);
|
|
133
|
+
$autoMarginRight.set(id, 10);
|
|
134
|
+
});
|
|
135
|
+
|
|
112
136
|
$effect(() => {
|
|
113
137
|
untrack(() => [$autoMarginTop, $autoMarginBottom]);
|
|
114
138
|
if (!text) return;
|
|
@@ -147,6 +171,8 @@
|
|
|
147
171
|
return () => {
|
|
148
172
|
if ($autoMarginBottom.has(id)) $autoMarginBottom.delete(id);
|
|
149
173
|
if ($autoMarginTop.has(id)) $autoMarginTop.delete(id);
|
|
174
|
+
if ($autoMarginLeft.has(id)) $autoMarginLeft.delete(id);
|
|
175
|
+
if ($autoMarginRight.has(id)) $autoMarginRight.delete(id);
|
|
150
176
|
};
|
|
151
177
|
});
|
|
152
178
|
</script>
|
|
@@ -222,7 +248,9 @@
|
|
|
222
248
|
{:else}
|
|
223
249
|
{#each textLines as line, i (i)}
|
|
224
250
|
<tspan x="0" dy={i ? 12 : 0}
|
|
225
|
-
>{!prevTextLines ||
|
|
251
|
+
>{!prevTextLines ||
|
|
252
|
+
prevTextLines[i] !== line ||
|
|
253
|
+
options.removeDuplicateTicks === false
|
|
226
254
|
? line
|
|
227
255
|
: ''}</tspan>
|
|
228
256
|
{/each}
|
|
@@ -16,7 +16,9 @@ type BaseAxisXProps = {
|
|
|
16
16
|
dx: ConstantAccessor<number>;
|
|
17
17
|
dy: ConstantAccessor<number>;
|
|
18
18
|
filter: ChannelAccessor;
|
|
19
|
+
wordwrap: boolean;
|
|
19
20
|
textAnchor: ConstantAccessor<'start' | 'middle' | 'end'> | 'auto';
|
|
21
|
+
removeDuplicateTicks: boolean;
|
|
20
22
|
};
|
|
21
23
|
text: boolean;
|
|
22
24
|
plot: PlotState;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { StackOptions } from '../../transforms/stack';
|
|
3
|
+
import type { BorderRadius, ConstantAccessor, PlotScales, ScaledDataRecord } from '../../types';
|
|
4
|
+
type Point = [number, number];
|
|
5
|
+
export type WaffleOptions<T> = {
|
|
6
|
+
/**
|
|
7
|
+
* the quantity represented by each square in the waffle chart, defaults to 1
|
|
8
|
+
*/
|
|
9
|
+
unit?: number;
|
|
10
|
+
/**
|
|
11
|
+
* the number of cells per row (or column); defaults to undefined
|
|
12
|
+
*/
|
|
13
|
+
multiple?: number;
|
|
14
|
+
/**
|
|
15
|
+
* the separation between adjacent cells, in pixels; defaults to 1
|
|
16
|
+
*/
|
|
17
|
+
gap?: number;
|
|
18
|
+
/**
|
|
19
|
+
* whether to round values to avoid partial cells; defaults to false
|
|
20
|
+
*/
|
|
21
|
+
round?: boolean;
|
|
22
|
+
stack?: StackOptions;
|
|
23
|
+
borderRadius?: ConstantAccessor<BorderRadius, T>;
|
|
24
|
+
symbol?: Snippet<[
|
|
25
|
+
{
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
style: string | null;
|
|
31
|
+
styleClass: string | null;
|
|
32
|
+
datum: T;
|
|
33
|
+
}
|
|
34
|
+
]>;
|
|
35
|
+
};
|
|
36
|
+
type WaffleProps = {
|
|
37
|
+
pattern: {
|
|
38
|
+
id: string;
|
|
39
|
+
patternUnits: 'userSpaceOnUse';
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
rect: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
};
|
|
49
|
+
path: {
|
|
50
|
+
fill: string;
|
|
51
|
+
transform: string;
|
|
52
|
+
d: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export declare function wafflePolygon(y: 'x' | 'y', options: WaffleOptions, scales: PlotScales): (d: ScaledDataRecord) => WaffleProps;
|
|
56
|
+
export declare function wafflePoints(i1: number, i2: number, columns: number): Point[];
|
|
57
|
+
export declare function maybeRound(round: boolean | ((x: number) => number) | undefined): (x: number) => number;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// A waffle is approximately a rectangular shape, but may have one or two corner
|
|
2
|
+
// cuts if the starting or ending value is not an even multiple of the number of
|
|
3
|
+
// columns (the width of the waffle in cells). We can represent any waffle by
|
|
4
|
+
// 8 points; below is a waffle of five columns representing the interval 2–11:
|
|
5
|
+
//
|
|
6
|
+
// 1-0
|
|
7
|
+
// |•7-------6
|
|
8
|
+
// |• • • • •|
|
|
9
|
+
// 2---3• • •|
|
|
10
|
+
// 4-----5
|
|
11
|
+
//
|
|
12
|
+
// Note that points 0 and 1 always have the same y-value, points 1 and 2 have
|
|
13
|
+
// the same x-value, and so on, so we don’t need to materialize the x- and y-
|
|
14
|
+
// values of all points. Also note that we can’t use the already-projected y-
|
|
15
|
+
// values because these assume that y-values are distributed linearly along y
|
|
16
|
+
// rather than wrapping around in columns.
|
|
17
|
+
//
|
|
18
|
+
// The corner points may be coincident. If the ending value is an even multiple
|
|
19
|
+
// of the number of columns, say representing the interval 2–10, then points 6,
|
|
20
|
+
// 7, and 0 are the same.
|
|
21
|
+
//
|
|
22
|
+
// 1-----0/7/6
|
|
23
|
+
// |• • • • •|
|
|
24
|
+
// 2---3• • •|
|
|
25
|
+
// 4-----5
|
|
26
|
+
//
|
|
27
|
+
// Likewise if the starting value is an even multiple, say representing the
|
|
28
|
+
// interval 0–10, points 2–4 are coincident.
|
|
29
|
+
//
|
|
30
|
+
// 1-----0/7/6
|
|
31
|
+
// |• • • • •|
|
|
32
|
+
// |• • • • •|
|
|
33
|
+
// 4/3/2-----5
|
|
34
|
+
//
|
|
35
|
+
// Waffles can also represent fractional intervals (e.g., 2.4–10.1). These
|
|
36
|
+
// require additional corner cuts, so the implementation below generates a few
|
|
37
|
+
// more points.
|
|
38
|
+
//
|
|
39
|
+
import { getPatternId } from '../../helpers/getBaseStyles';
|
|
40
|
+
export function wafflePolygon(y, options, scales) {
|
|
41
|
+
const x = y === 'y' ? 'x' : 'y';
|
|
42
|
+
const y1 = `${y}1`;
|
|
43
|
+
const y2 = `${y}2`;
|
|
44
|
+
const xScale = scales[x];
|
|
45
|
+
const yScale = scales[y];
|
|
46
|
+
const barwidth = xScale.fn.bandwidth();
|
|
47
|
+
const { unit = 1, gap = 1 } = options;
|
|
48
|
+
const round = maybeRound(options.round);
|
|
49
|
+
// The length of a unit along y in pixels.
|
|
50
|
+
const scale = Math.abs(yScale.fn(unit) - yScale.fn(0));
|
|
51
|
+
// The number of cells on each row (or column) of the waffle.
|
|
52
|
+
const multiple = options.multiple ?? Math.max(1, Math.floor(Math.sqrt(barwidth / scale)));
|
|
53
|
+
// The outer size of each square cell, in pixels, including the gap.
|
|
54
|
+
const cx = Math.min(barwidth / multiple, scale * multiple);
|
|
55
|
+
const cy = scale * multiple;
|
|
56
|
+
// The reference position.
|
|
57
|
+
const tx = (barwidth - multiple * cx) / 2;
|
|
58
|
+
const transform = y === 'y' ? ([x, y]) => [x * cx, -y * cy] : ([x, y]) => [y * cy, x * cx];
|
|
59
|
+
// const mx = typeof x0 === 'function' ? (i) => x0(i) - barwidth / 2 : () => x0;
|
|
60
|
+
const [ix, iy] = y === 'y' ? [0, 1] : [1, 0];
|
|
61
|
+
const y0 = yScale.fn(0);
|
|
62
|
+
const mx = -barwidth / 2;
|
|
63
|
+
return (d) => {
|
|
64
|
+
const y1val = d.resolved[y1];
|
|
65
|
+
const y2val = d.resolved[y2];
|
|
66
|
+
const P = wafflePoints(round(y1val / unit), round(y2val / unit), multiple).map(transform);
|
|
67
|
+
P.pop();
|
|
68
|
+
const id = getPatternId();
|
|
69
|
+
const pos = [d[x] + tx + mx, y0];
|
|
70
|
+
return {
|
|
71
|
+
pattern: {
|
|
72
|
+
id,
|
|
73
|
+
patternUnits: 'userSpaceOnUse',
|
|
74
|
+
width: cx,
|
|
75
|
+
height: cy
|
|
76
|
+
},
|
|
77
|
+
rect: {
|
|
78
|
+
x: gap / 2,
|
|
79
|
+
y: gap / 2,
|
|
80
|
+
width: cx - gap,
|
|
81
|
+
height: cy - gap
|
|
82
|
+
},
|
|
83
|
+
path: {
|
|
84
|
+
fill: `url(#${id})`,
|
|
85
|
+
transform: `translate(${pos[ix]},${pos[iy]})`,
|
|
86
|
+
d: `M${P.join('L')}Z`
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// return `M${P.join('L')}Z`;
|
|
90
|
+
};
|
|
91
|
+
// const points = wafflePoints(i1, i2, columns);
|
|
92
|
+
// return dimension === 'x' ? points : points.map(([x, y]: Point): Point => [y, x]);
|
|
93
|
+
}
|
|
94
|
+
export function wafflePoints(i1, i2, columns) {
|
|
95
|
+
if (i2 < i1)
|
|
96
|
+
return wafflePoints(i2, i1, columns); // ensure i1 <= i2
|
|
97
|
+
if (i1 < 0)
|
|
98
|
+
return wafflePointsOffset(i1, i2, columns, Math.ceil(-Math.min(i1, i2) / columns)); // ensure i1 >= 0
|
|
99
|
+
const x1f = Math.floor(i1 % columns);
|
|
100
|
+
const x1c = Math.ceil(i1 % columns);
|
|
101
|
+
const x2f = Math.floor(i2 % columns);
|
|
102
|
+
const x2c = Math.ceil(i2 % columns);
|
|
103
|
+
const y1f = Math.floor(i1 / columns);
|
|
104
|
+
const y1c = Math.ceil(i1 / columns);
|
|
105
|
+
const y2f = Math.floor(i2 / columns);
|
|
106
|
+
const y2c = Math.ceil(i2 / columns);
|
|
107
|
+
const points = [];
|
|
108
|
+
if (y2c > y1c)
|
|
109
|
+
points.push([0, y1c]);
|
|
110
|
+
points.push([x1f, y1c], [x1f, y1f + (i1 % 1)], [x1c, y1f + (i1 % 1)]);
|
|
111
|
+
if (!(i1 % columns > columns - 1)) {
|
|
112
|
+
points.push([x1c, y1f]);
|
|
113
|
+
if (y2f > y1f)
|
|
114
|
+
points.push([columns, y1f]);
|
|
115
|
+
}
|
|
116
|
+
if (y2f > y1f)
|
|
117
|
+
points.push([columns, y2f]);
|
|
118
|
+
points.push([x2c, y2f], [x2c, y2f + (i2 % 1)], [x2f, y2f + (i2 % 1)]);
|
|
119
|
+
if (!(i2 % columns < 1)) {
|
|
120
|
+
points.push([x2f, y2c]);
|
|
121
|
+
if (y2c > y1c)
|
|
122
|
+
points.push([0, y2c]);
|
|
123
|
+
}
|
|
124
|
+
points.push(waffleCentroid(i1, i2, columns));
|
|
125
|
+
return points;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Compute waffle points when indices start in negative rows by applying a row offset.
|
|
129
|
+
* - Shifts both indices down by `k` rows (adding `k * columns`) so they are non-negative,
|
|
130
|
+
* delegates to `wafflePoints`, then translates the resulting points back up by `k` on y.
|
|
131
|
+
* - `k` is the number of rows of vertical offset applied.
|
|
132
|
+
*/
|
|
133
|
+
function wafflePointsOffset(i1, i2, columns, k) {
|
|
134
|
+
return wafflePoints(i1 + k * columns, i2 + k * columns, columns).map(([x, y]) => [x, y - k]);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Centroid of the waffle region representing the interval [i1, i2).
|
|
138
|
+
* Chooses a strategy based on how many rows the interval spans:
|
|
139
|
+
* - Single row: delegate to `waffleRowCentroid`.
|
|
140
|
+
* - Two rows: if the projected columns overlap, return the midpoint of the overlap;
|
|
141
|
+
* otherwise, return the centroid of the larger partial row.
|
|
142
|
+
* - >= 3 rows: return the center column and halfway between the middle rows.
|
|
143
|
+
*/
|
|
144
|
+
function waffleCentroid(i1, i2, columns) {
|
|
145
|
+
const r = Math.floor(i2 / columns) - Math.floor(i1 / columns);
|
|
146
|
+
return r === 0
|
|
147
|
+
? // Single row
|
|
148
|
+
waffleRowCentroid(i1, i2, columns)
|
|
149
|
+
: r === 1
|
|
150
|
+
? // Two incomplete rows; use the midpoint of their overlap if any, otherwise the larger row
|
|
151
|
+
Math.floor(i2 % columns) > Math.ceil(i1 % columns)
|
|
152
|
+
? [(Math.floor(i2 % columns) + Math.ceil(i1 % columns)) / 2, Math.floor(i2 / columns)]
|
|
153
|
+
: i2 % columns > columns - (i1 % columns)
|
|
154
|
+
? waffleRowCentroid(i2 - (i2 % columns), i2, columns)
|
|
155
|
+
: waffleRowCentroid(i1, columns * Math.ceil(i1 / columns), columns)
|
|
156
|
+
: // At least one full row; take the midpoint of all the rows that include the middle
|
|
157
|
+
[columns / 2, (Math.round(i1 / columns) + Math.round(i2 / columns)) / 2];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Centroid of a waffle segment constrained to a single row.
|
|
161
|
+
* Cases:
|
|
162
|
+
* - c === 0: both endpoints fall into the same cell; center on x, average fractional y.
|
|
163
|
+
* - c === 1: two adjacent partial cells; use the overlap center if > 0.5 cell,
|
|
164
|
+
* otherwise the center of the larger partial cell.
|
|
165
|
+
* - c >= 2: at least one full cell between; x is the midpoint of full cells,
|
|
166
|
+
* y is the row center (0.5) if there’s a full cell spanned, otherwise average fractional y.
|
|
167
|
+
*/
|
|
168
|
+
function waffleRowCentroid(i1, i2, columns) {
|
|
169
|
+
const c = Math.floor(i2) - Math.floor(i1);
|
|
170
|
+
return c === 0
|
|
171
|
+
? // Single cell
|
|
172
|
+
[Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (((i1 + i2) / 2) % 1)]
|
|
173
|
+
: c === 1
|
|
174
|
+
? // Two incomplete cells; use the overlap if large enough, otherwise use the largest
|
|
175
|
+
(i2 % 1) - (i1 % 1) > 0.5
|
|
176
|
+
? [Math.ceil(i1 % columns), Math.floor(i2 / columns) + ((i1 % 1) + (i2 % 1)) / 2]
|
|
177
|
+
: i2 % 1 > 1 - (i1 % 1)
|
|
178
|
+
? [Math.floor(i2 % columns) + 0.5, Math.floor(i2 / columns) + (i2 % 1) / 2]
|
|
179
|
+
: [Math.floor(i1 % columns) + 0.5, Math.floor(i1 / columns) + (1 + (i1 % 1)) / 2]
|
|
180
|
+
: // At least one full cell; take the midpoint
|
|
181
|
+
[
|
|
182
|
+
Math.ceil(i1 % columns) + Math.ceil(Math.floor(i2) - Math.ceil(i1)) / 2,
|
|
183
|
+
Math.floor(i1 / columns) + (i2 >= 1 + i1 ? 0.5 : ((i1 + i2) / 2) % 1)
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
export function maybeRound(round) {
|
|
187
|
+
if (round === undefined || round === false)
|
|
188
|
+
return Number;
|
|
189
|
+
if (round === true)
|
|
190
|
+
return Math.round;
|
|
191
|
+
if (typeof round !== 'function')
|
|
192
|
+
throw new Error(`invalid round: ${round}`);
|
|
193
|
+
return round;
|
|
194
|
+
}
|
package/dist/marks/index.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ export { default as LineX } from './LineX.svelte';
|
|
|
33
33
|
export { default as LineY } from './LineY.svelte';
|
|
34
34
|
export { default as Link } from './Link.svelte';
|
|
35
35
|
export { default as Pointer } from './Pointer.svelte';
|
|
36
|
-
export { default as Vector } from './Vector.svelte';
|
|
37
36
|
export { default as Rect } from './Rect.svelte';
|
|
38
37
|
export { default as RectX } from './RectX.svelte';
|
|
39
38
|
export { default as RectY } from './RectY.svelte';
|
|
@@ -46,6 +45,9 @@ export { default as Spike } from './Spike.svelte';
|
|
|
46
45
|
export { default as Text } from './Text.svelte';
|
|
47
46
|
export { default as TickX } from './TickX.svelte';
|
|
48
47
|
export { default as TickY } from './TickY.svelte';
|
|
48
|
+
export { default as Vector } from './Vector.svelte';
|
|
49
|
+
export { default as WaffleX } from './WaffleX.svelte';
|
|
50
|
+
export { default as WaffleY } from './WaffleY.svelte';
|
|
49
51
|
export { default as ColorLegend } from './ColorLegend.svelte';
|
|
50
52
|
export { default as HTMLTooltip } from './HTMLTooltip.svelte';
|
|
51
53
|
export { default as SymbolLegend } from './SymbolLegend.svelte';
|
package/dist/marks/index.js
CHANGED
|
@@ -33,7 +33,6 @@ export { default as LineX } from './LineX.svelte';
|
|
|
33
33
|
export { default as LineY } from './LineY.svelte';
|
|
34
34
|
export { default as Link } from './Link.svelte';
|
|
35
35
|
export { default as Pointer } from './Pointer.svelte';
|
|
36
|
-
export { default as Vector } from './Vector.svelte';
|
|
37
36
|
export { default as Rect } from './Rect.svelte';
|
|
38
37
|
export { default as RectX } from './RectX.svelte';
|
|
39
38
|
export { default as RectY } from './RectY.svelte';
|
|
@@ -46,6 +45,9 @@ export { default as Spike } from './Spike.svelte';
|
|
|
46
45
|
export { default as Text } from './Text.svelte';
|
|
47
46
|
export { default as TickX } from './TickX.svelte';
|
|
48
47
|
export { default as TickY } from './TickY.svelte';
|
|
48
|
+
export { default as Vector } from './Vector.svelte';
|
|
49
|
+
export { default as WaffleX } from './WaffleX.svelte';
|
|
50
|
+
export { default as WaffleY } from './WaffleY.svelte';
|
|
49
51
|
// HTML marks
|
|
50
52
|
export { default as ColorLegend } from './ColorLegend.svelte';
|
|
51
53
|
export { default as HTMLTooltip } from './HTMLTooltip.svelte';
|
package/dist/transforms/group.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { groupFacetsAndZ } from '../helpers/group.js';
|
|
2
|
-
import { testFilter } from '../helpers/index.js';
|
|
2
|
+
import { isValid, testFilter } from '../helpers/index.js';
|
|
3
3
|
import { reduceOutputs } from '../helpers/reduce.js';
|
|
4
4
|
import { resolveChannel } from '../helpers/resolve.js';
|
|
5
5
|
import { groups as d3Groups } from 'd3-array';
|
|
@@ -58,8 +58,12 @@ export function groupY(input, options = {}) {
|
|
|
58
58
|
export function groupZ(input, options = {}) {
|
|
59
59
|
return groupXYZ('z', input, options);
|
|
60
60
|
}
|
|
61
|
+
const groupDimRaw = Symbol('groupDimRaw');
|
|
61
62
|
function groupXYZ(dim, { data, ...channels }, options = {}) {
|
|
62
|
-
|
|
63
|
+
// console.log({ dim, data, channels, options });
|
|
64
|
+
if ((dim === 'z'
|
|
65
|
+
? channels.z || channels.fill || channels.stroke || channels.fx || channels.fy
|
|
66
|
+
: channels[dim]) == null)
|
|
63
67
|
throw new Error('you must provide a channel to group on ' + dim);
|
|
64
68
|
const propName = options[`${dim}PropName`] != null
|
|
65
69
|
? options[`${dim}PropName`]
|
|
@@ -70,9 +74,11 @@ function groupXYZ(dim, { data, ...channels }, options = {}) {
|
|
|
70
74
|
// group by x or y
|
|
71
75
|
const groups = dim === 'z'
|
|
72
76
|
? [[null, data]]
|
|
73
|
-
: d3Groups(data
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
: d3Groups(data
|
|
78
|
+
.filter((d) => testFilter(d, channels))
|
|
79
|
+
.map((d) => ({ ...d, [groupDimRaw]: resolveChannel(dim, d, channels) }))
|
|
80
|
+
.filter((d) => isValid(d[groupDimRaw])), (d) => {
|
|
81
|
+
return interval ? interval.floor(d[groupDimRaw]) : d[groupDimRaw];
|
|
76
82
|
});
|
|
77
83
|
const newData = [];
|
|
78
84
|
let newChannels = omit({ ...channels }, 'filter');
|
package/dist/types/data.d.ts
CHANGED
package/dist/types/mark.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type Mark<T> = {
|
|
|
6
6
|
data: DataRecord<T>[];
|
|
7
7
|
options: T;
|
|
8
8
|
};
|
|
9
|
-
export type MarkType = 'area' | 'arrow' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'line' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY';
|
|
9
|
+
export type MarkType = 'area' | 'arrow' | 'barX' | 'barY' | 'cell' | 'custom' | 'dot' | 'vector' | 'frame' | 'geo' | 'gridX' | 'gridY' | 'line' | 'rect' | 'regression' | 'ruleX' | 'ruleY' | 'swoopyArrow' | 'text' | 'tickX' | 'tickY' | 'waffleX' | 'waffleY';
|
|
10
10
|
export type MarkStyleProps = 'strokeDasharray' | 'strokeLinejoin' | 'strokeLinecap' | 'opacity' | 'cursor' | 'pointerEvents' | 'blend' | 'fill' | 'fillOpacity' | 'fontFamily' | 'fontWeight' | 'fontVariant' | 'fontSize' | 'fontStyle' | 'letterSpacing' | 'wordSpacing' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'x' | 'y' | 'clipPath' | 'mask' | 'filter' | 'angle' | 'radius' | 'symbol' | 'textAnchor' | 'textTransform' | 'textDecoration' | 'width';
|
|
11
11
|
import type { MouseEventHandler } from 'svelte/elements';
|
|
12
12
|
import type { ChannelAccessor, ConstantAccessor, DataRecord, RawValue } from './index.js';
|