@uwdata/vgplot 0.4.0 → 0.5.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 +4 -2
- package/dist/vgplot.js +5643 -5842
- package/dist/vgplot.min.js +14 -35
- package/package.json +8 -10
- package/src/api.js +292 -0
- package/src/connect.js +14 -0
- package/src/context.js +20 -0
- package/src/index.js +14 -303
- package/src/inputs.js +24 -0
- package/src/{directives → plot}/attributes.js +14 -5
- package/src/{directives → plot}/interactors.js +8 -6
- package/src/{directives → plot}/legends.js +14 -6
- package/src/{directives → plot}/marks.js +16 -13
- package/src/plot/named-plots.js +49 -0
- package/src/plot/plot.js +9 -0
- package/src/directives/plot.js +0 -39
- package/src/interactors/Highlight.js +0 -101
- package/src/interactors/Interval1D.js +0 -90
- package/src/interactors/Interval2D.js +0 -102
- package/src/interactors/Nearest.js +0 -66
- package/src/interactors/PanZoom.js +0 -121
- package/src/interactors/Toggle.js +0 -111
- package/src/interactors/util/brush.js +0 -45
- package/src/interactors/util/close-to.js +0 -9
- package/src/interactors/util/get-field.js +0 -4
- package/src/interactors/util/invert.js +0 -3
- package/src/interactors/util/patchScreenCTM.js +0 -13
- package/src/interactors/util/sanitize-styles.js +0 -9
- package/src/interactors/util/to-kebab-case.js +0 -9
- package/src/layout/index.js +0 -2
- package/src/legend.js +0 -64
- package/src/marks/ConnectedMark.js +0 -63
- package/src/marks/ContourMark.js +0 -89
- package/src/marks/DenseLineMark.js +0 -146
- package/src/marks/Density1DMark.js +0 -104
- package/src/marks/Density2DMark.js +0 -69
- package/src/marks/Grid2DMark.js +0 -191
- package/src/marks/HexbinMark.js +0 -88
- package/src/marks/Mark.js +0 -195
- package/src/marks/RasterMark.js +0 -122
- package/src/marks/RasterTileMark.js +0 -332
- package/src/marks/RegressionMark.js +0 -117
- package/src/marks/util/bin-field.js +0 -17
- package/src/marks/util/density.js +0 -226
- package/src/marks/util/extent.js +0 -56
- package/src/marks/util/grid.js +0 -57
- package/src/marks/util/handle-param.js +0 -14
- package/src/marks/util/is-arrow-table.js +0 -3
- package/src/marks/util/is-color.js +0 -18
- package/src/marks/util/is-constant-option.js +0 -40
- package/src/marks/util/is-symbol.js +0 -20
- package/src/marks/util/raster.js +0 -44
- package/src/marks/util/stats.js +0 -133
- package/src/marks/util/to-data-array.js +0 -58
- package/src/plot-attributes.js +0 -211
- package/src/plot-renderer.js +0 -161
- package/src/plot.js +0 -136
- package/src/spec/parse-data.js +0 -69
- package/src/spec/parse-spec.js +0 -422
- package/src/spec/to-module.js +0 -465
- package/src/spec/util.js +0 -43
- package/src/symbols.js +0 -3
- package/src/transforms/bin.js +0 -81
- package/src/transforms/index.js +0 -3
- /package/src/{directives → plot}/data.js +0 -0
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { select, zoom, ZoomTransform } from 'd3';
|
|
2
|
-
import { Selection } from '@uwdata/mosaic-core';
|
|
3
|
-
import { isBetween } from '@uwdata/mosaic-sql';
|
|
4
|
-
import { getField } from './util/get-field.js';
|
|
5
|
-
|
|
6
|
-
const asc = (a, b) => a - b;
|
|
7
|
-
|
|
8
|
-
export class PanZoom {
|
|
9
|
-
constructor(mark, {
|
|
10
|
-
x = new Selection(),
|
|
11
|
-
y = new Selection(),
|
|
12
|
-
xfield,
|
|
13
|
-
yfield,
|
|
14
|
-
zoom = true,
|
|
15
|
-
panx = true,
|
|
16
|
-
pany = true
|
|
17
|
-
}) {
|
|
18
|
-
this.mark = mark;
|
|
19
|
-
this.xsel = x;
|
|
20
|
-
this.ysel = y;
|
|
21
|
-
this.xfield = xfield || getField(mark, ['x', 'x1', 'x2']);
|
|
22
|
-
this.yfield = yfield || getField(mark, ['y', 'y1', 'y2']);
|
|
23
|
-
this.zoom = extent(zoom, [0, Infinity], [1, 1]);
|
|
24
|
-
this.panx = this.xsel && panx;
|
|
25
|
-
this.pany = this.ysel && pany;
|
|
26
|
-
|
|
27
|
-
const { plot } = mark;
|
|
28
|
-
if (panx) {
|
|
29
|
-
this.xsel.addEventListener('value', value => {
|
|
30
|
-
if (plot.setAttribute('xDomain', value)) plot.update();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
if (pany) {
|
|
34
|
-
this.ysel.addEventListener('value', value => {
|
|
35
|
-
if (plot.setAttribute('yDomain', value)) plot.update();
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
publish(transform) {
|
|
41
|
-
if (this.panx) {
|
|
42
|
-
const xdom = rescaleX(transform, this.xscale);
|
|
43
|
-
this.xsel.update(this.clause(xdom, this.xfield, this.xscale));
|
|
44
|
-
}
|
|
45
|
-
if (this.pany) {
|
|
46
|
-
const ydom = rescaleY(transform, this.yscale);
|
|
47
|
-
this.ysel.update(this.clause(ydom, this.yfield, this.yscale));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
clause(value, field, scale) {
|
|
52
|
-
return {
|
|
53
|
-
source: this,
|
|
54
|
-
schema: { type: 'interval', scales: [scale] },
|
|
55
|
-
clients: this.mark.plot.markSet,
|
|
56
|
-
value,
|
|
57
|
-
predicate: value ? isBetween(field, value) : null
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
init(svg) {
|
|
62
|
-
this.svg = svg;
|
|
63
|
-
if (this.initialized) return; else this.initialized = true;
|
|
64
|
-
|
|
65
|
-
const { panx, pany, mark: { plot: { element } }, xsel, ysel } = this;
|
|
66
|
-
|
|
67
|
-
this.xscale = svg.scale('x');
|
|
68
|
-
this.yscale = svg.scale('y');
|
|
69
|
-
const rx = this.xscale.range.slice().sort(asc);
|
|
70
|
-
const ry = this.yscale.range.slice().sort(asc);
|
|
71
|
-
const tx = extent(panx, [-Infinity, Infinity], rx);
|
|
72
|
-
const ty = extent(pany, [-Infinity, Infinity], ry);
|
|
73
|
-
|
|
74
|
-
const z = zoom()
|
|
75
|
-
.extent([[rx[0], ry[0]], [rx[1], ry[1]]])
|
|
76
|
-
.scaleExtent(this.zoom)
|
|
77
|
-
.translateExtent([[tx[0], ty[0]], [tx[1], ty[1]]])
|
|
78
|
-
.on('start', () => {
|
|
79
|
-
this.xscale = this.svg.scale('x');
|
|
80
|
-
this.yscale = this.svg.scale('y');
|
|
81
|
-
})
|
|
82
|
-
.on('end', () => element.__zoom = new ZoomTransform(1, 0, 0))
|
|
83
|
-
.on('zoom', ({ transform }) => this.publish(transform));
|
|
84
|
-
|
|
85
|
-
select(element).call(z);
|
|
86
|
-
|
|
87
|
-
if (panx || pany) {
|
|
88
|
-
let enter = false;
|
|
89
|
-
element.addEventListener('mouseenter', () => {
|
|
90
|
-
if (enter) return; else enter = true;
|
|
91
|
-
if (panx) {
|
|
92
|
-
const { xscale, xfield } = this;
|
|
93
|
-
xsel.activate(this.clause(xscale.domain, xfield, xscale));
|
|
94
|
-
}
|
|
95
|
-
if (pany) {
|
|
96
|
-
const { yscale, yfield } = this;
|
|
97
|
-
ysel.activate(this.clause(yscale.domain, yfield, yscale));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
element.addEventListener('mouseleave', () => enter = false);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function extent(ext, defaultTrue, defaultFalse) {
|
|
106
|
-
return ext
|
|
107
|
-
? (Array.isArray(ext) ? ext : defaultTrue)
|
|
108
|
-
: defaultFalse;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function rescaleX(transform, scale) {
|
|
112
|
-
return scale.range
|
|
113
|
-
.map(transform.invertX, transform)
|
|
114
|
-
.map(scale.invert, scale);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function rescaleY(transform, scale) {
|
|
118
|
-
return scale.range
|
|
119
|
-
.map(transform.invertY, transform)
|
|
120
|
-
.map(scale.invert, scale);
|
|
121
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { and, or, isNotDistinct, literal } from '@uwdata/mosaic-sql';
|
|
2
|
-
|
|
3
|
-
export class Toggle {
|
|
4
|
-
constructor(mark, {
|
|
5
|
-
selection,
|
|
6
|
-
channels,
|
|
7
|
-
peers = true
|
|
8
|
-
}) {
|
|
9
|
-
this.value = null;
|
|
10
|
-
this.mark = mark;
|
|
11
|
-
this.selection = selection;
|
|
12
|
-
this.peers = peers;
|
|
13
|
-
this.channels = channels.map(c => {
|
|
14
|
-
const q = c === 'color' ? ['fill', 'stroke']
|
|
15
|
-
: c === 'x' ? ['x', 'x1', 'x2']
|
|
16
|
-
: c === 'y' ? ['y', 'y1', 'y2']
|
|
17
|
-
: [c];
|
|
18
|
-
for (let i = 0; i < q.length; ++i) {
|
|
19
|
-
const f = mark.channelField(q[i]);
|
|
20
|
-
if (f) return {
|
|
21
|
-
field: f.field?.basis || f.field,
|
|
22
|
-
as: f.as
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
throw new Error(`Missing channel: ${c}`);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
clause(value) {
|
|
30
|
-
const { channels, mark } = this;
|
|
31
|
-
let predicate = null;
|
|
32
|
-
|
|
33
|
-
if (value) {
|
|
34
|
-
const clauses = value.map(vals => {
|
|
35
|
-
const list = vals.map((v, i) => {
|
|
36
|
-
return isNotDistinct(channels[i].field, literal(v));
|
|
37
|
-
});
|
|
38
|
-
return list.length > 1 ? and(list) : list[0];
|
|
39
|
-
});
|
|
40
|
-
predicate = clauses.length > 1 ? or(clauses) : clauses[0];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
source: this,
|
|
45
|
-
schema: { type: 'point' },
|
|
46
|
-
clients: this.peers ? mark.plot.markSet : new Set().add(mark),
|
|
47
|
-
value,
|
|
48
|
-
predicate
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
init(svg, selector, accessor) {
|
|
53
|
-
const { mark, channels, selection } = this;
|
|
54
|
-
const { data } = mark;
|
|
55
|
-
accessor = accessor || (target => {
|
|
56
|
-
const datum = data[target.__data__];
|
|
57
|
-
return channels.map(c => datum[c.as]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
selector = selector || `[data-index="${mark.index}"]`;
|
|
61
|
-
const groups = new Set(svg.querySelectorAll(selector));
|
|
62
|
-
|
|
63
|
-
svg.addEventListener('pointerdown', evt => {
|
|
64
|
-
const state = selection.single ? selection.value : this.value;
|
|
65
|
-
const target = evt.target;
|
|
66
|
-
let value = null;
|
|
67
|
-
|
|
68
|
-
if (isTargetElement(groups, target)) {
|
|
69
|
-
const point = accessor(target);
|
|
70
|
-
if (evt.shiftKey && state?.length) {
|
|
71
|
-
value = state.filter(s => neq(s, point));
|
|
72
|
-
if (value.length === state.length) value.push(point);
|
|
73
|
-
} else if (state?.length === 1 && !neq(state[0], point)) {
|
|
74
|
-
value = null;
|
|
75
|
-
} else {
|
|
76
|
-
value = [point];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.value = value;
|
|
81
|
-
if (neqSome(state, value)) {
|
|
82
|
-
selection.update(this.clause(value));
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
svg.addEventListener('pointerenter', () => {
|
|
87
|
-
this.selection.activate(this.clause([this.channels.map(() => 0)]));
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isTargetElement(groups, node) {
|
|
93
|
-
return groups.has(node)
|
|
94
|
-
|| groups.has(node.parentNode)
|
|
95
|
-
|| groups.has(node.parentNode?.parentNode);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function neqSome(a, b) {
|
|
99
|
-
return (a == null || b == null)
|
|
100
|
-
? (a != null || b != null)
|
|
101
|
-
: (a.length !== b.length || a.some((x, i) => neq(x, b[i])));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function neq(a, b) {
|
|
105
|
-
const n = a.length;
|
|
106
|
-
if (b.length !== n) return true;
|
|
107
|
-
for (let i = 0; i < n; ++i) {
|
|
108
|
-
if (a[i] !== b[i]) return true;
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
brush as d3_brush, brushX as d3_brushX, brushY as d3_brushY
|
|
3
|
-
} from 'd3';
|
|
4
|
-
|
|
5
|
-
function wrap(brush) {
|
|
6
|
-
const brushOn = brush.on;
|
|
7
|
-
let enabled = true;
|
|
8
|
-
|
|
9
|
-
function silence(callback) {
|
|
10
|
-
enabled = false;
|
|
11
|
-
callback();
|
|
12
|
-
enabled = true;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
brush.reset = (...args) => {
|
|
16
|
-
silence(() => brush.clear(...args));
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
brush.moveSilent = (...args) => {
|
|
20
|
-
silence(() => brush.move(...args));
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
brush.on = (...args) => {
|
|
24
|
-
if (args.length > 1 && args[1]) {
|
|
25
|
-
// wrap callback to respect enabled flag
|
|
26
|
-
const callback = args[1];
|
|
27
|
-
args[1] = (...event) => enabled && callback(...event);
|
|
28
|
-
}
|
|
29
|
-
return brushOn(...args);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return brush;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function brush() {
|
|
36
|
-
return wrap(d3_brush());
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function brushX() {
|
|
40
|
-
return wrap(d3_brushX());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function brushY() {
|
|
44
|
-
return wrap(d3_brushY());
|
|
45
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Patch the getScreenCTM method to memoize the last non-null
|
|
3
|
-
* result seen. This will let the method continue to function
|
|
4
|
-
* even after the node is removed from the DOM.
|
|
5
|
-
*/
|
|
6
|
-
export function patchScreenCTM() {
|
|
7
|
-
const node = this;
|
|
8
|
-
const getScreenCTM = node.getScreenCTM;
|
|
9
|
-
let memo;
|
|
10
|
-
node.getScreenCTM = () => {
|
|
11
|
-
return node.isConnected ? (memo = getScreenCTM.call(node)) : memo;
|
|
12
|
-
};
|
|
13
|
-
}
|
package/src/layout/index.js
DELETED
package/src/legend.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Toggle } from './interactors/Toggle.js';
|
|
2
|
-
|
|
3
|
-
export class Legend {
|
|
4
|
-
constructor(channel, options) {
|
|
5
|
-
const { as, ...rest } = options;
|
|
6
|
-
this.channel = channel;
|
|
7
|
-
this.options = { label: null, ...rest };
|
|
8
|
-
this.selection = as;
|
|
9
|
-
|
|
10
|
-
this.element = document.createElement('div');
|
|
11
|
-
this.element.setAttribute('class', 'legend');
|
|
12
|
-
this.element.value = this;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
setPlot(plot) {
|
|
16
|
-
const { channel, selection } = this;
|
|
17
|
-
const mark = findMark(plot, channel);
|
|
18
|
-
if (this.selection && mark) {
|
|
19
|
-
this.handler = new Toggle(mark, { selection, channels: [channel] });
|
|
20
|
-
this.selection.addEventListener('value', () => this.update());
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
init(svg) {
|
|
25
|
-
const { channel, options, handler } = this;
|
|
26
|
-
const scale = svg.scale(channel);
|
|
27
|
-
const opt = scale.type === 'ordinal'
|
|
28
|
-
? options
|
|
29
|
-
: { marginTop: 1, tickSize: 2, height: 28, ...options };
|
|
30
|
-
this.legend = svg.legend(channel, opt);
|
|
31
|
-
|
|
32
|
-
if (handler) {
|
|
33
|
-
handler.init(this.legend, ':scope > div', el => [el.__data__]);
|
|
34
|
-
this.update();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
this.element.replaceChildren(this.legend);
|
|
38
|
-
return this.element;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
update() {
|
|
42
|
-
if (!this.legend) return;
|
|
43
|
-
const { value } = this.selection;
|
|
44
|
-
const curr = value && value.length ? new Set(value.map(v => v[0])) : null;
|
|
45
|
-
const nodes = this.legend.querySelectorAll(':scope > div');
|
|
46
|
-
for (const node of nodes) {
|
|
47
|
-
const selected = curr ? curr.has(node.__data__) : true;
|
|
48
|
-
node.style.opacity = selected ? 1 : 0.2;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function findMark({ marks }, channel) {
|
|
54
|
-
const channels = channel === 'color' ? ['fill', 'stroke']
|
|
55
|
-
: channel === 'opacity' ? ['opacity', 'fillOpacity', 'strokeOpacity']
|
|
56
|
-
: null;
|
|
57
|
-
if (channels == null) return null;
|
|
58
|
-
for (let i = marks.length - 1; i > -1; --i) {
|
|
59
|
-
if (marks[i].channelField(channels)) {
|
|
60
|
-
return marks[i];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Query, argmax, argmin, max, min, sql } from '@uwdata/mosaic-sql';
|
|
2
|
-
import { binField } from './util/bin-field.js';
|
|
3
|
-
import { filteredExtent } from './util/extent.js';
|
|
4
|
-
import { Mark } from './Mark.js';
|
|
5
|
-
|
|
6
|
-
export class ConnectedMark extends Mark {
|
|
7
|
-
constructor(type, source, encodings) {
|
|
8
|
-
const dim = type.endsWith('X') ? 'y' : 'x';
|
|
9
|
-
const req = { [dim]: ['count', 'min', 'max'] };
|
|
10
|
-
|
|
11
|
-
super(type, source, encodings, req);
|
|
12
|
-
this.dim = dim;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
query(filter = []) {
|
|
16
|
-
const { plot, dim, source, stats } = this;
|
|
17
|
-
const { optimize = true } = source.options || {};
|
|
18
|
-
const { field, as } = this.channelField(dim);
|
|
19
|
-
const q = super.query(filter);
|
|
20
|
-
|
|
21
|
-
if (optimize) {
|
|
22
|
-
// TODO: handle stacked data
|
|
23
|
-
const { column } = field;
|
|
24
|
-
const { count, max, min } = stats[column];
|
|
25
|
-
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
26
|
-
|
|
27
|
-
const [lo, hi] = filteredExtent(filter, column) || [min, max];
|
|
28
|
-
const scale = (hi - lo) / (max - min);
|
|
29
|
-
if (count * scale > size * 4) {
|
|
30
|
-
const dd = binField(this, dim, as);
|
|
31
|
-
const val = this.channelField(dim === 'x' ? 'y' : 'x').as;
|
|
32
|
-
const cols = q.select().map(c => c.as).filter(c => c !== as && c !== val);
|
|
33
|
-
return m4(q, dd, as, val, lo, hi, size, cols);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return q.orderby(as);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* M4 is an optimization for value-preserving time-series aggregation
|
|
43
|
-
* (http://www.vldb.org/pvldb/vol7/p797-jugel.pdf). This implementation uses
|
|
44
|
-
* an efficient version with a single scan and the aggregate function
|
|
45
|
-
* argmin and argmax, following https://arxiv.org/pdf/2306.03714.pdf.
|
|
46
|
-
*/
|
|
47
|
-
function m4(input, bx, x, y, lo, hi, width, cols = []) {
|
|
48
|
-
const bins = sql`FLOOR(${width / (hi - lo)}::DOUBLE * (${bx} - ${+lo}::DOUBLE))::INTEGER`;
|
|
49
|
-
|
|
50
|
-
const q = (sel) => Query
|
|
51
|
-
.from(input)
|
|
52
|
-
.select(sel)
|
|
53
|
-
.groupby(bins, cols);
|
|
54
|
-
|
|
55
|
-
return Query
|
|
56
|
-
.union(
|
|
57
|
-
q([{ [x]: min(x), [y]: argmin(y, x) }, ...cols]),
|
|
58
|
-
q([{ [x]: max(x), [y]: argmax(y, x) }, ...cols]),
|
|
59
|
-
q([{ [x]: argmin(x, y), [y]: min(y) }, ...cols]),
|
|
60
|
-
q([{ [x]: argmax(x, y), [y]: max(y) }, ...cols])
|
|
61
|
-
)
|
|
62
|
-
.orderby(cols, x);
|
|
63
|
-
}
|
package/src/marks/ContourMark.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { contours, max } from 'd3';
|
|
2
|
-
import { handleParam } from './util/handle-param.js';
|
|
3
|
-
import { Grid2DMark } from './Grid2DMark.js';
|
|
4
|
-
import { channelOption } from './Mark.js';
|
|
5
|
-
|
|
6
|
-
export class ContourMark extends Grid2DMark {
|
|
7
|
-
constructor(source, options) {
|
|
8
|
-
const { thresholds = 10, ...channels } = options;
|
|
9
|
-
super('geo', source, channels);
|
|
10
|
-
handleParam(this, 'thresholds', thresholds, () => {
|
|
11
|
-
return this.grids ? this.contours().update() : null
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
convolve() {
|
|
16
|
-
return super.convolve().contours();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
contours() {
|
|
20
|
-
const { bins, densityMap, kde, thresholds, groupby, plot } = this;
|
|
21
|
-
|
|
22
|
-
let tz = thresholds;
|
|
23
|
-
if (!Array.isArray(tz)) {
|
|
24
|
-
const scale = max(kde.map(k => max(k)));
|
|
25
|
-
tz = Array.from({length: tz - 1}, (_, i) => (scale * (i + 1)) / tz);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (densityMap.fill || densityMap.stroke) {
|
|
29
|
-
if (this.plot.getAttribute('colorScale') !== 'log') {
|
|
30
|
-
this.plot.setAttribute('colorZero', true);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// transform contours into data space coordinates
|
|
35
|
-
// so we play nice with scale domains & axes
|
|
36
|
-
const [nx, ny] = bins;
|
|
37
|
-
const [x0, x1] = plot.getAttribute('xDomain');
|
|
38
|
-
const [y0, y1] = plot.getAttribute('yDomain');
|
|
39
|
-
const sx = (x1 - x0) / nx;
|
|
40
|
-
const sy = (y1 - y0) / ny;
|
|
41
|
-
const xo = +x0;
|
|
42
|
-
const yo = +y0;
|
|
43
|
-
const x = v => xo + v * sx;
|
|
44
|
-
const y = v => yo + v * sy;
|
|
45
|
-
const contour = contours().size(bins);
|
|
46
|
-
|
|
47
|
-
// generate contours
|
|
48
|
-
this.data = kde.flatMap(k => tz.map(t => {
|
|
49
|
-
const c = transform(contour.contour(k, t), x, y);
|
|
50
|
-
groupby.forEach((name, i) => c[name] = k.key[i]);
|
|
51
|
-
c.density = t;
|
|
52
|
-
return c;
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
return this;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
plotSpecs() {
|
|
59
|
-
const { type, channels, densityMap, data } = this;
|
|
60
|
-
const options = {};
|
|
61
|
-
for (const c of channels) {
|
|
62
|
-
const { channel } = c;
|
|
63
|
-
if (channel !== 'x' && channel !== 'y') {
|
|
64
|
-
options[channel] = channelOption(c);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (densityMap.fill) options.fill = 'density';
|
|
68
|
-
if (densityMap.stroke) options.stroke = 'density';
|
|
69
|
-
return [{ type, data, options }];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function transform(geometry, x, y) {
|
|
74
|
-
function transformPolygon(coordinates) {
|
|
75
|
-
coordinates.forEach(transformRing);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function transformRing(coordinates) {
|
|
79
|
-
coordinates.forEach(transformPoint);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function transformPoint(coordinates) {
|
|
83
|
-
coordinates[0] = x(coordinates[0]);
|
|
84
|
-
coordinates[1] = y(coordinates[1]);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
geometry.coordinates.forEach(transformPolygon);
|
|
88
|
-
return geometry;
|
|
89
|
-
}
|