@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
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jrkasprzyk/parcoord-es",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "ES6 module of Parallel Coordinates, based on d3 v7 modules",
|
|
5
|
+
"main": "dist/parcoords.js",
|
|
6
|
+
"module": "dist/parcoords.esm.js",
|
|
7
|
+
"jsnext:main": "dist/parcoords.esm.js",
|
|
8
|
+
"style": "dist/parcoords.css",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"_prebuild": "eslint src test",
|
|
11
|
+
"build": "rollup -c",
|
|
12
|
+
"dev": "rollup -c rollup.config.dev.js -w",
|
|
13
|
+
"watch": "rollup -c -w",
|
|
14
|
+
"pretest": "npm run build",
|
|
15
|
+
"test:watch": "mocha --reporter min --watch",
|
|
16
|
+
"test": "mocha",
|
|
17
|
+
"test:cover": "babel-node ./node_modules/mocha/bin/_mocha",
|
|
18
|
+
"prepare": "npm test",
|
|
19
|
+
"format": "prettier --single-quote --trailing-comma es5 --write \"{src,__{tests,demo,dist}__}/**/*.{js,html}\""
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"d3-array": "^3.0.0",
|
|
27
|
+
"d3-axis": "^3.0.0",
|
|
28
|
+
"d3-brush": "^3.0.0",
|
|
29
|
+
"d3-dispatch": "^3.0.0",
|
|
30
|
+
"d3-drag": "^3.0.0",
|
|
31
|
+
"d3-interpolate": "^3.0.0",
|
|
32
|
+
"d3-scale": "^4.0.0",
|
|
33
|
+
"d3-selection": "^3.0.0",
|
|
34
|
+
"d3-shape": "^3.0.0",
|
|
35
|
+
"d3-transition": "^3.0.0",
|
|
36
|
+
"requestanimationframe": "0.0.23",
|
|
37
|
+
"sylvester-es6": "0.0.2"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@babel/cli": "^7.25.0",
|
|
41
|
+
"@babel/core": "^7.25.0",
|
|
42
|
+
"@babel/eslint-parser": "^7.25.0",
|
|
43
|
+
"@babel/plugin-proposal-object-rest-spread": "^7.20.0",
|
|
44
|
+
"@babel/preset-env": "^7.25.0",
|
|
45
|
+
"@babel/register": "^7.25.0",
|
|
46
|
+
"@rollup/plugin-babel": "^6.0.0",
|
|
47
|
+
"@rollup/plugin-commonjs": "^25.0.0",
|
|
48
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
49
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
50
|
+
"eslint": "^8.57.0",
|
|
51
|
+
"mocha": "^11.0.0",
|
|
52
|
+
"prettier": "1.13.5",
|
|
53
|
+
"rollup": "^4.20.0",
|
|
54
|
+
"rollup-plugin-livereload": "^2.0.5",
|
|
55
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
56
|
+
"rollup-plugin-serve": "^3.0.0"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/jrkasprzyk/parcoords-es.git"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"es6",
|
|
64
|
+
"d3",
|
|
65
|
+
"v4",
|
|
66
|
+
"ParCoords",
|
|
67
|
+
"visualization",
|
|
68
|
+
"parallel coordinates"
|
|
69
|
+
],
|
|
70
|
+
"author": {
|
|
71
|
+
"name": "Xing Yun"
|
|
72
|
+
},
|
|
73
|
+
"contributors": [
|
|
74
|
+
{
|
|
75
|
+
"name": "Joseph Kasprzyk"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"license": "MIT",
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public"
|
|
81
|
+
},
|
|
82
|
+
"overrides": {
|
|
83
|
+
"diff": "^8.0.3",
|
|
84
|
+
"serialize-javascript": "^7.0.5"
|
|
85
|
+
},
|
|
86
|
+
"bugs": {
|
|
87
|
+
"url": "https://github.com/jrkasprzyk/parcoords-es/issues"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { axisBottom, axisLeft, axisRight, axisTop } from 'd3-axis';
|
|
2
|
+
|
|
3
|
+
const applyAxisConfig = (axis, dimension) => {
|
|
4
|
+
let axisCfg;
|
|
5
|
+
|
|
6
|
+
switch (dimension.orient) {
|
|
7
|
+
case 'left':
|
|
8
|
+
axisCfg = axisLeft(dimension.yscale);
|
|
9
|
+
break;
|
|
10
|
+
case 'right':
|
|
11
|
+
axisCfg = axisRight(dimension.yscale);
|
|
12
|
+
break;
|
|
13
|
+
case 'top':
|
|
14
|
+
axisCfg = axisTop(dimension.yscale);
|
|
15
|
+
break;
|
|
16
|
+
case 'bottom':
|
|
17
|
+
axisCfg = axisBottom(dimension.yscale);
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
axisCfg = axisLeft(dimension.yscale);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
axisCfg
|
|
25
|
+
.ticks(dimension.ticks)
|
|
26
|
+
.tickValues(dimension.tickValues)
|
|
27
|
+
.tickSizeInner(dimension.innerTickSize)
|
|
28
|
+
.tickSizeOuter(dimension.outerTickSize)
|
|
29
|
+
.tickPadding(dimension.tickPadding)
|
|
30
|
+
.tickFormat(dimension.tickFormat);
|
|
31
|
+
|
|
32
|
+
return axisCfg;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default applyAxisConfig;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const isValid = d => d !== null && d !== undefined;
|
|
2
|
+
|
|
3
|
+
const applyDimensionDefaults = (config, pc) =>
|
|
4
|
+
function(dims) {
|
|
5
|
+
const types = pc.detectDimensionTypes(config.data);
|
|
6
|
+
dims = dims ? dims : Object.keys(types);
|
|
7
|
+
|
|
8
|
+
return dims.reduce((acc, cur, i) => {
|
|
9
|
+
const k = config.dimensions[cur] ? config.dimensions[cur] : {};
|
|
10
|
+
acc[cur] = {
|
|
11
|
+
...k,
|
|
12
|
+
orient: isValid(k.orient) ? k.orient : 'left',
|
|
13
|
+
ticks: isValid(k.ticks) ? k.ticks : 5,
|
|
14
|
+
innerTickSize: isValid(k.innerTickSize) ? k.innerTickSize : 6,
|
|
15
|
+
outerTickSize: isValid(k.outerTickSize) ? k.outerTickSize : 0,
|
|
16
|
+
tickPadding: isValid(k.tickPadding) ? k.tickPadding : 3,
|
|
17
|
+
type: isValid(k.type) ? k.type : types[cur],
|
|
18
|
+
index: isValid(k.index) ? k.index : i,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return acc;
|
|
22
|
+
}, {});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default applyDimensionDefaults;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { scaleLinear, scaleOrdinal, scalePoint, scaleTime } from 'd3-scale';
|
|
2
|
+
import { extent } from 'd3-array';
|
|
3
|
+
|
|
4
|
+
import getRange from '../util/getRange';
|
|
5
|
+
import w from '../util/width';
|
|
6
|
+
import h from '../util/height';
|
|
7
|
+
|
|
8
|
+
const autoscale = (config, pc, xscale, ctx) =>
|
|
9
|
+
function() {
|
|
10
|
+
// yscale
|
|
11
|
+
const defaultScales = {
|
|
12
|
+
date: function(k) {
|
|
13
|
+
let _extent = extent(config.data, d => (d[k] ? d[k].getTime() : null));
|
|
14
|
+
// special case if single value
|
|
15
|
+
if (_extent[0] === _extent[1]) {
|
|
16
|
+
return scalePoint()
|
|
17
|
+
.domain(_extent)
|
|
18
|
+
.range(getRange(config));
|
|
19
|
+
}
|
|
20
|
+
if (config.flipAxes.includes(k)) {
|
|
21
|
+
_extent = _extent.map(val => tempDate.unshift(val));
|
|
22
|
+
}
|
|
23
|
+
return scaleTime()
|
|
24
|
+
.domain(_extent)
|
|
25
|
+
.range(getRange(config));
|
|
26
|
+
},
|
|
27
|
+
number: function(k) {
|
|
28
|
+
let _extent = extent(config.data, d => +d[k]);
|
|
29
|
+
// special case if single value
|
|
30
|
+
if (_extent[0] === _extent[1]) {
|
|
31
|
+
return scalePoint()
|
|
32
|
+
.domain(_extent)
|
|
33
|
+
.range(getRange(config));
|
|
34
|
+
}
|
|
35
|
+
if (config.flipAxes.includes(k)) {
|
|
36
|
+
_extent = _extent.map(val => tempDate.unshift(val));
|
|
37
|
+
}
|
|
38
|
+
return scaleLinear()
|
|
39
|
+
.domain(_extent)
|
|
40
|
+
.range(getRange(config));
|
|
41
|
+
},
|
|
42
|
+
string: function(k) {
|
|
43
|
+
let counts = {},
|
|
44
|
+
domain = [];
|
|
45
|
+
// Let's get the count for each value so that we can sort the domain based
|
|
46
|
+
// on the number of items for each value.
|
|
47
|
+
config.data.map(p => {
|
|
48
|
+
if (p[k] === undefined && config.nullValueSeparator !== 'undefined') {
|
|
49
|
+
return null; // null values will be drawn beyond the horizontal null value separator!
|
|
50
|
+
}
|
|
51
|
+
if (counts[p[k]] === undefined) {
|
|
52
|
+
counts[p[k]] = 1;
|
|
53
|
+
} else {
|
|
54
|
+
counts[p[k]] = counts[p[k]] + 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (config.flipAxes.includes(k)) {
|
|
58
|
+
domain = Object.getOwnPropertyNames(counts).sort();
|
|
59
|
+
} else {
|
|
60
|
+
let tempArr = Object.getOwnPropertyNames(counts).sort();
|
|
61
|
+
for (let i = 0; i < Object.getOwnPropertyNames(counts).length; i++) {
|
|
62
|
+
domain.push(tempArr.pop());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//need to create an ordinal scale for categorical data
|
|
67
|
+
let categoricalRange = [];
|
|
68
|
+
if (domain.length === 1) {
|
|
69
|
+
//edge case
|
|
70
|
+
domain = [' ', domain[0], ' '];
|
|
71
|
+
}
|
|
72
|
+
let addBy = getRange(config)[0] / (domain.length - 1);
|
|
73
|
+
for (let j = 0; j < domain.length; j++) {
|
|
74
|
+
if (categoricalRange.length === 0) {
|
|
75
|
+
categoricalRange.push(0);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
categoricalRange.push(categoricalRange[j - 1] + addBy);
|
|
79
|
+
}
|
|
80
|
+
return scaleOrdinal()
|
|
81
|
+
.domain(domain)
|
|
82
|
+
.range(categoricalRange);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
Object.keys(config.dimensions).forEach(function(k) {
|
|
86
|
+
if (
|
|
87
|
+
config.dimensions[k].yscale === undefined ||
|
|
88
|
+
config.dimensions[k].yscale === null
|
|
89
|
+
) {
|
|
90
|
+
config.dimensions[k].yscale = defaultScales[config.dimensions[k].type](
|
|
91
|
+
k
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// xscale
|
|
97
|
+
// add padding for d3 >= v4 default 0.2
|
|
98
|
+
xscale.range([0, w(config)]).padding(0.2);
|
|
99
|
+
|
|
100
|
+
// Retina display, etc.
|
|
101
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
102
|
+
|
|
103
|
+
// canvas sizes
|
|
104
|
+
pc.selection
|
|
105
|
+
.selectAll('canvas')
|
|
106
|
+
.style('margin-top', config.margin.top + 'px')
|
|
107
|
+
.style('margin-left', config.margin.left + 'px')
|
|
108
|
+
.style('width', w(config) + 2 + 'px')
|
|
109
|
+
.style('height', h(config) + 2 + 'px')
|
|
110
|
+
.attr('width', (w(config) + 2) * devicePixelRatio)
|
|
111
|
+
.attr('height', (h(config) + 2) * devicePixelRatio);
|
|
112
|
+
// default styles, needs to be set when canvas width changes
|
|
113
|
+
ctx.foreground.strokeStyle = config.color;
|
|
114
|
+
ctx.foreground.lineWidth = config.lineWidth;
|
|
115
|
+
ctx.foreground.globalCompositeOperation = config.composite;
|
|
116
|
+
ctx.foreground.globalAlpha = config.alpha;
|
|
117
|
+
ctx.foreground.scale(devicePixelRatio, devicePixelRatio);
|
|
118
|
+
ctx.brushed.strokeStyle = config.brushedColor;
|
|
119
|
+
ctx.brushed.lineWidth = config.lineWidth;
|
|
120
|
+
ctx.brushed.globalCompositeOperation = config.composite;
|
|
121
|
+
ctx.brushed.globalAlpha = config.alpha;
|
|
122
|
+
ctx.brushed.scale(devicePixelRatio, devicePixelRatio);
|
|
123
|
+
ctx.highlight.lineWidth = config.highlightedLineWidth;
|
|
124
|
+
ctx.highlight.scale(devicePixelRatio, devicePixelRatio);
|
|
125
|
+
ctx.marked.lineWidth = config.markedLineWidth;
|
|
126
|
+
ctx.marked.shadowColor = config.markedShadowColor;
|
|
127
|
+
ctx.marked.shadowBlur = config.markedShadowBlur;
|
|
128
|
+
ctx.marked.scale(devicePixelRatio, devicePixelRatio);
|
|
129
|
+
|
|
130
|
+
return this;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default autoscale;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { min } from 'd3-array';
|
|
2
|
+
|
|
3
|
+
//draw dots with radius r on the axis line where data intersects
|
|
4
|
+
const axisDots = (config, pc, position) => _r => {
|
|
5
|
+
const r = _r || 0.1;
|
|
6
|
+
const ctx = pc.ctx.dots;
|
|
7
|
+
const startAngle = 0;
|
|
8
|
+
const endAngle = 2 * Math.PI;
|
|
9
|
+
ctx.globalAlpha = min([1 / Math.pow(config.data.length, 1 / 2), 1]);
|
|
10
|
+
config.data.forEach(d => {
|
|
11
|
+
Object.entries(config.dimensions).forEach(([key], i) => {
|
|
12
|
+
ctx.beginPath();
|
|
13
|
+
ctx.arc(
|
|
14
|
+
position(key),
|
|
15
|
+
config.dimensions[key].yscale(d[key]),
|
|
16
|
+
r,
|
|
17
|
+
startAngle,
|
|
18
|
+
endAngle
|
|
19
|
+
);
|
|
20
|
+
ctx.stroke();
|
|
21
|
+
ctx.fill();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
return this;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default axisDots;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const brushPredicate = (brushGroup, config, pc) => (predicate = null) => {
|
|
2
|
+
if (predicate === null) {
|
|
3
|
+
return brushGroup.predicate;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
predicate = String(predicate).toUpperCase();
|
|
7
|
+
if (predicate !== 'AND' && predicate !== 'OR') {
|
|
8
|
+
throw new Error('Invalid predicate ' + predicate);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
brushGroup.predicate = predicate;
|
|
12
|
+
config.brushed = brushGroup.currentMode().selected();
|
|
13
|
+
pc.renderBrushed();
|
|
14
|
+
return pc;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const brushMode = (brushGroup, config, pc) => (mode = null) => {
|
|
18
|
+
if (mode === null) {
|
|
19
|
+
return brushGroup.mode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (pc.brushModes().indexOf(mode) === -1) {
|
|
23
|
+
throw new Error('pc.brushmode: Unsupported brush mode: ' + mode);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Make sure that we don't trigger unnecessary events by checking if the mode
|
|
27
|
+
// actually changes.
|
|
28
|
+
if (mode !== brushGroup.mode) {
|
|
29
|
+
// When changing brush modes, the first thing we need to do is clearing any
|
|
30
|
+
// brushes from the current mode, if any.
|
|
31
|
+
if (brushGroup.mode !== 'None') {
|
|
32
|
+
pc.brushReset();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Next, we need to 'uninstall' the current brushMode.
|
|
36
|
+
brushGroup.modes[brushGroup.mode].uninstall(pc);
|
|
37
|
+
// Finally, we can install the requested one.
|
|
38
|
+
brushGroup.mode = mode;
|
|
39
|
+
brushGroup.modes[brushGroup.mode].install();
|
|
40
|
+
if (mode === 'None') {
|
|
41
|
+
delete pc.brushPredicate;
|
|
42
|
+
} else {
|
|
43
|
+
pc.brushPredicate = brushPredicate(brushGroup, config, pc);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return pc;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default brushMode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { select } from 'd3-selection';
|
|
2
|
+
|
|
3
|
+
const brushReset = (config, pc) =>
|
|
4
|
+
function(dimension) {
|
|
5
|
+
const brushesToKeep = [];
|
|
6
|
+
for (let j = 0; j < config.brushes.length; j++) {
|
|
7
|
+
if (config.brushes[j].data !== dimension) {
|
|
8
|
+
brushesToKeep.push(config.brushes[j]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
config.brushes = brushesToKeep;
|
|
13
|
+
config.brushed = false;
|
|
14
|
+
|
|
15
|
+
if (pc.g() !== undefined) {
|
|
16
|
+
const nodes = pc
|
|
17
|
+
.g()
|
|
18
|
+
.selectAll('.brush')
|
|
19
|
+
.nodes();
|
|
20
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
21
|
+
if (nodes[i].__data__ === dimension) {
|
|
22
|
+
// remove all dummy brushes for this axis or the real brush
|
|
23
|
+
select(select(nodes[i]).nodes()[0].parentNode)
|
|
24
|
+
.selectAll('.dummy')
|
|
25
|
+
.remove();
|
|
26
|
+
config.dimensions[dimension].brush.move(select(nodes[i], null));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return this;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default brushReset;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { brushSelection, brushY } from 'd3-brush';
|
|
2
|
+
import { select } from 'd3-selection';
|
|
3
|
+
|
|
4
|
+
const brushable = (config, pc, flags) =>
|
|
5
|
+
function() {
|
|
6
|
+
if (!pc.g()) {
|
|
7
|
+
pc.createAxes();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const g = pc.g();
|
|
11
|
+
|
|
12
|
+
// Add and store a brush for each axis.
|
|
13
|
+
g.append('svg:g')
|
|
14
|
+
.attr('class', 'brush')
|
|
15
|
+
.each(function(d) {
|
|
16
|
+
if (config.dimensions[d] !== undefined) {
|
|
17
|
+
config.dimensions[d]['brush'] = brushY(select(this)).extent([
|
|
18
|
+
[-15, 0],
|
|
19
|
+
[15, config.dimensions[d].yscale.range()[0]],
|
|
20
|
+
]);
|
|
21
|
+
select(this).call(
|
|
22
|
+
config.dimensions[d]['brush']
|
|
23
|
+
.on('start', function(event) {
|
|
24
|
+
if (event.sourceEvent !== null && !event.sourceEvent.ctrlKey) {
|
|
25
|
+
pc.brushReset();
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.on('brush', function(event) {
|
|
29
|
+
if (!event.sourceEvent.ctrlKey) {
|
|
30
|
+
pc.brush();
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.on('end', function(event) {
|
|
34
|
+
// save brush selection is ctrl key is held
|
|
35
|
+
// store important brush information and
|
|
36
|
+
// the html element of the selection,
|
|
37
|
+
// to make a dummy selection element
|
|
38
|
+
if (event.sourceEvent.ctrlKey) {
|
|
39
|
+
let html = select(this)
|
|
40
|
+
.select('.selection')
|
|
41
|
+
.nodes()[0].outerHTML;
|
|
42
|
+
html = html.replace(
|
|
43
|
+
'class="selection"',
|
|
44
|
+
'class="selection dummy' +
|
|
45
|
+
' selection-' +
|
|
46
|
+
config.brushes.length +
|
|
47
|
+
'"'
|
|
48
|
+
);
|
|
49
|
+
let dat = select(this).nodes()[0].__data__;
|
|
50
|
+
let brush = {
|
|
51
|
+
id: config.brushes.length,
|
|
52
|
+
extent: brushSelection(this),
|
|
53
|
+
html: html,
|
|
54
|
+
data: dat,
|
|
55
|
+
};
|
|
56
|
+
config.brushes.push(brush);
|
|
57
|
+
select(select(this).nodes()[0].parentNode)
|
|
58
|
+
.select('.axis')
|
|
59
|
+
.nodes()[0].outerHTML += html;
|
|
60
|
+
pc.brush();
|
|
61
|
+
config.dimensions[d].brush.move(select(this, null));
|
|
62
|
+
select(this)
|
|
63
|
+
.select('.selection')
|
|
64
|
+
.attr('style', 'display:none');
|
|
65
|
+
pc.brushable();
|
|
66
|
+
} else {
|
|
67
|
+
pc.brush();
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
select(this).on('dblclick', function() {
|
|
72
|
+
pc.brushReset(d);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
flags.brushable = true;
|
|
78
|
+
return this;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default brushable;
|
package/src/api/clear.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import isBrushed from '../util/isBrushed';
|
|
2
|
+
import w from '../util/width';
|
|
3
|
+
import h from '../util/height';
|
|
4
|
+
|
|
5
|
+
const clear = (config, pc, ctx, brushGroup) =>
|
|
6
|
+
function(layer) {
|
|
7
|
+
ctx[layer].clearRect(0, 0, w(config) + 2, h(config) + 2);
|
|
8
|
+
|
|
9
|
+
// This will make sure that the foreground items are transparent
|
|
10
|
+
// without the need for changing the opacity style of the foreground canvas
|
|
11
|
+
// as this would stop the css styling from working
|
|
12
|
+
if (layer === 'brushed' && isBrushed(config, brushGroup)) {
|
|
13
|
+
ctx.brushed.fillStyle = pc.selection.style('background-color');
|
|
14
|
+
ctx.brushed.globalAlpha = 1 - config.alphaOnBrushed;
|
|
15
|
+
ctx.brushed.fillRect(0, 0, w(config) + 2, h(config) + 2);
|
|
16
|
+
ctx.brushed.globalAlpha = config.alpha;
|
|
17
|
+
}
|
|
18
|
+
return this;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default clear;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { extent } from 'd3-array';
|
|
2
|
+
|
|
3
|
+
const commonScale = (config, pc) =>
|
|
4
|
+
function(global, type) {
|
|
5
|
+
const t = type || 'number';
|
|
6
|
+
if (typeof global === 'undefined') {
|
|
7
|
+
global = true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// try to autodetect dimensions and create scales
|
|
11
|
+
if (!Object.keys(config.dimensions).length) {
|
|
12
|
+
pc.detectDimensions();
|
|
13
|
+
}
|
|
14
|
+
pc.autoscale();
|
|
15
|
+
|
|
16
|
+
// scales of the same type
|
|
17
|
+
const scales = Object.keys(config.dimensions).filter(
|
|
18
|
+
p => config.dimensions[p].type == t
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (global) {
|
|
22
|
+
let _extent = extent(
|
|
23
|
+
scales
|
|
24
|
+
.map(d => config.dimensions[d].yscale.domain())
|
|
25
|
+
.reduce((cur, acc) => cur.concat(acc))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
scales.forEach(d => {
|
|
29
|
+
config.dimensions[d].yscale.domain(_extent);
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
scales.forEach(d => {
|
|
33
|
+
config.dimensions[d].yscale.domain(extent(config.data, d => +d[k]));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// update centroids
|
|
38
|
+
if (config.bundleDimension !== null) {
|
|
39
|
+
pc.bundleDimension(config.bundleDimension);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return this;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default commonScale;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { select } from 'd3-selection';
|
|
2
|
+
|
|
3
|
+
import dimensionLabels from '../util/dimensionLabels';
|
|
4
|
+
import flipAxisAndUpdatePCP from '../util/flipAxisAndUpdatePCP';
|
|
5
|
+
import rotateLabels from '../util/rotateLabels';
|
|
6
|
+
|
|
7
|
+
import w from '../util/width';
|
|
8
|
+
import h from '../util/height';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create static SVG axes with dimension names, ticks, and labels.
|
|
12
|
+
*
|
|
13
|
+
* @param config
|
|
14
|
+
* @param pc
|
|
15
|
+
* @param xscale
|
|
16
|
+
* @param flags
|
|
17
|
+
* @param axis
|
|
18
|
+
* @returns {Function}
|
|
19
|
+
*/
|
|
20
|
+
const createAxes = (config, pc, xscale, flags, axis) =>
|
|
21
|
+
function() {
|
|
22
|
+
if (pc.g() !== undefined) {
|
|
23
|
+
pc.removeAxes();
|
|
24
|
+
}
|
|
25
|
+
// Add a group element for each dimension.
|
|
26
|
+
pc._g = pc.svg
|
|
27
|
+
.selectAll('.dimension')
|
|
28
|
+
.data(pc.getOrderedDimensionKeys(), function(d) {
|
|
29
|
+
return d;
|
|
30
|
+
})
|
|
31
|
+
.enter()
|
|
32
|
+
.append('svg:g')
|
|
33
|
+
.attr('class', 'dimension')
|
|
34
|
+
.attr('transform', function(d) {
|
|
35
|
+
return 'translate(' + xscale(d) + ')';
|
|
36
|
+
});
|
|
37
|
+
// Add an axis and title.
|
|
38
|
+
pc._g
|
|
39
|
+
.append('svg:g')
|
|
40
|
+
.attr('class', 'axis')
|
|
41
|
+
.attr('transform', 'translate(0,0)')
|
|
42
|
+
.each(function(d) {
|
|
43
|
+
let axisElement = select(this).call(
|
|
44
|
+
pc.applyAxisConfig(axis, config.dimensions[d])
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
axisElement
|
|
48
|
+
.selectAll('path')
|
|
49
|
+
.style('fill', 'none')
|
|
50
|
+
.style('stroke', '#222')
|
|
51
|
+
.style('shape-rendering', 'crispEdges');
|
|
52
|
+
|
|
53
|
+
axisElement
|
|
54
|
+
.selectAll('line')
|
|
55
|
+
.style('fill', 'none')
|
|
56
|
+
.style('stroke', '#222')
|
|
57
|
+
.style('shape-rendering', 'crispEdges');
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
.append('svg:text')
|
|
61
|
+
.attr('text-anchor', 'middle')
|
|
62
|
+
.attr('y', 0)
|
|
63
|
+
.attr(
|
|
64
|
+
'transform',
|
|
65
|
+
'translate(0,-5) rotate(' + config.dimensionTitleRotation + ')'
|
|
66
|
+
)
|
|
67
|
+
.attr('x', 0)
|
|
68
|
+
.attr('class', 'label')
|
|
69
|
+
.text(dimensionLabels(config))
|
|
70
|
+
.on('dblclick', flipAxisAndUpdatePCP(config, pc, axis))
|
|
71
|
+
.on('wheel', rotateLabels(config, pc));
|
|
72
|
+
|
|
73
|
+
if (config.nullValueSeparator === 'top') {
|
|
74
|
+
pc.svg
|
|
75
|
+
.append('line')
|
|
76
|
+
.attr('x1', 0)
|
|
77
|
+
.attr('y1', 1 + config.nullValueSeparatorPadding.top)
|
|
78
|
+
.attr('x2', w(config))
|
|
79
|
+
.attr('y2', 1 + config.nullValueSeparatorPadding.top)
|
|
80
|
+
.attr('stroke-width', 1)
|
|
81
|
+
.attr('stroke', '#777')
|
|
82
|
+
.attr('fill', 'none')
|
|
83
|
+
.attr('shape-rendering', 'crispEdges');
|
|
84
|
+
} else if (config.nullValueSeparator === 'bottom') {
|
|
85
|
+
pc.svg
|
|
86
|
+
.append('line')
|
|
87
|
+
.attr('x1', 0)
|
|
88
|
+
.attr('y1', h(config) + 1 - config.nullValueSeparatorPadding.bottom)
|
|
89
|
+
.attr('x2', w(config))
|
|
90
|
+
.attr('y2', h(config) + 1 - config.nullValueSeparatorPadding.bottom)
|
|
91
|
+
.attr('stroke-width', 1)
|
|
92
|
+
.attr('stroke', '#777')
|
|
93
|
+
.attr('fill', 'none')
|
|
94
|
+
.attr('shape-rendering', 'crispEdges');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
flags.axes = true;
|
|
98
|
+
return this;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default createAxes;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import toTypeCoerceNumbers from './toTypeCoerceNumbers';
|
|
2
|
+
|
|
3
|
+
// attempt to determine types of each dimension based on first row of data
|
|
4
|
+
const detectDimensionTypes = data =>
|
|
5
|
+
Object.keys(data[0]).reduce((acc, cur) => {
|
|
6
|
+
const key = isNaN(Number(cur)) ? cur : parseInt(cur);
|
|
7
|
+
acc[key] = toTypeCoerceNumbers(data[0][cur]);
|
|
8
|
+
|
|
9
|
+
return acc;
|
|
10
|
+
}, {});
|
|
11
|
+
|
|
12
|
+
export default detectDimensionTypes;
|