@rokkit/chart 1.7.0 → 1.8.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/README.md +13 -0
- package/dist/Plot.svelte +38 -10
- package/dist/Plot.svelte.d.ts +7 -2
- 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/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/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/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/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/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'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The segment of `y = slope·x + intercept` that lies inside a rectangular domain, or null
|
|
3
|
+
* when the line misses it.
|
|
4
|
+
*
|
|
5
|
+
* Clipping in DATA space, not pixels, is what keeps a reference line honest under zoom: the
|
|
6
|
+
* scales' domains are already the visible window, so the same call is correct at any zoom.
|
|
7
|
+
* A line that merely touches a corner returns a zero-length segment rather than null — it is
|
|
8
|
+
* inside the domain, just barely.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} slope
|
|
11
|
+
* @param {number} intercept
|
|
12
|
+
* @param {[number, number]} xDomain
|
|
13
|
+
* @param {[number, number]} yDomain
|
|
14
|
+
* @returns {{ x1: number, y1: number, x2: number, y2: number } | null}
|
|
15
|
+
*/
|
|
16
|
+
export function clipAbline(slope: number, intercept: number, xDomain: [number, number], yDomain: [number, number]): {
|
|
17
|
+
x1: number;
|
|
18
|
+
y1: number;
|
|
19
|
+
x2: number;
|
|
20
|
+
y2: number;
|
|
21
|
+
} | null;
|
|
22
|
+
/**
|
|
23
|
+
* Where a sloped line's label goes: its midpoint, rotated to run along it.
|
|
24
|
+
*
|
|
25
|
+
* Not the end, where an axis-aligned rule puts its label — a sloped line's ends are corners
|
|
26
|
+
* of the plot, exactly where points and axis ticks crowd. The angle is folded into
|
|
27
|
+
* (-90°, 90°] so the text never reads upside down, whichever way the line was drawn.
|
|
28
|
+
*
|
|
29
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }} seg screen coordinates
|
|
30
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
31
|
+
*/
|
|
32
|
+
export function slopeLabel({ x1, y1, x2, y2 }: {
|
|
33
|
+
x1: number;
|
|
34
|
+
y1: number;
|
|
35
|
+
x2: number;
|
|
36
|
+
y2: number;
|
|
37
|
+
}): {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
angle: number;
|
|
41
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The segment of `y = slope·x + intercept` that lies inside a rectangular domain, or null
|
|
3
|
+
* when the line misses it.
|
|
4
|
+
*
|
|
5
|
+
* Clipping in DATA space, not pixels, is what keeps a reference line honest under zoom: the
|
|
6
|
+
* scales' domains are already the visible window, so the same call is correct at any zoom.
|
|
7
|
+
* A line that merely touches a corner returns a zero-length segment rather than null — it is
|
|
8
|
+
* inside the domain, just barely.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} slope
|
|
11
|
+
* @param {number} intercept
|
|
12
|
+
* @param {[number, number]} xDomain
|
|
13
|
+
* @param {[number, number]} yDomain
|
|
14
|
+
* @returns {{ x1: number, y1: number, x2: number, y2: number } | null}
|
|
15
|
+
*/
|
|
16
|
+
export function clipAbline(slope, intercept, xDomain, yDomain) {
|
|
17
|
+
const [x0, x1] = [Math.min(...xDomain), Math.max(...xDomain)]
|
|
18
|
+
const [y0, y1] = [Math.min(...yDomain), Math.max(...yDomain)]
|
|
19
|
+
const at = (x) => slope * x + intercept
|
|
20
|
+
|
|
21
|
+
if (slope === 0) {
|
|
22
|
+
return intercept < y0 || intercept > y1
|
|
23
|
+
? null
|
|
24
|
+
: { x1: x0, y1: intercept, x2: x1, y2: intercept }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Where the line crosses the lower and upper y bounds, ordered along x.
|
|
28
|
+
const xAtY0 = (y0 - intercept) / slope
|
|
29
|
+
const xAtY1 = (y1 - intercept) / slope
|
|
30
|
+
const lo = Math.max(x0, Math.min(xAtY0, xAtY1))
|
|
31
|
+
const hi = Math.min(x1, Math.max(xAtY0, xAtY1))
|
|
32
|
+
if (lo > hi) return null
|
|
33
|
+
return { x1: lo, y1: at(lo), x2: hi, y2: at(hi) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Where a sloped line's label goes: its midpoint, rotated to run along it.
|
|
38
|
+
*
|
|
39
|
+
* Not the end, where an axis-aligned rule puts its label — a sloped line's ends are corners
|
|
40
|
+
* of the plot, exactly where points and axis ticks crowd. The angle is folded into
|
|
41
|
+
* (-90°, 90°] so the text never reads upside down, whichever way the line was drawn.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }} seg screen coordinates
|
|
44
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
45
|
+
*/
|
|
46
|
+
export function slopeLabel({ x1, y1, x2, y2 }) {
|
|
47
|
+
let angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
|
|
48
|
+
if (angle > 90) angle -= 180
|
|
49
|
+
if (angle <= -90) angle += 180
|
|
50
|
+
return { x: (x1 + x2) / 2, y: (y1 + y2) / 2, angle }
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hull vertices, counter-clockwise in a y-up frame, starting from the lowest-x point.
|
|
3
|
+
* Collinear edge points are dropped; duplicates collapse; one or two distinct points come
|
|
4
|
+
* back as-is (a dot, a segment).
|
|
5
|
+
*
|
|
6
|
+
* @param {Pt[]} points
|
|
7
|
+
* @returns {Pt[]}
|
|
8
|
+
*/
|
|
9
|
+
export function convexHull(points: Pt[]): Pt[];
|
|
10
|
+
/**
|
|
11
|
+
* Closed SVG path through hull vertices. One vertex is a zero-length subpath and two are a
|
|
12
|
+
* segment — with a round cap and a wide stroke those render as a dot and a capsule, which is
|
|
13
|
+
* how `Hull` pads every group, whatever its size, with one element.
|
|
14
|
+
*
|
|
15
|
+
* @param {Pt[]} hull
|
|
16
|
+
*/
|
|
17
|
+
export function hullPath(hull: Pt[]): string;
|
|
18
|
+
export type Pt = [number, number];
|
package/dist/lib/hull.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convex hull (Andrew's monotone chain), O(n log n), and its SVG path.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled rather than `d3-polygon` because the degenerate cases matter here and d3 returns
|
|
5
|
+
* null for them: a group with one or two members is still a group, and an outline that
|
|
6
|
+
* silently disappears for a small package is the same lie as an edge that disappears for an
|
|
7
|
+
* unresolved call.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @typedef {[number, number]} Pt */
|
|
11
|
+
|
|
12
|
+
/** z of (b - a) × (c - a): > 0 is a left (counter-clockwise) turn. */
|
|
13
|
+
const cross = (/** @type {Pt} */ a, /** @type {Pt} */ b, /** @type {Pt} */ c) =>
|
|
14
|
+
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hull vertices, counter-clockwise in a y-up frame, starting from the lowest-x point.
|
|
18
|
+
* Collinear edge points are dropped; duplicates collapse; one or two distinct points come
|
|
19
|
+
* back as-is (a dot, a segment).
|
|
20
|
+
*
|
|
21
|
+
* @param {Pt[]} points
|
|
22
|
+
* @returns {Pt[]}
|
|
23
|
+
*/
|
|
24
|
+
export function convexHull(points) {
|
|
25
|
+
const seen = new Set()
|
|
26
|
+
const pts = []
|
|
27
|
+
for (const p of points) {
|
|
28
|
+
const key = `${p[0]},${p[1]}`
|
|
29
|
+
if (!seen.has(key)) {
|
|
30
|
+
seen.add(key)
|
|
31
|
+
pts.push(p)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
pts.sort((a, b) => a[0] - b[0] || a[1] - b[1])
|
|
35
|
+
if (pts.length < 3) return pts
|
|
36
|
+
|
|
37
|
+
/** @param {Pt[]} seq */
|
|
38
|
+
const chain = (seq) => {
|
|
39
|
+
/** @type {Pt[]} */
|
|
40
|
+
const out = []
|
|
41
|
+
for (const p of seq) {
|
|
42
|
+
while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], p) <= 0) out.pop()
|
|
43
|
+
out.push(p)
|
|
44
|
+
}
|
|
45
|
+
out.pop()
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
const hull = [...chain(pts), ...chain([...pts].reverse())]
|
|
49
|
+
// All points collinear: both chains collapse to the two extremes.
|
|
50
|
+
return hull.length < 2 ? [pts[0], pts[pts.length - 1]] : hull
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Closed SVG path through hull vertices. One vertex is a zero-length subpath and two are a
|
|
55
|
+
* segment — with a round cap and a wide stroke those render as a dot and a capsule, which is
|
|
56
|
+
* how `Hull` pads every group, whatever its size, with one element.
|
|
57
|
+
*
|
|
58
|
+
* @param {Pt[]} hull
|
|
59
|
+
*/
|
|
60
|
+
export function hullPath(hull) {
|
|
61
|
+
if (hull.length === 0) return ''
|
|
62
|
+
const fmt = (/** @type {number} */ n) => Math.round(n * 1000) / 1000
|
|
63
|
+
return `M${hull.map(([x, y]) => `${fmt(x)},${fmt(y)}`).join('L')}Z`
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band categories ordered by their summed value — "sort bars by size". Null when not sorting.
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, unknown>[][]} datasets - every geom's rows
|
|
5
|
+
* @param {string | undefined} xf
|
|
6
|
+
* @param {string | undefined} yf
|
|
7
|
+
* @param {'asc' | 'desc' | undefined} sort
|
|
8
|
+
*/
|
|
9
|
+
export function sortedBandDomain(datasets: Record<string, unknown>[][], xf: string | undefined, yf: string | undefined, sort: "asc" | "desc" | undefined): any[] | null;
|
|
10
|
+
/**
|
|
11
|
+
* A box/violin spans its whiskers (`iqr_min` / `iqr_max`) and every outlier.
|
|
12
|
+
*
|
|
13
|
+
* @param {Record<string, any>[]} rows - box-stat rows
|
|
14
|
+
*/
|
|
15
|
+
export function boxDomain(rows: Record<string, any>[]): number[] | null;
|
|
16
|
+
/**
|
|
17
|
+
* A stacked bar reaches its column total; a 100%-filled one reaches 1.
|
|
18
|
+
*
|
|
19
|
+
* Summed per (x, stack key), keeping the LAST value per key — the builder's own lookup does the
|
|
20
|
+
* same, so rows sharing a key under an identity stat do not double-count. The stack key is the
|
|
21
|
+
* builder's (`stackFieldOf`); with none, the builder draws plain bars, so there is no total to
|
|
22
|
+
* size to and the normal extent applies (null).
|
|
23
|
+
*
|
|
24
|
+
* @param {Record<string, unknown>[]} rows
|
|
25
|
+
* @param {{ x?: string, y?: string, group?: string, fill?: string, color?: string, pattern?: string }} channels
|
|
26
|
+
* @param {'stack' | 'fill' | undefined} position
|
|
27
|
+
*/
|
|
28
|
+
export function stackDomain(rows: Record<string, unknown>[], channels: {
|
|
29
|
+
x?: string;
|
|
30
|
+
y?: string;
|
|
31
|
+
group?: string;
|
|
32
|
+
fill?: string;
|
|
33
|
+
color?: string;
|
|
34
|
+
pattern?: string;
|
|
35
|
+
}, position: "stack" | "fill" | undefined): number[] | null;
|
|
36
|
+
/**
|
|
37
|
+
* A waterfall spans its running total: each step from where the last ended, a total row from 0.
|
|
38
|
+
*
|
|
39
|
+
* @param {Record<string, unknown>[]} rows
|
|
40
|
+
* @param {string} field
|
|
41
|
+
* @param {string} [totalField] - rows flagged here are totals, drawn from zero
|
|
42
|
+
*/
|
|
43
|
+
export function waterfallDomain(rows: Record<string, unknown>[], field: string, totalField?: string): number[] | null;
|
|
44
|
+
/**
|
|
45
|
+
* The value-axis domain the mounted geoms' geometry demands, or null for the plain extent.
|
|
46
|
+
*
|
|
47
|
+
* @param {Array<{ id: string, type: string, options?: Record<string, any> }>} geoms
|
|
48
|
+
* @param {(id: string) => Record<string, unknown>[]} rowsOf - a geom's rows (post-stat)
|
|
49
|
+
* @param {{ x?: string, fill?: string, color?: string, pattern?: string }} channels - effective
|
|
50
|
+
* @param {string} field - the value channel
|
|
51
|
+
*/
|
|
52
|
+
export function resolveValueDomain(geoms: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
type: string;
|
|
55
|
+
options?: Record<string, any>;
|
|
56
|
+
}>, rowsOf: (id: string) => Record<string, unknown>[], channels: {
|
|
57
|
+
x?: string;
|
|
58
|
+
fill?: string;
|
|
59
|
+
color?: string;
|
|
60
|
+
pattern?: string;
|
|
61
|
+
}, field: string): number[] | null;
|