@rokkit/chart 1.7.0 → 1.8.1
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/README.md +13 -0
- package/dist/Plot/DataTable.svelte +54 -0
- package/dist/Plot/DataTable.svelte.d.ts +12 -0
- package/dist/Plot/SpecGeoms.svelte +78 -0
- package/dist/Plot/SpecGeoms.svelte.d.ts +11 -0
- package/dist/Plot.svelte +47 -151
- package/dist/Plot.svelte.d.ts +5 -0
- package/dist/PlotState.svelte.d.ts +115 -71
- package/dist/PlotState.svelte.js +174 -547
- package/dist/SparkState.svelte.d.ts +3 -1
- package/dist/SparkState.svelte.js +11 -41
- package/dist/geoms/Arc.svelte +1 -1
- package/dist/geoms/Bar.svelte +23 -32
- package/dist/geoms/Contour.svelte +88 -0
- package/dist/geoms/Contour.svelte.d.ts +19 -0
- package/dist/geoms/Hull.svelte +89 -0
- package/dist/geoms/Hull.svelte.d.ts +17 -0
- package/dist/geoms/Region.svelte +107 -0
- package/dist/geoms/Region.svelte.d.ts +27 -0
- package/dist/geoms/Rule.svelte +88 -26
- package/dist/geoms/Rule.svelte.d.ts +8 -0
- package/dist/geoms/lib/PlotAreaClip.svelte +18 -0
- package/dist/geoms/lib/PlotAreaClip.svelte.d.ts +6 -0
- package/dist/geoms/lib/bars.js +13 -20
- package/dist/geoms/lib/grouping.d.ts +35 -0
- package/dist/geoms/lib/grouping.js +59 -0
- package/dist/geoms/lib/marks/arc.d.ts +1 -0
- package/dist/geoms/lib/marks/contour.d.ts +26 -0
- package/dist/geoms/lib/marks/contour.js +67 -0
- package/dist/geoms/lib/marks/hull.d.ts +26 -0
- package/dist/geoms/lib/marks/hull.js +36 -0
- package/dist/geoms/lib/marks/waterfall.js +15 -20
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/abline.d.ts +41 -0
- package/dist/lib/abline.js +51 -0
- package/dist/lib/brewing/marks/arcs.d.ts +6 -1
- package/dist/lib/brewing/marks/arcs.js +9 -1
- package/dist/lib/brewing/polar.js +8 -3
- package/dist/lib/hull.d.ts +18 -0
- package/dist/lib/hull.js +64 -0
- package/dist/lib/plot/domains.d.ts +61 -0
- package/dist/lib/plot/domains.js +130 -0
- package/dist/lib/plot/running.d.ts +23 -0
- package/dist/lib/plot/running.js +32 -0
- package/dist/lib/plot/spec.d.ts +59 -0
- package/dist/lib/plot/spec.js +131 -0
- package/dist/lib/plot/stacking.d.ts +32 -0
- package/dist/lib/plot/stacking.js +34 -0
- package/dist/lib/plot/types.d.ts +8 -1
- package/dist/lib/plot/types.js +5 -1
- package/dist/lib/preset.d.ts +2 -0
- package/dist/lib/preset.js +76 -41
- package/dist/lib/region.d.ts +63 -0
- package/dist/lib/region.js +127 -0
- package/dist/state/AestheticState.svelte.d.ts +43 -0
- package/dist/state/AestheticState.svelte.js +84 -0
- package/dist/state/AxisState.svelte.d.ts +26 -0
- package/dist/state/AxisState.svelte.js +56 -0
- package/dist/state/ChannelState.svelte.d.ts +34 -0
- package/dist/state/ChannelState.svelte.js +67 -0
- package/dist/state/GeomRegistry.svelte.d.ts +44 -0
- package/dist/state/GeomRegistry.svelte.js +86 -0
- package/dist/state/InteractionState.svelte.d.ts +30 -0
- package/dist/state/InteractionState.svelte.js +69 -0
- package/dist/state/OrientationState.svelte.d.ts +34 -0
- package/dist/state/OrientationState.svelte.js +64 -0
- package/dist/state/PlotConfig.svelte.d.ts +179 -0
- package/dist/state/PlotConfig.svelte.js +195 -0
- package/dist/state/PlotFrame.svelte.d.ts +17 -0
- package/dist/state/PlotFrame.svelte.js +18 -0
- package/dist/state/ScaleState.svelte.d.ts +36 -0
- package/dist/state/ScaleState.svelte.js +121 -0
- package/package.json +5 -3
package/dist/geoms/Rule.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte'
|
|
3
3
|
import type { PlotState } from '../PlotState.svelte.js'
|
|
4
|
+
import { clipAbline, slopeLabel } from '../lib/abline.js'
|
|
4
5
|
|
|
5
6
|
type RuleValue = number | string
|
|
6
7
|
type Props = {
|
|
@@ -8,6 +9,14 @@
|
|
|
8
9
|
x?: RuleValue | RuleValue[]
|
|
9
10
|
/** Horizontal reference line(s) at these y-axis value(s). */
|
|
10
11
|
y?: RuleValue | RuleValue[]
|
|
12
|
+
/**
|
|
13
|
+
* A sloped line `y = slope·x + intercept` (ggplot's `abline`), clipped to the visible
|
|
14
|
+
* domain. Continuous axes only — a slope over categories has no meaning, so it is
|
|
15
|
+
* ignored on a band axis. The main sequence is `slope={-1} intercept={1}`.
|
|
16
|
+
*/
|
|
17
|
+
slope?: number
|
|
18
|
+
/** Where the sloped line crosses x = 0. Defaults to 0. */
|
|
19
|
+
intercept?: number
|
|
11
20
|
/** Line color (aligns with the other geoms' `color` aesthetic). */
|
|
12
21
|
color?: string
|
|
13
22
|
/** @deprecated use `color` — kept as an alias. */
|
|
@@ -22,6 +31,8 @@
|
|
|
22
31
|
let {
|
|
23
32
|
x,
|
|
24
33
|
y,
|
|
34
|
+
slope = undefined,
|
|
35
|
+
intercept = 0,
|
|
25
36
|
color = undefined,
|
|
26
37
|
stroke = undefined,
|
|
27
38
|
dash = '4 4',
|
|
@@ -37,7 +48,10 @@
|
|
|
37
48
|
v === undefined ? [] : Array.isArray(v) ? v : [v]
|
|
38
49
|
|
|
39
50
|
// Position along an axis: band scales resolve to the band centre, linear scales map directly.
|
|
40
|
-
function axisPos(
|
|
51
|
+
function axisPos(
|
|
52
|
+
scale: { (v: unknown): number | undefined; bandwidth?: () => number },
|
|
53
|
+
v: unknown
|
|
54
|
+
) {
|
|
41
55
|
const base = scale(v) ?? 0
|
|
42
56
|
return typeof scale.bandwidth === 'function' ? base + scale.bandwidth() / 2 : base
|
|
43
57
|
}
|
|
@@ -50,37 +64,72 @@
|
|
|
50
64
|
const labelFor = (v: RuleValue) =>
|
|
51
65
|
typeof label === 'function' ? label(v) : typeof label === 'string' ? label : null
|
|
52
66
|
|
|
67
|
+
type Point = { x: number; y: number }
|
|
68
|
+
type RuleLine = {
|
|
69
|
+
key: string
|
|
70
|
+
x1: number
|
|
71
|
+
y1: number
|
|
72
|
+
x2: number
|
|
73
|
+
y2: number
|
|
74
|
+
value: RuleValue
|
|
75
|
+
text: string | null
|
|
76
|
+
kind: 'x' | 'y' | 'slope'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const lineOf = (id: Pick<RuleLine, 'key' | 'kind' | 'value'>, a: Point, b: Point): RuleLine => ({
|
|
80
|
+
...id,
|
|
81
|
+
x1: a.x,
|
|
82
|
+
y1: a.y,
|
|
83
|
+
x2: b.x,
|
|
84
|
+
y2: b.y,
|
|
85
|
+
text: labelFor(id.value)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
type AnyScale = {
|
|
89
|
+
(v: unknown): number | undefined
|
|
90
|
+
bandwidth?: () => number
|
|
91
|
+
domain: () => unknown[]
|
|
92
|
+
}
|
|
93
|
+
const isBand = (scale: AnyScale) => typeof scale.bandwidth === 'function'
|
|
94
|
+
|
|
95
|
+
// The sloped line, clipped in DATA space so it stays inside the plot at any zoom. A band
|
|
96
|
+
// axis has no numeric domain to clip against, so the slope is ignored there.
|
|
97
|
+
function slopeLine(
|
|
98
|
+
xs: AnyScale,
|
|
99
|
+
ys: AnyScale,
|
|
100
|
+
place: (u: number, v: number) => Point
|
|
101
|
+
): RuleLine | null {
|
|
102
|
+
if (slope === undefined || isBand(xs) || isBand(ys)) return null
|
|
103
|
+
const seg = clipAbline(
|
|
104
|
+
slope,
|
|
105
|
+
intercept,
|
|
106
|
+
xs.domain() as [number, number],
|
|
107
|
+
ys.domain() as [number, number]
|
|
108
|
+
)
|
|
109
|
+
if (!seg) return null
|
|
110
|
+
const a = place(Number(xs(seg.x1)), Number(ys(seg.y1)))
|
|
111
|
+
const b = place(Number(xs(seg.x2)), Number(ys(seg.y2)))
|
|
112
|
+
return lineOf({ key: `slope-${slope}-${intercept}`, kind: 'slope', value: slope }, a, b)
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
// Each rule is expressed in (x-channel, y-channel) space and placed to screen, so it
|
|
54
116
|
// transposes under orientation flip: a y-value reference is horizontal when vertical and
|
|
55
117
|
// vertical when flipped (it stays perpendicular to the value axis).
|
|
56
|
-
const rules = $derived.by(() => {
|
|
118
|
+
const rules = $derived.by((): RuleLine[] => {
|
|
57
119
|
if (!xScale || !yScale) return []
|
|
58
120
|
const place = plotState.place.bind(plotState)
|
|
59
|
-
const out: Array<{
|
|
60
|
-
key: string
|
|
61
|
-
x1: number
|
|
62
|
-
y1: number
|
|
63
|
-
x2: number
|
|
64
|
-
y2: number
|
|
65
|
-
value: RuleValue
|
|
66
|
-
text: string | null
|
|
67
|
-
}> = []
|
|
68
121
|
const [xLo, xHi] = axisExtent(xScale)
|
|
69
122
|
const [yLo, yHi] = axisExtent(yScale)
|
|
70
|
-
|
|
71
|
-
for (const yv of toArray(y)) {
|
|
123
|
+
const horizontal = toArray(y).map((yv) => {
|
|
72
124
|
const yc = axisPos(yScale, yv)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
for (const xv of toArray(x)) {
|
|
125
|
+
return lineOf({ key: `y-${yv}`, kind: 'y', value: yv }, place(xLo, yc), place(xHi, yc))
|
|
126
|
+
})
|
|
127
|
+
const vertical = toArray(x).map((xv) => {
|
|
78
128
|
const xc = axisPos(xScale, xv)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return out
|
|
129
|
+
return lineOf({ key: `x-${xv}`, kind: 'x', value: xv }, place(xc, yLo), place(xc, yHi))
|
|
130
|
+
})
|
|
131
|
+
const sloped = slopeLine(xScale, yScale, place)
|
|
132
|
+
return sloped ? [...horizontal, ...vertical, sloped] : [...horizontal, ...vertical]
|
|
84
133
|
})
|
|
85
134
|
|
|
86
135
|
const strokeColor = $derived(color ?? stroke ?? 'currentColor')
|
|
@@ -98,9 +147,22 @@
|
|
|
98
147
|
stroke-width={strokeWidth}
|
|
99
148
|
stroke-dasharray={dash || undefined}
|
|
100
149
|
data-plot-element="rule"
|
|
150
|
+
data-plot-rule={rule.kind}
|
|
101
151
|
data-plot-value={rule.value}
|
|
102
152
|
/>
|
|
103
|
-
{#if rule.text}
|
|
153
|
+
{#if rule.text && rule.kind === 'slope'}
|
|
154
|
+
{@const at = slopeLabel(rule)}
|
|
155
|
+
<text
|
|
156
|
+
x={at.x}
|
|
157
|
+
y={at.y}
|
|
158
|
+
dy="-5"
|
|
159
|
+
text-anchor="middle"
|
|
160
|
+
transform="rotate({at.angle} {at.x} {at.y})"
|
|
161
|
+
font-size="10"
|
|
162
|
+
fill={strokeColor}
|
|
163
|
+
data-plot-element="rule-label">{rule.text}</text
|
|
164
|
+
>
|
|
165
|
+
{:else if rule.text}
|
|
104
166
|
<text
|
|
105
167
|
x={rule.x2}
|
|
106
168
|
y={rule.y2}
|
|
@@ -109,8 +171,8 @@
|
|
|
109
171
|
text-anchor="end"
|
|
110
172
|
font-size="10"
|
|
111
173
|
fill={strokeColor}
|
|
112
|
-
data-plot-element="rule-label"
|
|
113
|
-
>
|
|
174
|
+
data-plot-element="rule-label">{rule.text}</text
|
|
175
|
+
>
|
|
114
176
|
{/if}
|
|
115
177
|
{/each}
|
|
116
178
|
</g>
|
|
@@ -4,6 +4,14 @@ type Props = {
|
|
|
4
4
|
x?: RuleValue | RuleValue[];
|
|
5
5
|
/** Horizontal reference line(s) at these y-axis value(s). */
|
|
6
6
|
y?: RuleValue | RuleValue[];
|
|
7
|
+
/**
|
|
8
|
+
* A sloped line `y = slope·x + intercept` (ggplot's `abline`), clipped to the visible
|
|
9
|
+
* domain. Continuous axes only — a slope over categories has no meaning, so it is
|
|
10
|
+
* ignored on a band axis. The main sequence is `slope={-1} intercept={1}`.
|
|
11
|
+
*/
|
|
12
|
+
slope?: number;
|
|
13
|
+
/** Where the sloped line crosses x = 0. Defaults to 0. */
|
|
14
|
+
intercept?: number;
|
|
7
15
|
/** Line color (aligns with the other geoms' `color` aesthetic). */
|
|
8
16
|
color?: string;
|
|
9
17
|
/** @deprecated use `color` — kept as an alias. */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A clip path the size of the plot area, for geoms whose shapes can legitimately extend
|
|
4
|
+
* past the domain — a region polygon drawn beyond the axes, a density ring whose kernel
|
|
5
|
+
* reaches past the last point. Clipped, they stop at the axes instead of painting over them.
|
|
6
|
+
*/
|
|
7
|
+
import { getContext } from 'svelte'
|
|
8
|
+
import type { PlotState } from '../../PlotState.svelte.js'
|
|
9
|
+
|
|
10
|
+
let { id }: { id: string } = $props()
|
|
11
|
+
const plotState = getContext<PlotState>('plot-state')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<defs>
|
|
15
|
+
<clipPath {id}>
|
|
16
|
+
<rect x="0" y="0" width={plotState.innerWidth} height={plotState.innerHeight} />
|
|
17
|
+
</clipPath>
|
|
18
|
+
</defs>
|
package/dist/geoms/lib/bars.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { scaleBand } from 'd3-scale'
|
|
2
2
|
import { stack, stackOffsetExpand } from 'd3-shape'
|
|
3
3
|
import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
4
|
-
import {
|
|
4
|
+
import { literalColor, markEntry } from '../../lib/brewing/colors.js'
|
|
5
|
+
import { subBandFields } from '../../lib/plot/stacking.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Returns a band scale suitable for bar x-positioning.
|
|
@@ -16,24 +17,6 @@ function ensureBandX(xScale, data, xField) {
|
|
|
16
17
|
return scaleBand().domain(domain).range([r0, r1]).padding(0.2)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
* Returns the sub-band fields: distinct non-x fields among [group, color, pattern].
|
|
21
|
-
* These are the fields that cause multiple bars within a single x-band (dodge/grouping).
|
|
22
|
-
* `group` (the explicit grouping channel) takes precedence as the first sub-band.
|
|
23
|
-
*/
|
|
24
|
-
function subBandFields(channels) {
|
|
25
|
-
const { x: xf, group: gf, color: cf, pattern: pf } = channels
|
|
26
|
-
const seen = new Set()
|
|
27
|
-
const out = []
|
|
28
|
-
for (const f of [gf, cf, pf]) {
|
|
29
|
-
if (f && f !== xf && !seen.has(f) && !isLiteralColor(f)) {
|
|
30
|
-
seen.add(f)
|
|
31
|
-
out.push(f)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return out
|
|
35
|
-
}
|
|
36
|
-
|
|
37
20
|
/**
|
|
38
21
|
* The one bar builder. Expresses each bar in abstract (category, value) space and maps it to
|
|
39
22
|
* screen via `place()`, so it renders vertical or horizontal (flip) with the same code.
|
|
@@ -141,7 +124,17 @@ export function buildBars(
|
|
|
141
124
|
})
|
|
142
125
|
}
|
|
143
126
|
|
|
144
|
-
export function buildStackedBars(
|
|
127
|
+
export function buildStackedBars(
|
|
128
|
+
data,
|
|
129
|
+
channels,
|
|
130
|
+
xScale,
|
|
131
|
+
yScale,
|
|
132
|
+
colors,
|
|
133
|
+
innerHeight,
|
|
134
|
+
patterns,
|
|
135
|
+
place = (x, y) => ({ x, y }),
|
|
136
|
+
normalize = false
|
|
137
|
+
) {
|
|
145
138
|
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
146
139
|
const lit = literalColor(cf)
|
|
147
140
|
const field = lit ? undefined : cf
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by the geoms that summarise GROUPS of points (Hull, Contour) rather than drawing one
|
|
3
|
+
* mark per row: bucket rows by the group field, and place each row on screen — or drop it
|
|
4
|
+
* when it has no usable coordinate, which is a normal state in a partially-measured dataset.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Screen position of one value on a scale, or null when the row has no usable coordinate
|
|
8
|
+
* (null / empty / a category the scale does not know / NaN). A band category resolves to its
|
|
9
|
+
* centre, where a point in that category is drawn.
|
|
10
|
+
*
|
|
11
|
+
* @param {any} scale
|
|
12
|
+
* @param {unknown} v
|
|
13
|
+
* @returns {number | null}
|
|
14
|
+
*/
|
|
15
|
+
export function coordOf(scale: any, v: unknown): number | null;
|
|
16
|
+
/**
|
|
17
|
+
* Rows bucketed by `field`, in first-seen order. One bucket (key `null`) without a field.
|
|
18
|
+
*
|
|
19
|
+
* @param {Record<string, unknown>[]} data
|
|
20
|
+
* @param {string | undefined} field
|
|
21
|
+
* @returns {Map<unknown, Record<string, unknown>[]>}
|
|
22
|
+
*/
|
|
23
|
+
export function groupRows(data: Record<string, unknown>[], field: string | undefined): Map<unknown, Record<string, unknown>[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Screen points for rows, after orientation `place`, skipping rows with no coordinate.
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string, unknown>[]} rows
|
|
28
|
+
* @param {any} plot
|
|
29
|
+
* @param {{ x?: string, y?: string }} channels
|
|
30
|
+
* @returns {Array<[number, number]>}
|
|
31
|
+
*/
|
|
32
|
+
export function screenPoints(rows: Record<string, unknown>[], plot: any, channels: {
|
|
33
|
+
x?: string;
|
|
34
|
+
y?: string;
|
|
35
|
+
}): Array<[number, number]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by the geoms that summarise GROUPS of points (Hull, Contour) rather than drawing one
|
|
3
|
+
* mark per row: bucket rows by the group field, and place each row on screen — or drop it
|
|
4
|
+
* when it has no usable coordinate, which is a normal state in a partially-measured dataset.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Screen position of one value on a scale, or null when the row has no usable coordinate
|
|
9
|
+
* (null / empty / a category the scale does not know / NaN). A band category resolves to its
|
|
10
|
+
* centre, where a point in that category is drawn.
|
|
11
|
+
*
|
|
12
|
+
* @param {any} scale
|
|
13
|
+
* @param {unknown} v
|
|
14
|
+
* @returns {number | null}
|
|
15
|
+
*/
|
|
16
|
+
export function coordOf(scale, v) {
|
|
17
|
+
if (v === null || v === undefined || v === '') return null
|
|
18
|
+
const s = scale(v)
|
|
19
|
+
if (s === null || s === undefined || Number.isNaN(s)) return null
|
|
20
|
+
return typeof scale.bandwidth === 'function' ? s + scale.bandwidth() / 2 : s
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rows bucketed by `field`, in first-seen order. One bucket (key `null`) without a field.
|
|
25
|
+
*
|
|
26
|
+
* @param {Record<string, unknown>[]} data
|
|
27
|
+
* @param {string | undefined} field
|
|
28
|
+
* @returns {Map<unknown, Record<string, unknown>[]>}
|
|
29
|
+
*/
|
|
30
|
+
export function groupRows(data, field) {
|
|
31
|
+
const groups = new Map()
|
|
32
|
+
for (const row of data) {
|
|
33
|
+
const key = field ? row[field] : null
|
|
34
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
35
|
+
groups.get(key).push(row)
|
|
36
|
+
}
|
|
37
|
+
return groups
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Screen points for rows, after orientation `place`, skipping rows with no coordinate.
|
|
42
|
+
*
|
|
43
|
+
* @param {Record<string, unknown>[]} rows
|
|
44
|
+
* @param {any} plot
|
|
45
|
+
* @param {{ x?: string, y?: string }} channels
|
|
46
|
+
* @returns {Array<[number, number]>}
|
|
47
|
+
*/
|
|
48
|
+
export function screenPoints(rows, plot, channels) {
|
|
49
|
+
/** @type {Array<[number, number]>} */
|
|
50
|
+
const pts = []
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const u = coordOf(plot.xScale, row[channels.x ?? ''])
|
|
53
|
+
const v = coordOf(plot.yScale, row[channels.y ?? ''])
|
|
54
|
+
if (u === null || v === null) continue
|
|
55
|
+
const p = plot.place(u, v)
|
|
56
|
+
pts.push([p.x, p.y])
|
|
57
|
+
}
|
|
58
|
+
return pts
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG path data for a GeoJSON MultiPolygon's coordinates — every ring of every polygon a
|
|
3
|
+
* closed subpath, so holes cut out under the default nonzero/evenodd rules d3-contour's
|
|
4
|
+
* winding produces.
|
|
5
|
+
*
|
|
6
|
+
* @param {number[][][][]} coordinates
|
|
7
|
+
*/
|
|
8
|
+
export function multiPolygonPath(coordinates: number[][][][]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Kernel-density contours per group of the `fill` (else `color`) field.
|
|
11
|
+
*
|
|
12
|
+
* Density is estimated in SCREEN space over the plot area, so `bandwidth` is in pixels — the
|
|
13
|
+
* unit a reader judges smoothness in — and the result is the same at any data scale. Level 0
|
|
14
|
+
* is the outermost (lowest-density) ring; filled bands ramp opacity toward `alpha` at the
|
|
15
|
+
* densest core, so pile-ups read darkest.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
|
|
18
|
+
*/
|
|
19
|
+
export function buildContourMarks({ data, plot, channels, options, alpha, type }: {
|
|
20
|
+
data: any[];
|
|
21
|
+
plot: any;
|
|
22
|
+
channels: any;
|
|
23
|
+
options?: any;
|
|
24
|
+
alpha?: number;
|
|
25
|
+
type?: string;
|
|
26
|
+
}): any[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { contourDensity } from 'd3-contour'
|
|
2
|
+
import { resolveFillStroke, resolveAlpha } from '../aesthetics.js'
|
|
3
|
+
import { groupRows, screenPoints } from '../grouping.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SVG path data for a GeoJSON MultiPolygon's coordinates — every ring of every polygon a
|
|
7
|
+
* closed subpath, so holes cut out under the default nonzero/evenodd rules d3-contour's
|
|
8
|
+
* winding produces.
|
|
9
|
+
*
|
|
10
|
+
* @param {number[][][][]} coordinates
|
|
11
|
+
*/
|
|
12
|
+
export function multiPolygonPath(coordinates) {
|
|
13
|
+
const fmt = (/** @type {number} */ n) => Math.round(n * 100) / 100
|
|
14
|
+
return coordinates
|
|
15
|
+
.flatMap((polygon) =>
|
|
16
|
+
polygon.map((ring) => `M${ring.map(([x, y]) => `${fmt(x)},${fmt(y)}`).join('L')}Z`)
|
|
17
|
+
)
|
|
18
|
+
.join('')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Kernel-density contours per group of the `fill` (else `color`) field.
|
|
23
|
+
*
|
|
24
|
+
* Density is estimated in SCREEN space over the plot area, so `bandwidth` is in pixels — the
|
|
25
|
+
* unit a reader judges smoothness in — and the result is the same at any data scale. Level 0
|
|
26
|
+
* is the outermost (lowest-density) ring; filled bands ramp opacity toward `alpha` at the
|
|
27
|
+
* densest core, so pile-ups read darkest.
|
|
28
|
+
*
|
|
29
|
+
* @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
|
|
30
|
+
*/
|
|
31
|
+
export function buildContourMarks({ data, plot, channels, options = {}, alpha, type = 'contour' }) {
|
|
32
|
+
if (!data?.length || !plot.xScale || !plot.yScale) return []
|
|
33
|
+
const groupField = channels.fill ?? channels.color
|
|
34
|
+
const a = resolveAlpha(alpha, type, plot.chartPreset)
|
|
35
|
+
const w = Math.max(1, Math.round(plot.innerWidth))
|
|
36
|
+
const h = Math.max(1, Math.round(plot.innerHeight))
|
|
37
|
+
|
|
38
|
+
const marks = []
|
|
39
|
+
for (const [key, rows] of groupRows(data, groupField)) {
|
|
40
|
+
const pts = screenPoints(rows, plot, channels)
|
|
41
|
+
if (pts.length === 0) continue
|
|
42
|
+
const density = contourDensity()
|
|
43
|
+
.x((d) => d[0])
|
|
44
|
+
.y((d) => d[1])
|
|
45
|
+
.size([w, h])
|
|
46
|
+
.bandwidth(options.bandwidth ?? 20)
|
|
47
|
+
.thresholds(options.thresholds ?? 8)(pts)
|
|
48
|
+
const { fill, stroke } = resolveFillStroke(
|
|
49
|
+
rows[0],
|
|
50
|
+
{ fill: channels.fill, color: channels.color },
|
|
51
|
+
plot.colors
|
|
52
|
+
)
|
|
53
|
+
density.forEach((level, i) => {
|
|
54
|
+
marks.push({
|
|
55
|
+
key: `contour-${String(key)}-${i}`,
|
|
56
|
+
group: key,
|
|
57
|
+
level: i,
|
|
58
|
+
value: level.value,
|
|
59
|
+
d: multiPolygonPath(level.coordinates),
|
|
60
|
+
fill,
|
|
61
|
+
stroke,
|
|
62
|
+
fillAlpha: (a * (i + 1)) / density.length
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
return marks
|
|
67
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One outline per group of the `fill` (else `color`) channel — ggplot's discrete-colour
|
|
3
|
+
* grouping. The hull is computed in SCREEN space after `place`, so it is convex as drawn,
|
|
4
|
+
* whatever the scales (a hull in data space is not convex on a log axis).
|
|
5
|
+
*
|
|
6
|
+
* @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
|
|
7
|
+
*/
|
|
8
|
+
export function buildHullMarks({ data, plot, channels, alpha, type }: {
|
|
9
|
+
data: any[];
|
|
10
|
+
plot: any;
|
|
11
|
+
channels: any;
|
|
12
|
+
options?: any;
|
|
13
|
+
alpha?: number;
|
|
14
|
+
type?: string;
|
|
15
|
+
}): {
|
|
16
|
+
key: string;
|
|
17
|
+
group: unknown;
|
|
18
|
+
d: string;
|
|
19
|
+
fill: string;
|
|
20
|
+
alpha: number;
|
|
21
|
+
at: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
count: number;
|
|
26
|
+
}[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { convexHull, hullPath } from '../../../lib/hull.js'
|
|
2
|
+
import { centroid } from '../../../lib/region.js'
|
|
3
|
+
import { resolveFillStroke, resolveAlpha } from '../aesthetics.js'
|
|
4
|
+
import { groupRows, screenPoints } from '../grouping.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One outline per group of the `fill` (else `color`) channel — ggplot's discrete-colour
|
|
8
|
+
* grouping. The hull is computed in SCREEN space after `place`, so it is convex as drawn,
|
|
9
|
+
* whatever the scales (a hull in data space is not convex on a log axis).
|
|
10
|
+
*
|
|
11
|
+
* @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
|
|
12
|
+
*/
|
|
13
|
+
export function buildHullMarks({ data, plot, channels, alpha, type = 'hull' }) {
|
|
14
|
+
const { xScale, yScale, colors } = plot
|
|
15
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
16
|
+
const groupField = channels.fill ?? channels.color
|
|
17
|
+
const a = resolveAlpha(alpha, type, plot.chartPreset)
|
|
18
|
+
|
|
19
|
+
const marks = []
|
|
20
|
+
for (const [key, rows] of groupRows(data, groupField)) {
|
|
21
|
+
const pts = screenPoints(rows, plot, channels)
|
|
22
|
+
if (pts.length === 0) continue
|
|
23
|
+
const hull = convexHull(pts)
|
|
24
|
+
const { fill } = resolveFillStroke(rows[0], { fill: groupField }, colors)
|
|
25
|
+
marks.push({
|
|
26
|
+
key: `hull-${String(key)}`,
|
|
27
|
+
group: key,
|
|
28
|
+
d: hullPath(hull),
|
|
29
|
+
fill,
|
|
30
|
+
alpha: a,
|
|
31
|
+
at: centroid(hull.map(([x, y]) => ({ x, y }))),
|
|
32
|
+
count: rows.length
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return marks
|
|
36
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { resolveAlpha } from '../aesthetics.js'
|
|
2
|
+
import { runningSpans } from '../../../lib/plot/running.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build renderable waterfall bars. Bar color is semantic (positive/negative/total from
|
|
5
6
|
* options), not the shared palette. Adds fixed `alpha` and carries connector anchors.
|
|
6
7
|
* @param {{ data: any[], plot: any, channels: any, options?: any, alpha?: number, type?: string }} ctx
|
|
7
8
|
*/
|
|
8
|
-
export function buildWaterfallMarks({
|
|
9
|
+
export function buildWaterfallMarks({
|
|
10
|
+
data,
|
|
11
|
+
plot,
|
|
12
|
+
channels,
|
|
13
|
+
options = {},
|
|
14
|
+
alpha,
|
|
15
|
+
type = 'waterfall'
|
|
16
|
+
}) {
|
|
9
17
|
const { xScale, yScale } = plot
|
|
10
18
|
if (!data?.length || !xScale || !yScale) return []
|
|
11
19
|
|
|
@@ -16,28 +24,15 @@ export function buildWaterfallMarks({ data, plot, channels, options = {}, alpha,
|
|
|
16
24
|
const a = resolveAlpha(alpha, type, plot.chartPreset)
|
|
17
25
|
|
|
18
26
|
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
|
|
19
|
-
|
|
27
|
+
const spans = runningSpans(data, channels.y ?? '', totalField)
|
|
20
28
|
|
|
21
29
|
return data.map((d, i) => {
|
|
22
30
|
const xVal = d[channels.x ?? '']
|
|
23
|
-
const
|
|
24
|
-
const isTotal = totalField ? Boolean(d[totalField]) : false
|
|
31
|
+
const { lo, hi, total, isTotal, delta } = spans[i]
|
|
25
32
|
const xPos = xScale(xVal) ?? 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (isTotal) {
|
|
31
|
-
barTop = yScale(Math.max(0, cumulative)) ?? 0
|
|
32
|
-
barBottom = yScale(0) ?? 0
|
|
33
|
-
fill = totalColor
|
|
34
|
-
} else {
|
|
35
|
-
const start = cumulative
|
|
36
|
-
cumulative += yVal
|
|
37
|
-
barTop = yScale(Math.max(start, cumulative)) ?? 0
|
|
38
|
-
barBottom = yScale(Math.min(start, cumulative)) ?? 0
|
|
39
|
-
fill = yVal >= 0 ? positiveColor : negativeColor
|
|
40
|
-
}
|
|
33
|
+
const barTop = yScale(hi) ?? 0
|
|
34
|
+
const barBottom = yScale(lo) ?? 0
|
|
35
|
+
const fill = isTotal ? totalColor : delta >= 0 ? positiveColor : negativeColor
|
|
41
36
|
|
|
42
37
|
const c1 = plot.place(xPos, barTop)
|
|
43
38
|
const c2 = plot.place(xPos + bw, barBottom)
|
|
@@ -51,7 +46,7 @@ export function buildWaterfallMarks({ data, plot, channels, options = {}, alpha,
|
|
|
51
46
|
alpha: a,
|
|
52
47
|
bandStart: xPos,
|
|
53
48
|
bandEnd: xPos + bw,
|
|
54
|
-
cumY: yScale(
|
|
49
|
+
cumY: yScale(total) ?? 0,
|
|
55
50
|
data: d
|
|
56
51
|
}
|
|
57
52
|
})
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export namespace Plot {
|
|
|
19
19
|
export { Ribbon };
|
|
20
20
|
export { Waterfall };
|
|
21
21
|
export { Rule };
|
|
22
|
+
export { Region };
|
|
23
|
+
export { Hull };
|
|
24
|
+
export { Contour };
|
|
22
25
|
export { Radar };
|
|
23
26
|
}
|
|
24
27
|
export namespace PlotLayers { }
|
|
@@ -45,6 +48,9 @@ export { default as GeomHighlight } from "./geoms/Highlight.svelte";
|
|
|
45
48
|
export { default as GeomTrend } from "./geoms/Trend.svelte";
|
|
46
49
|
export { default as GeomJitter } from "./geoms/Jitter.svelte";
|
|
47
50
|
export { default as GeomRule } from "./geoms/Rule.svelte";
|
|
51
|
+
export { default as GeomRegion } from "./geoms/Region.svelte";
|
|
52
|
+
export { default as GeomHull } from "./geoms/Hull.svelte";
|
|
53
|
+
export { default as GeomContour } from "./geoms/Contour.svelte";
|
|
48
54
|
export { default as Chart } from "./Chart.svelte";
|
|
49
55
|
export { default as Sparkline } from "./Sparkline.svelte";
|
|
50
56
|
export { default as Spark } from "./Spark.svelte";
|
|
@@ -83,6 +89,9 @@ import Hexbin from './geoms/Hexbin.svelte';
|
|
|
83
89
|
import Ribbon from './geoms/Ribbon.svelte';
|
|
84
90
|
import Waterfall from './geoms/Waterfall.svelte';
|
|
85
91
|
import Rule from './geoms/Rule.svelte';
|
|
92
|
+
import Region from './geoms/Region.svelte';
|
|
93
|
+
import Hull from './geoms/Hull.svelte';
|
|
94
|
+
import Contour from './geoms/Contour.svelte';
|
|
86
95
|
import Radar from './geoms/Radar.svelte';
|
|
87
96
|
export { createChartPreset, defaultPreset } from "./lib/preset.js";
|
|
88
97
|
export { SparkState, GEOM_CONTRACT } from "./SparkState.svelte.js";
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ import Hexbin from './geoms/Hexbin.svelte'
|
|
|
18
18
|
import Ribbon from './geoms/Ribbon.svelte'
|
|
19
19
|
import Waterfall from './geoms/Waterfall.svelte'
|
|
20
20
|
import Rule from './geoms/Rule.svelte'
|
|
21
|
+
import Region from './geoms/Region.svelte'
|
|
22
|
+
import Hull from './geoms/Hull.svelte'
|
|
23
|
+
import Contour from './geoms/Contour.svelte'
|
|
21
24
|
import Radar from './geoms/Radar.svelte'
|
|
22
25
|
|
|
23
26
|
// Composable Plot primitives — use as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
|
|
@@ -42,6 +45,9 @@ export const Plot = {
|
|
|
42
45
|
Ribbon,
|
|
43
46
|
Waterfall,
|
|
44
47
|
Rule,
|
|
48
|
+
Region,
|
|
49
|
+
Hull,
|
|
50
|
+
Contour,
|
|
45
51
|
Radar
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -77,6 +83,9 @@ export { default as GeomHighlight } from './geoms/Highlight.svelte'
|
|
|
77
83
|
export { default as GeomTrend } from './geoms/Trend.svelte'
|
|
78
84
|
export { default as GeomJitter } from './geoms/Jitter.svelte'
|
|
79
85
|
export { default as GeomRule } from './geoms/Rule.svelte'
|
|
86
|
+
export { default as GeomRegion } from './geoms/Region.svelte'
|
|
87
|
+
export { default as GeomHull } from './geoms/Hull.svelte'
|
|
88
|
+
export { default as GeomContour } from './geoms/Contour.svelte'
|
|
80
89
|
|
|
81
90
|
// Export standalone components
|
|
82
91
|
export { default as Chart } from './Chart.svelte'
|