@jrkasprzyk/parcoord-es 3.0.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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/parcoords.css +61 -0
- package/dist/parcoords.esm.js +4132 -0
- package/dist/parcoords.esm.js.map +1 -0
- package/dist/parcoords.js +4130 -0
- package/dist/parcoords.js.map +1 -0
- package/dist/parcoords.standalone.js +11233 -0
- package/dist/parcoords.standalone.js.map +1 -0
- package/package.json +89 -0
- package/src/api/adjacentPairs.js +10 -0
- package/src/api/applyAxisConfig.js +35 -0
- package/src/api/applyDimensionDefaults.js +25 -0
- package/src/api/autoscale.js +133 -0
- package/src/api/axisDots.js +27 -0
- package/src/api/brushMode.js +50 -0
- package/src/api/brushReset.js +34 -0
- package/src/api/brushable.js +81 -0
- package/src/api/clear.js +21 -0
- package/src/api/commonScale.js +45 -0
- package/src/api/computeRealCentroids.js +8 -0
- package/src/api/createAxes.js +101 -0
- package/src/api/detectDimensionTypes.js +12 -0
- package/src/api/detectDimensions.js +7 -0
- package/src/api/flip.js +11 -0
- package/src/api/getOrderedDimensionKeys.js +8 -0
- package/src/api/highlight.js +26 -0
- package/src/api/init.js +53 -0
- package/src/api/interactive.js +7 -0
- package/src/api/intersection.js +16 -0
- package/src/api/mark.js +20 -0
- package/src/api/mergeParcoords.js +86 -0
- package/src/api/removeAxes.js +9 -0
- package/src/api/render.js +26 -0
- package/src/api/renderBrushed.js +39 -0
- package/src/api/renderDefault.js +27 -0
- package/src/api/renderMarked.js +34 -0
- package/src/api/reorder.js +37 -0
- package/src/api/reorderable.js +41 -0
- package/src/api/resize.js +36 -0
- package/src/api/scale.js +10 -0
- package/src/api/selected.js +209 -0
- package/src/api/shadows.js +9 -0
- package/src/api/sortDimensions.js +13 -0
- package/src/api/sortDimensionsByRowData.js +20 -0
- package/src/api/toString.js +11 -0
- package/src/api/toType.js +9 -0
- package/src/api/toTypeCoerceNumbers.js +7 -0
- package/src/api/unhighlight.js +12 -0
- package/src/api/unmark.js +12 -0
- package/src/api/updateAxes.js +103 -0
- package/src/bindEvents.js +60 -0
- package/src/brush/1d/brushExtents.js +73 -0
- package/src/brush/1d/brushFor.js +92 -0
- package/src/brush/1d/brushReset.js +36 -0
- package/src/brush/1d/index.js +20 -0
- package/src/brush/1d/install.js +41 -0
- package/src/brush/1d/selected.js +79 -0
- package/src/brush/1d/uninstall.js +12 -0
- package/src/brush/1d-multi/brushExtents.js +101 -0
- package/src/brush/1d-multi/brushFor.js +13 -0
- package/src/brush/1d-multi/brushReset.js +53 -0
- package/src/brush/1d-multi/drawBrushes.js +36 -0
- package/src/brush/1d-multi/index.js +20 -0
- package/src/brush/1d-multi/install.js +29 -0
- package/src/brush/1d-multi/newBrush.js +102 -0
- package/src/brush/1d-multi/selected.js +160 -0
- package/src/brush/1d-multi/uninstall.js +12 -0
- package/src/brush/angular/brushReset.js +14 -0
- package/src/brush/angular/index.js +19 -0
- package/src/brush/angular/install.js +141 -0
- package/src/brush/angular/onDrag.js +109 -0
- package/src/brush/angular/onDragEnd.js +30 -0
- package/src/brush/angular/onDragStart.js +38 -0
- package/src/brush/angular/removeStrum.js +12 -0
- package/src/brush/angular/selected.js +63 -0
- package/src/brush/angular/uninstall.js +17 -0
- package/src/brush/angular/util/hypothenuse.js +3 -0
- package/src/brush/consecutive.js +11 -0
- package/src/brush/dimensionsForPoint.js +32 -0
- package/src/brush/invertByScale.js +24 -0
- package/src/brush/strums/brushReset.js +14 -0
- package/src/brush/strums/index.js +19 -0
- package/src/brush/strums/install.js +81 -0
- package/src/brush/strums/onDrag.js +83 -0
- package/src/brush/strums/onDragEnd.js +20 -0
- package/src/brush/strums/onDragStart.js +33 -0
- package/src/brush/strums/removeStrum.js +10 -0
- package/src/brush/strums/selected.js +52 -0
- package/src/brush/strums/uninstall.js +16 -0
- package/src/index.js +226 -0
- package/src/parallel-coordinates.css +61 -0
- package/src/state/defaultConfig.js +36 -0
- package/src/state/index.js +86 -0
- package/src/state/sideEffects.js +89 -0
- package/src/util/colorPath.js +71 -0
- package/src/util/computeCentroids.js +44 -0
- package/src/util/computeClusterCentroids.js +33 -0
- package/src/util/computeControlPoints.js +38 -0
- package/src/util/dimensionLabels.js +11 -0
- package/src/util/flipAxisAndUpdatePCP.js +19 -0
- package/src/util/functor.js +3 -0
- package/src/util/getRange.js +24 -0
- package/src/util/getset.js +23 -0
- package/src/util/height.js +3 -0
- package/src/util/isBrushed.js +15 -0
- package/src/util/renderQueue.js +67 -0
- package/src/util/rotateLabels.js +18 -0
- package/src/util/width.js +3 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import onDragEnd from './onDragEnd';
|
|
2
|
+
import removeStrum from './removeStrum';
|
|
3
|
+
|
|
4
|
+
const brushReset = (brushGroup, state, config, pc, events) => () => {
|
|
5
|
+
const ids = Object.getOwnPropertyNames(state.arcs).filter(d => !isNaN(d));
|
|
6
|
+
|
|
7
|
+
ids.forEach(d => {
|
|
8
|
+
state.arcs.active = d;
|
|
9
|
+
removeStrum(state, pc);
|
|
10
|
+
});
|
|
11
|
+
onDragEnd(brushGroup, state, config, pc, events)();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default brushReset;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import uninstall from './uninstall';
|
|
2
|
+
import install from './install';
|
|
3
|
+
import selected from './selected';
|
|
4
|
+
|
|
5
|
+
const installAngularBrush = (brushGroup, config, pc, events, xscale) => {
|
|
6
|
+
const state = {
|
|
7
|
+
arcs: {},
|
|
8
|
+
strumRect: {},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
brushGroup.modes['angular'] = {
|
|
12
|
+
install: install(brushGroup, state, config, pc, events, xscale),
|
|
13
|
+
uninstall: uninstall(state, pc),
|
|
14
|
+
selected: selected(brushGroup, state, config),
|
|
15
|
+
brushState: () => state.arcs,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default installAngularBrush;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { drag } from 'd3-drag';
|
|
2
|
+
import onDragEnd from './onDragEnd';
|
|
3
|
+
import onDrag from './onDrag';
|
|
4
|
+
import onDragStart from './onDragStart';
|
|
5
|
+
import removeStrum from './removeStrum';
|
|
6
|
+
import brushReset from './brushReset';
|
|
7
|
+
import w from '../../util/width';
|
|
8
|
+
import h from '../../util/height';
|
|
9
|
+
|
|
10
|
+
import hypothenuse from './util/hypothenuse';
|
|
11
|
+
import consecutive from '../consecutive';
|
|
12
|
+
|
|
13
|
+
// returns angles in [-PI/2, PI/2]
|
|
14
|
+
const angle = (p1, p2) => {
|
|
15
|
+
const a = p1[0] - p2[0],
|
|
16
|
+
b = p1[1] - p2[1],
|
|
17
|
+
c = hypothenuse(a, b);
|
|
18
|
+
|
|
19
|
+
return Math.asin(b / c);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const endAngle = state => id => {
|
|
23
|
+
const arc = state.arcs[id];
|
|
24
|
+
if (arc === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
let sAngle = angle(arc.p1, arc.p2),
|
|
28
|
+
uAngle = -sAngle + Math.PI / 2;
|
|
29
|
+
|
|
30
|
+
if (arc.p1[0] > arc.p2[0]) {
|
|
31
|
+
uAngle = 2 * Math.PI - uAngle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return uAngle;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const startAngle = state => id => {
|
|
38
|
+
const arc = state.arcs[id];
|
|
39
|
+
if (arc === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let sAngle = angle(arc.p1, arc.p3),
|
|
44
|
+
uAngle = -sAngle + Math.PI / 2;
|
|
45
|
+
|
|
46
|
+
if (arc.p1[0] > arc.p3[0]) {
|
|
47
|
+
uAngle = 2 * Math.PI - uAngle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return uAngle;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const length = state => id => {
|
|
54
|
+
const arc = state.arcs[id];
|
|
55
|
+
|
|
56
|
+
if (arc === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const a = arc.p1[0] - arc.p2[0],
|
|
61
|
+
b = arc.p1[1] - arc.p2[1];
|
|
62
|
+
|
|
63
|
+
return hypothenuse(a, b);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const install = (brushGroup, state, config, pc, events, xscale) => () => {
|
|
67
|
+
if (!pc.g()) {
|
|
68
|
+
pc.createAxes();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const _drag = drag();
|
|
72
|
+
|
|
73
|
+
// Map of current arcs. arcs are stored per segment of the PC. A segment,
|
|
74
|
+
// being the area between two axes. The left most area is indexed at 0.
|
|
75
|
+
state.arcs.active = undefined;
|
|
76
|
+
// Returns the width of the PC segment where currently a arc is being
|
|
77
|
+
// placed. NOTE: even though they are evenly spaced in our current
|
|
78
|
+
// implementation, we keep for when non-even spaced segments are supported as
|
|
79
|
+
// well.
|
|
80
|
+
state.arcs.width = id => {
|
|
81
|
+
const arc = state.arcs[id];
|
|
82
|
+
return arc === undefined ? undefined : arc.maxX - arc.minX;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// returns angles in [0, 2 * PI]
|
|
86
|
+
state.arcs.endAngle = endAngle(state);
|
|
87
|
+
state.arcs.startAngle = startAngle(state);
|
|
88
|
+
state.arcs.length = length(state);
|
|
89
|
+
|
|
90
|
+
pc.on('axesreorder.arcs', () => {
|
|
91
|
+
const ids = Object.getOwnPropertyNames(arcs).filter(d => !isNaN(d));
|
|
92
|
+
|
|
93
|
+
if (ids.length > 0) {
|
|
94
|
+
// We have some arcs, which might need to be removed.
|
|
95
|
+
ids.forEach(d => {
|
|
96
|
+
const dims = arcs[d].dims;
|
|
97
|
+
state.arcs.active = d;
|
|
98
|
+
// If the two dimensions of the current arc are not next to each other
|
|
99
|
+
// any more, than we'll need to remove the arc. Otherwise we keep it.
|
|
100
|
+
if (!consecutive(dims)(dims.left, dims.right)) {
|
|
101
|
+
removeStrum(state, pc);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
onDragEnd(brushGroup, state, config, pc, events)();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Add a new svg group in which we draw the arcs.
|
|
109
|
+
pc.selection
|
|
110
|
+
.select('svg')
|
|
111
|
+
.append('g')
|
|
112
|
+
.attr('id', 'arcs')
|
|
113
|
+
.attr(
|
|
114
|
+
'transform',
|
|
115
|
+
'translate(' + config.margin.left + ',' + config.margin.top + ')'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Install the required brushReset function
|
|
119
|
+
pc.brushReset = brushReset(brushGroup, state, config, pc, events);
|
|
120
|
+
|
|
121
|
+
_drag
|
|
122
|
+
.on('start', onDragStart(state, config, pc, xscale))
|
|
123
|
+
.on('drag', onDrag(brushGroup, state, config, pc, events))
|
|
124
|
+
.on('end', onDragEnd(brushGroup, state, config, pc, events));
|
|
125
|
+
|
|
126
|
+
// NOTE: The styling needs to be done here and not in the css. This is because
|
|
127
|
+
// for 1D brushing, the canvas layers should not listen to
|
|
128
|
+
// pointer-events._.
|
|
129
|
+
state.strumRect = pc.selection
|
|
130
|
+
.select('svg')
|
|
131
|
+
.insert('rect', 'g#arcs')
|
|
132
|
+
.attr('id', 'arc-events')
|
|
133
|
+
.attr('x', config.margin.left)
|
|
134
|
+
.attr('y', config.margin.top)
|
|
135
|
+
.attr('width', w(config))
|
|
136
|
+
.attr('height', h(config) + 2)
|
|
137
|
+
.style('opacity', 0)
|
|
138
|
+
.call(_drag);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default install;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { select } from 'd3-selection';
|
|
2
|
+
import { drag } from 'd3-drag';
|
|
3
|
+
import onDragEnd from './onDragEnd';
|
|
4
|
+
|
|
5
|
+
const drawStrum = (brushGroup, state, config, pc, events, arc, activePoint) => {
|
|
6
|
+
const svg = pc.selection.select('svg').select('g#arcs'),
|
|
7
|
+
id = arc.dims.i,
|
|
8
|
+
points = [arc.p2, arc.p3],
|
|
9
|
+
_line = svg
|
|
10
|
+
.selectAll('line#arc-' + id)
|
|
11
|
+
.data([{ p1: arc.p1, p2: arc.p2 }, { p1: arc.p1, p2: arc.p3 }]),
|
|
12
|
+
circles = svg.selectAll('circle#arc-' + id).data(points),
|
|
13
|
+
_drag = drag(),
|
|
14
|
+
_path = svg.selectAll('path#arc-' + id).data([arc]);
|
|
15
|
+
|
|
16
|
+
_path
|
|
17
|
+
.enter()
|
|
18
|
+
.append('path')
|
|
19
|
+
.attr('id', 'arc-' + id)
|
|
20
|
+
.attr('class', 'arc')
|
|
21
|
+
.style('fill', 'orange')
|
|
22
|
+
.style('opacity', 0.5);
|
|
23
|
+
|
|
24
|
+
_path
|
|
25
|
+
.attr('d', arc.arc)
|
|
26
|
+
.attr('transform', 'translate(' + arc.p1[0] + ',' + arc.p1[1] + ')');
|
|
27
|
+
|
|
28
|
+
_line
|
|
29
|
+
.enter()
|
|
30
|
+
.append('line')
|
|
31
|
+
.attr('id', 'arc-' + id)
|
|
32
|
+
.attr('class', 'arc');
|
|
33
|
+
|
|
34
|
+
_line
|
|
35
|
+
.attr('x1', d => d.p1[0])
|
|
36
|
+
.attr('y1', d => d.p1[1])
|
|
37
|
+
.attr('x2', d => d.p2[0])
|
|
38
|
+
.attr('y2', d => d.p2[1])
|
|
39
|
+
.attr('stroke', 'black')
|
|
40
|
+
.attr('stroke-width', 2);
|
|
41
|
+
|
|
42
|
+
_drag
|
|
43
|
+
.on('drag', (event, d) => {
|
|
44
|
+
const i = points.indexOf(d) + 2;
|
|
45
|
+
|
|
46
|
+
arc['p' + i][0] = Math.min(Math.max(arc.minX + 1, event.x), arc.maxX);
|
|
47
|
+
arc['p' + i][1] = Math.min(Math.max(arc.minY, event.y), arc.maxY);
|
|
48
|
+
|
|
49
|
+
const angle =
|
|
50
|
+
i === 3 ? state.arcs.startAngle(id) : state.arcs.endAngle(id);
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
(arc.startAngle < Math.PI &&
|
|
54
|
+
arc.endAngle < Math.PI &&
|
|
55
|
+
angle < Math.PI) ||
|
|
56
|
+
(arc.startAngle >= Math.PI &&
|
|
57
|
+
arc.endAngle >= Math.PI &&
|
|
58
|
+
angle >= Math.PI)
|
|
59
|
+
) {
|
|
60
|
+
if (i === 2) {
|
|
61
|
+
arc.endAngle = angle;
|
|
62
|
+
arc.arc.endAngle(angle);
|
|
63
|
+
} else if (i === 3) {
|
|
64
|
+
arc.startAngle = angle;
|
|
65
|
+
arc.arc.startAngle(angle);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
drawStrum(brushGroup, state, config, pc, events, arc, i - 2);
|
|
70
|
+
})
|
|
71
|
+
.on('end', onDragEnd(brushGroup, state, config, pc, events));
|
|
72
|
+
|
|
73
|
+
circles
|
|
74
|
+
.enter()
|
|
75
|
+
.append('circle')
|
|
76
|
+
.attr('id', 'arc-' + id)
|
|
77
|
+
.attr('class', 'arc');
|
|
78
|
+
|
|
79
|
+
circles
|
|
80
|
+
.attr('cx', d => d[0])
|
|
81
|
+
.attr('cy', d => d[1])
|
|
82
|
+
.attr('r', 5)
|
|
83
|
+
.style(
|
|
84
|
+
'opacity',
|
|
85
|
+
(d, i) => (activePoint !== undefined && i === activePoint ? 0.8 : 0)
|
|
86
|
+
)
|
|
87
|
+
.on('mouseover', function() {
|
|
88
|
+
select(this).style('opacity', 0.8);
|
|
89
|
+
})
|
|
90
|
+
.on('mouseout', function() {
|
|
91
|
+
select(this).style('opacity', 0);
|
|
92
|
+
})
|
|
93
|
+
.call(_drag);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onDrag = (brushGroup, state, config, pc, events) => (event) => {
|
|
97
|
+
const arc = state.arcs[state.arcs.active];
|
|
98
|
+
|
|
99
|
+
// Make sure that the point is within the bounds
|
|
100
|
+
arc.p2[0] = Math.min(
|
|
101
|
+
Math.max(arc.minX + 1, event.x - config.margin.left),
|
|
102
|
+
arc.maxX
|
|
103
|
+
);
|
|
104
|
+
arc.p2[1] = Math.min(Math.max(arc.minY, event.y - config.margin.top), arc.maxY);
|
|
105
|
+
arc.p3 = arc.p2.slice();
|
|
106
|
+
drawStrum(brushGroup, state, config, pc, events, arc, 1);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default onDrag;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import selected from './selected';
|
|
2
|
+
import removeStrum from './removeStrum';
|
|
3
|
+
|
|
4
|
+
const onDragEnd = (brushGroup, state, config, pc, events) => () => {
|
|
5
|
+
const arc = state.arcs[state.arcs.active];
|
|
6
|
+
|
|
7
|
+
// Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
|
|
8
|
+
// considered a drag without move. So we have to deal with that case
|
|
9
|
+
if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
|
|
10
|
+
removeStrum(state, pc);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (arc) {
|
|
14
|
+
const angle = state.arcs.startAngle(state.arcs.active);
|
|
15
|
+
|
|
16
|
+
arc.startAngle = angle;
|
|
17
|
+
arc.endAngle = angle;
|
|
18
|
+
arc.arc
|
|
19
|
+
.outerRadius(state.arcs.length(state.arcs.active))
|
|
20
|
+
.startAngle(angle)
|
|
21
|
+
.endAngle(angle);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
state.arcs.active = undefined;
|
|
25
|
+
config.brushed = selected(brushGroup, state, config);
|
|
26
|
+
pc.renderBrushed();
|
|
27
|
+
events.call('brushend', pc, config.brushed);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default onDragEnd;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { pointer } from 'd3-selection';
|
|
2
|
+
import { arc as d3Arc } from 'd3-shape';
|
|
3
|
+
import dimensionsForPoint from '../dimensionsForPoint';
|
|
4
|
+
import h from '../../util/height';
|
|
5
|
+
|
|
6
|
+
// First we need to determine between which two axes the arc was started.
|
|
7
|
+
// This will determine the freedom of movement, because a arc can
|
|
8
|
+
// logically only happen between two axes, so no movement outside these axes
|
|
9
|
+
// should be allowed.
|
|
10
|
+
const onDragStart = (state, config, pc, xscale) => (event) => {
|
|
11
|
+
const p = pointer(event, state.strumRect.node());
|
|
12
|
+
|
|
13
|
+
p[0] = p[0] - config.margin.left;
|
|
14
|
+
p[1] = p[1] - config.margin.top;
|
|
15
|
+
|
|
16
|
+
const dims = dimensionsForPoint(config, pc, xscale, p);
|
|
17
|
+
const arc = {
|
|
18
|
+
p1: p,
|
|
19
|
+
dims: dims,
|
|
20
|
+
minX: xscale(dims.left),
|
|
21
|
+
maxX: xscale(dims.right),
|
|
22
|
+
minY: 0,
|
|
23
|
+
maxY: h(config),
|
|
24
|
+
startAngle: undefined,
|
|
25
|
+
endAngle: undefined,
|
|
26
|
+
arc: d3Arc().innerRadius(0),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Make sure that the point is within the bounds
|
|
30
|
+
arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
|
|
31
|
+
arc.p2 = arc.p1.slice();
|
|
32
|
+
arc.p3 = arc.p1.slice();
|
|
33
|
+
|
|
34
|
+
state.arcs[dims.i] = arc;
|
|
35
|
+
state.arcs.active = dims.i;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default onDragStart;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const removeStrum = (state, pc) => {
|
|
2
|
+
const arc = state.arcs[state.arcs.active],
|
|
3
|
+
svg = pc.selection.select('svg').select('g#arcs');
|
|
4
|
+
|
|
5
|
+
delete state.arcs[state.arcs.active];
|
|
6
|
+
state.arcs.active = undefined;
|
|
7
|
+
svg.selectAll('line#arc-' + arc.dims.i).remove();
|
|
8
|
+
svg.selectAll('circle#arc-' + arc.dims.i).remove();
|
|
9
|
+
svg.selectAll('path#arc-' + arc.dims.i).remove();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default removeStrum;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import hypothenuse from './util/hypothenuse';
|
|
2
|
+
|
|
3
|
+
// [0, 2*PI] -> [-PI/2, PI/2]
|
|
4
|
+
const signedAngle = angle =>
|
|
5
|
+
angle > Math.PI ? 1.5 * Math.PI - angle : 0.5 * Math.PI - angle;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
|
|
9
|
+
* However, one can only select lines from 0 to PI, so we compute the
|
|
10
|
+
* 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
|
|
11
|
+
* are 12 and 6 o'clock respectively.
|
|
12
|
+
*/
|
|
13
|
+
const containmentTest = arc => a => {
|
|
14
|
+
let startAngle = signedAngle(arc.startAngle);
|
|
15
|
+
let endAngle = signedAngle(arc.endAngle);
|
|
16
|
+
|
|
17
|
+
if (startAngle > endAngle) {
|
|
18
|
+
const tmp = startAngle;
|
|
19
|
+
startAngle = endAngle;
|
|
20
|
+
endAngle = tmp;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// test if segment angle is contained in angle interval
|
|
24
|
+
return a >= startAngle && a <= endAngle;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const crossesStrum = (state, config) => (d, id) => {
|
|
28
|
+
const arc = state.arcs[id],
|
|
29
|
+
test = containmentTest(arc),
|
|
30
|
+
d1 = arc.dims.left,
|
|
31
|
+
d2 = arc.dims.right,
|
|
32
|
+
y1 = config.dimensions[d1].yscale,
|
|
33
|
+
y2 = config.dimensions[d2].yscale,
|
|
34
|
+
a = state.arcs.width(id),
|
|
35
|
+
b = y1(d[d1]) - y2(d[d2]),
|
|
36
|
+
c = hypothenuse(a, b),
|
|
37
|
+
angle = Math.asin(b / c); // rad in [-PI/2, PI/2]
|
|
38
|
+
return test(angle);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const selected = (brushGroup, state, config) => {
|
|
42
|
+
const ids = Object.getOwnPropertyNames(state.arcs).filter(d => !isNaN(d));
|
|
43
|
+
const brushed = config.data;
|
|
44
|
+
|
|
45
|
+
if (ids.length === 0) {
|
|
46
|
+
return brushed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const crossTest = crossesStrum(state, config);
|
|
50
|
+
|
|
51
|
+
return brushed.filter(d => {
|
|
52
|
+
switch (brushGroup.predicate) {
|
|
53
|
+
case 'AND':
|
|
54
|
+
return ids.every(id => crossTest(d, id));
|
|
55
|
+
case 'OR':
|
|
56
|
+
return ids.some(id => crossTest(d, id));
|
|
57
|
+
default:
|
|
58
|
+
throw new Error('Unknown brush predicate ' + config.brushPredicate);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default selected;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const uninstall = (state, pc) => () => {
|
|
2
|
+
pc.selection
|
|
3
|
+
.select('svg')
|
|
4
|
+
.select('g#arcs')
|
|
5
|
+
.remove();
|
|
6
|
+
pc.selection
|
|
7
|
+
.select('svg')
|
|
8
|
+
.select('rect#arc-events')
|
|
9
|
+
.remove();
|
|
10
|
+
pc.on('axesreorder.arcs', undefined);
|
|
11
|
+
|
|
12
|
+
delete pc.brushReset;
|
|
13
|
+
|
|
14
|
+
state.strumRect = undefined;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default uninstall;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Checks if the first dimension is directly left of the second dimension.
|
|
2
|
+
const consecutive = dimensions => (first, second) => {
|
|
3
|
+
const keys = Object.keys(dimensions);
|
|
4
|
+
|
|
5
|
+
return keys.some(
|
|
6
|
+
(d, i) =>
|
|
7
|
+
d === first ? i + i < keys.length && dimensions[i + 1] === second : false
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default consecutive;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const dimensionsForPoint = (config, pc, xscale, p) => {
|
|
2
|
+
const dims = { i: -1, left: undefined, right: undefined };
|
|
3
|
+
Object.keys(config.dimensions).some((dim, i) => {
|
|
4
|
+
if (xscale(dim) < p[0]) {
|
|
5
|
+
dims.i = i;
|
|
6
|
+
dims.left = dim;
|
|
7
|
+
dims.right = Object.keys(config.dimensions)[
|
|
8
|
+
pc.getOrderedDimensionKeys().indexOf(dim) + 1
|
|
9
|
+
];
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (dims.left === undefined) {
|
|
16
|
+
// Event on the left side of the first axis.
|
|
17
|
+
dims.i = 0;
|
|
18
|
+
dims.left = pc.getOrderedDimensionKeys()[0];
|
|
19
|
+
dims.right = pc.getOrderedDimensionKeys()[1];
|
|
20
|
+
} else if (dims.right === undefined) {
|
|
21
|
+
// Event on the right side of the last axis
|
|
22
|
+
dims.i = Object.keys(config.dimensions).length - 1;
|
|
23
|
+
dims.right = dims.left;
|
|
24
|
+
dims.left = pc.getOrderedDimensionKeys()[
|
|
25
|
+
Object.keys(config.dimensions).length - 2
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return dims;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default dimensionsForPoint;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const invertCategorical = (selection, scale) => {
|
|
2
|
+
if (selection.length === 0) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const domain = scale.domain();
|
|
6
|
+
const range = scale.range();
|
|
7
|
+
const found = [];
|
|
8
|
+
range.forEach((d, i) => {
|
|
9
|
+
if (d >= selection[0] && d <= selection[1]) {
|
|
10
|
+
found.push(domain[i]);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return found;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const invertByScale = (selection, scale) => {
|
|
17
|
+
if (scale === null) return [];
|
|
18
|
+
return typeof scale.invert === 'undefined'
|
|
19
|
+
? invertCategorical(selection, scale)
|
|
20
|
+
: selection.map(d => scale.invert(d));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default invertByScale;
|
|
24
|
+
export { invertByScale };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import onDragEnd from './onDragEnd';
|
|
2
|
+
import removeStrum from './removeStrum';
|
|
3
|
+
|
|
4
|
+
const brushReset = (brushGroup, state, config, pc, events) => () => {
|
|
5
|
+
const ids = Object.getOwnPropertyNames(state.strums).filter(d => !isNaN(d));
|
|
6
|
+
|
|
7
|
+
ids.forEach(d => {
|
|
8
|
+
state.strums.active = d;
|
|
9
|
+
removeStrum(state, pc);
|
|
10
|
+
});
|
|
11
|
+
onDragEnd(brushGroup, state, config, pc, events)();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default brushReset;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import uninstall from './uninstall';
|
|
2
|
+
import install from './install';
|
|
3
|
+
import selected from './selected';
|
|
4
|
+
|
|
5
|
+
const install2DStrums = (brushGroup, config, pc, events, xscale) => {
|
|
6
|
+
const state = {
|
|
7
|
+
strums: {},
|
|
8
|
+
strumRect: {},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
brushGroup.modes['2D-strums'] = {
|
|
12
|
+
install: install(brushGroup, state, config, pc, events, xscale),
|
|
13
|
+
uninstall: uninstall(state, pc),
|
|
14
|
+
selected: selected(brushGroup, state, config),
|
|
15
|
+
brushState: () => state.strums,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default install2DStrums;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { drag } from 'd3-drag';
|
|
2
|
+
import onDragEnd from './onDragEnd';
|
|
3
|
+
import onDrag from './onDrag';
|
|
4
|
+
import onDragStart from './onDragStart';
|
|
5
|
+
import removeStrum from './removeStrum';
|
|
6
|
+
import brushReset from './brushReset';
|
|
7
|
+
import w from '../../util/width';
|
|
8
|
+
import h from '../../util/height';
|
|
9
|
+
import consecutive from '../consecutive';
|
|
10
|
+
|
|
11
|
+
const install = (brushGroup, state, config, pc, events, xscale) => () => {
|
|
12
|
+
if (pc.g() === undefined || pc.g() === null) {
|
|
13
|
+
pc.createAxes();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const _drag = drag();
|
|
17
|
+
|
|
18
|
+
// Map of current strums. Strums are stored per segment of the PC. A segment,
|
|
19
|
+
// being the area between two axes. The left most area is indexed at 0.
|
|
20
|
+
state.strums.active = undefined;
|
|
21
|
+
// Returns the width of the PC segment where currently a strum is being
|
|
22
|
+
// placed. NOTE: even though they are evenly spaced in our current
|
|
23
|
+
// implementation, we keep for when non-even spaced segments are supported as
|
|
24
|
+
// well.
|
|
25
|
+
state.strums.width = id =>
|
|
26
|
+
state.strums[id] === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: state.strums[id].maxX - state.strums[id].minX;
|
|
29
|
+
|
|
30
|
+
pc.on('axesreorder.strums', () => {
|
|
31
|
+
const ids = Object.getOwnPropertyNames(state.strums).filter(d => !isNaN(d));
|
|
32
|
+
|
|
33
|
+
if (ids.length > 0) {
|
|
34
|
+
// We have some strums, which might need to be removed.
|
|
35
|
+
ids.forEach(d => {
|
|
36
|
+
const dims = state.strums[d].dims;
|
|
37
|
+
state.strums.active = d;
|
|
38
|
+
// If the two dimensions of the current strum are not next to each other
|
|
39
|
+
// any more, than we'll need to remove the strum. Otherwise we keep it.
|
|
40
|
+
if (!consecutive(config.dimensions)(dims.left, dims.right)) {
|
|
41
|
+
removeStrum(state, pc);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
onDragEnd(brushGroup, state, config, pc, events)();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add a new svg group in which we draw the strums.
|
|
49
|
+
pc.selection
|
|
50
|
+
.select('svg')
|
|
51
|
+
.append('g')
|
|
52
|
+
.attr('id', 'strums')
|
|
53
|
+
.attr(
|
|
54
|
+
'transform',
|
|
55
|
+
'translate(' + config.margin.left + ',' + config.margin.top + ')'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Install the required brushReset function
|
|
59
|
+
pc.brushReset = brushReset(brushGroup, state, config, pc, events);
|
|
60
|
+
|
|
61
|
+
_drag
|
|
62
|
+
.on('start', onDragStart(state, config, pc, xscale))
|
|
63
|
+
.on('drag', onDrag(brushGroup, state, config, pc, events))
|
|
64
|
+
.on('end', onDragEnd(brushGroup, state, config, pc, events));
|
|
65
|
+
|
|
66
|
+
// NOTE: The styling needs to be done here and not in the css. This is because
|
|
67
|
+
// for 1D brushing, the canvas layers should not listen to
|
|
68
|
+
// pointer-events._.
|
|
69
|
+
state.strumRect = pc.selection
|
|
70
|
+
.select('svg')
|
|
71
|
+
.insert('rect', 'g#strums')
|
|
72
|
+
.attr('id', 'strum-events')
|
|
73
|
+
.attr('x', config.margin.left)
|
|
74
|
+
.attr('y', config.margin.top)
|
|
75
|
+
.attr('width', w(config))
|
|
76
|
+
.attr('height', h(config) + 2)
|
|
77
|
+
.style('opacity', 0)
|
|
78
|
+
.call(_drag);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default install;
|