This commit is contained in:
bkmartinjr
2018-05-05 13:15:10 -07:00
parent d91d42f2c4
commit 90c7daf571
62 changed files with 2398 additions and 1912 deletions
+1
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";
+51 -43
View File
@@ -1,9 +1,6 @@
import styles from './parallelCoordinates.css';
import {
yAxis,
brushstart,
} from "./util";
// jshint esversion: 6
import styles from "./parallelCoordinates.css";
import { yAxis, brushstart } from "./util";
const drawAxes = (
svg,
@@ -13,19 +10,19 @@ const drawAxes = (
height,
width,
handleBrushAction,
handleColorAction,
handleColorAction
) => {
/*****************************************
******************************************
Handles a brush event, toggling the display of foreground lines.
******************************************
******************************************/
function brush () {
function brush() {
var actives = [];
svg.selectAll(".parcoords_axis .parcoords_brush")
.filter(function (d) {
svg
.selectAll(".parcoords_axis .parcoords_brush")
.filter(function(d) {
return d3.brushSelection(this);
})
.each(function(d) {
@@ -35,46 +32,57 @@ const drawAxes = (
});
});
/* fire action, with selected dimensions & their values */
handleBrushAction(actives)
handleBrushAction(actives);
}
var axes = svg.selectAll(".parcoords_axis")
.data(dimensions)
.enter().append("g")
.attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", (d,i) => { return "translate(" + xscale(i) + ")"; });
var axes = svg
.selectAll(".parcoords_axis")
.data(dimensions)
.enter()
.append("g")
.attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", (d, i) => {
return "translate(" + xscale(i) + ")";
});
axes.append("g")
.each(function(d) {
var renderAxis = "axis" in d
? d.axis.scale(d.scale) // custom axis
: yAxis.scale(d.scale); // default axis
d3.select(this).call(renderAxis);
})
axes
.append("g")
.each(function(d) {
var renderAxis =
"axis" in d
? d.axis.scale(d.scale) // custom axis
: yAxis.scale(d.scale); // default axis
d3.select(this).call(renderAxis);
})
.append("text")
.on("click", (d) => { handleColorAction(d.key) })
.attr("class", styles.title)
.attr("text-anchor", "start")
.text(function(d) { return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️"; });
.on("click", d => {
handleColorAction(d.key);
})
.attr("class", styles.title)
.attr("text-anchor", "start")
.text(function(d) {
return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️";
});
// Add and store a brush for each axis.
axes.append("g")
.attr("class", `${styles.brush} parcoords_brush`)
.each(function(d) {
d3.select(this).call(
d.brush = d3.brushY()
.extent([[-10,0], [10, height]])
.on("start", brushstart)
.on("brush", brush)
.on("end", brush)
)
})
axes
.append("g")
.attr("class", `${styles.brush} parcoords_brush`)
.each(function(d) {
d3.select(this).call(
(d.brush = d3
.brushY()
.extent([[-10, 0], [10, height]])
.on("start", brushstart)
.on("brush", brush)
.on("end", brush))
);
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
.attr("x", -8)
.attr("width", 16);
return axes;
}
};
export default drawAxes;
+24 -33
View File
@@ -1,7 +1,6 @@
// jshint esversion: 6
import _ from "lodash";
import {
project
} from "./util";
import { project } from "./util";
import renderQueue from "../../util/renderQueue";
@@ -16,16 +15,15 @@ const drawLinesCanvas = (
dimensions,
xscale,
colorAccessor,
colorScale,
colorScale
) => {
return (d) => {
ctx.globalAlpha = .1;
return d => {
ctx.globalAlpha = 0.1;
if (d["__selected__"]) {
ctx.strokeStyle = d["__color__"];
} else {
return
return;
}
ctx.beginPath();
@@ -36,31 +34,31 @@ const drawLinesCanvas = (
// this bit renders horizontal lines on the previous/next
// dimensions, so that sandwiched null values are visible
if (i > 0) {
var prev = coords[i-1];
var prev = coords[i - 1];
if (prev !== null) {
ctx.moveTo(prev[0],prev[1]);
ctx.lineTo(prev[0]+6,prev[1]);
ctx.moveTo(prev[0], prev[1]);
ctx.lineTo(prev[0] + 6, prev[1]);
}
}
if (i < coords.length-1) {
var next = coords[i+1];
if (i < coords.length - 1) {
var next = coords[i + 1];
if (next !== null) {
ctx.moveTo(next[0]-6,next[1]);
ctx.moveTo(next[0] - 6, next[1]);
}
}
return;
}
if (i == 0) {
ctx.moveTo(p[0],p[1]);
ctx.moveTo(p[0], p[1]);
return;
}
ctx.lineTo(p[0],p[1]);
ctx.lineTo(p[0], p[1]);
});
ctx.stroke();
}
}
};
};
const drawCellLinesUsingRenderQueue = (
metadata,
@@ -68,21 +66,14 @@ const drawCellLinesUsingRenderQueue = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _renderLinesWithQueue = renderQueue(
drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
drawLinesCanvas(ctx, dimensions, xscale, colorAccessor, colorScale)
).rate(50);
_renderLinesWithQueue(metadata);
return _renderLinesWithQueue;
}
};
const drawCellLinesSync = (
metadata,
@@ -90,17 +81,17 @@ const drawCellLinesSync = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _draw = drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
_.each(metadata, _draw)
}
colorScale
);
_.each(metadata, _draw);
};
export default drawCellLinesUsingRenderQueue;
// export default drawCellLinesUsingRenderQueue;
+87 -59
View File
@@ -3,22 +3,31 @@ https://bl.ocks.org/mbostock/4341954
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
*/
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@connect((state) => {
@connect(state => {
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null;
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null;
const initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
currentCellSelection: state.controls.currentCellSelection,
}
currentCellSelection: state.controls.currentCellSelection
};
})
class HistogramBrush extends React.Component {
constructor(props) {
@@ -33,15 +42,11 @@ class HistogramBrush extends React.Component {
ctx: null,
axes: null,
dimensions: null,
brush: null,
brush: null
};
}
componentDidMount() {
}
componentDidUpdate() {
}
componentDidMount() {}
componentDidUpdate() {}
onBrush(selection, x) {
return () => {
if (d3.event.selection) {
@@ -57,81 +62,104 @@ class HistogramBrush extends React.Component {
range: null
});
}
}
};
}
drawHistogram(svgRef) {
const allValuesForContinuousFieldAsArray = _.map(
this.props.currentCellSelection,
this.props.metadataField
)
);
var x = d3.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, (d) => +d))
.range([0, this.width])
// .range([margin.left, width - margin.right]);
var x = d3
.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, d => +d))
.range([0, this.width]);
// .range([margin.left, width - margin.right]);
var y = d3.scaleLinear()
.range([this.height - this.marginBottom, 0])
// .range([height - margin.bottom, margin.top]);
var y = d3.scaleLinear().range([this.height - this.marginBottom, 0]);
// .range([height - margin.bottom, margin.top]);
const bins = d3.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray)
const bins = d3
.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray);
d3.select(svgRef)
d3
.select(svgRef)
.insert("g", "*")
.attr("fill", "#bbb")
.selectAll("rect")
.data(bins)
.enter().append("rect")
.attr("x", function(d) { return x(d.x0) + 1; })
.attr("y", function(d) { return y(d.length / allValuesForContinuousFieldAsArray.length); })
.attr("width", function(d) { return Math.abs(x(d.x1) - x(d.x0) - 1); })
.attr("height", function(d) { return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length); });
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x0) + 1;
})
.attr("y", function(d) {
return y(d.length / allValuesForContinuousFieldAsArray.length);
})
.attr("width", function(d) {
return Math.abs(x(d.x1) - x(d.x0) - 1);
})
.attr("height", function(d) {
return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length);
});
if (!this.state.brush && !this.state.axis) {
const brush = d3.select(svgRef)
.append('g')
.attr('class', 'brush')
const brush = d3
.select(svgRef)
.append("g")
.attr("class", "brush")
.call(
d3.brushX()
.on('end', this.onBrush(this.props.metadataField, x.invert).bind(this))
)
d3
.brushX()
.on(
"end",
this.onBrush(this.props.metadataField, x.invert).bind(this)
)
);
const xAxis = d3.select(svgRef)
const xAxis = d3
.select(svgRef)
.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + (this.height - this.marginBottom) + ")")
.call(
d3.axisBottom(x)
.ticks(5)
.attr(
"transform",
"translate(0," + (this.height - this.marginBottom) + ")"
)
.call(d3.axisBottom(x).ticks(5))
.append("text")
.attr("x", 300)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text(this.props.metadataField)
.attr("x", 300)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text(this.props.metadataField);
this.setState({brush, xAxis})
this.setState({ brush, xAxis });
}
}
render() {
return (
<div style={{marginTop: 10}} id={"histogram_" + this.props.metadataField}>
<svg width={this.width} height={this.height} ref={(svgRef) => { this.drawHistogram(svgRef)}}>
<div
style={{ marginTop: 10 }}
id={"histogram_" + this.props.metadataField}
>
<svg
width={this.width}
height={this.height}
ref={svgRef => {
this.drawHistogram(svgRef);
}}
>
{this.props.ranges.min}
{" to "}
{this.props.ranges.max}
</svg>
</div>
)
);
}
};
}
export default HistogramBrush;
export default HistogramBrush;
+42 -44
View File
@@ -1,29 +1,33 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from 'react';
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import styles from './parallelCoordinates.css';
import styles from "./parallelCoordinates.css";
import SectionHeader from "../framework/sectionHeader";
import setupParallelCoordinates from "./setupParallelCoordinates";
import drawAxes from "./drawAxes";
import drawLinesCanvas from "./drawLinesCanvas";
import {
margin,
width,
height,
createDimensions,
} from "./util";
import { margin, width, height, createDimensions } from "./util";
@connect((state) => {
@connect(state => {
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null;
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null;
const initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
ranges,
@@ -33,8 +37,8 @@ import {
colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection,
currentCellSelection: state.controls.currentCellSelection,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn,
}
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
};
})
class Parallel extends React.Component {
constructor(props) {
@@ -43,16 +47,12 @@ class Parallel extends React.Component {
svg: null,
ctx: null,
axes: null,
dimensions: null,
dimensions: null
};
}
componentDidMount() {
const {svg, ctx} = setupParallelCoordinates(
width,
height,
margin
);
this.setState({svg, ctx})
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
this.setState({ svg, ctx });
}
componentWillReceiveProps(nextProps) {
this.maybeDrawAxes(nextProps);
@@ -63,10 +63,10 @@ class Parallel extends React.Component {
!this.state.axes &&
nextProps.initializeRanges /* axes are created on full range of data */
) {
const dimensions = createDimensions(nextProps.initializeRanges);
const xscale = d3.scalePoint()
const xscale = d3
.scalePoint()
.domain(d3.range(dimensions.length))
.range([0, width]);
@@ -78,28 +78,27 @@ class Parallel extends React.Component {
height,
width,
this.handleBrushAction.bind(this),
this.handleColorAction.bind(this),
this.handleColorAction.bind(this)
);
this.setState({
axes,
xscale,
dimensions,
})
dimensions
});
this.props.dispatch({
type: "parallel coordinates axes have been drawn"
})
});
}
}
maybeDrawLines = _.debounce((nextProps) => { /* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
maybeDrawLines = _.debounce(nextProps => {
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
if (
nextProps.ranges &&
nextProps.currentCellSelection &&
nextProps.axesHaveBeenDrawn
) {
if (this.state._drawLinesCanvas) {
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
}
@@ -112,22 +111,22 @@ class Parallel extends React.Component {
this.state.xscale,
this.state.ctx,
nextProps.colorAccessor,
nextProps.colorScale,
nextProps.colorScale
);
this.setState({
_drawLinesCanvas, /* this will only exist if the internals of drawLinesCanvas are using the render queue */
})
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
});
}
}, 200)
}, 200);
handleBrushAction (selection) {
handleBrushAction(selection) {
this.props.dispatch({
type: "continuous selection using parallel coords brushing",
data: selection
})
});
}
handleColorAction (key) {
handleColorAction(key) {
this.props.dispatch({
type: "color by continuous metadata",
colorAccessor: key,
@@ -136,22 +135,21 @@ class Parallel extends React.Component {
}
render() {
return (
<div id="parcoords_wrapper">
<div
className={styles.parcoords}
id="parcoords"
style={{
width: width + margin.left + margin.right + "px",
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}></div>
}}
/>
</div>
)
);
}
};
}
export default Parallel;
// <SectionHeader text="Continuous Metadata"/>
@@ -3,22 +3,19 @@
Setup SVG & Canvas elements
******************************************
******************************************/
// jshint esversion: 6
const setupParallelCoordinates = (width, height, margin) => {
var container = d3.select("#parcoords");
const setupParallelCoordinates = (
width,
height,
margin
) => {
var container = d3.select("#parcoords")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svg = container
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = container.append("canvas")
var canvas = container
.append("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
@@ -27,16 +24,15 @@ const setupParallelCoordinates = (
.style("margin-left", margin.left + "px");
var ctx = canvas.node().getContext("2d");
ctx.globalCompositeOperation = 'darken';
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
ctx.globalCompositeOperation = "darken";
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
return {
svg,
ctx,
}
ctx
};
};
}
export default setupParallelCoordinates
export default setupParallelCoordinates;
+25 -19
View File
@@ -1,51 +1,57 @@
// jshint esversion: 6
import _ from "lodash";
const paddingRight = 120;
const continuousChartWidth = 1200;
export const margin = {top: 66, right: 110, bottom: 20, left: 60};
export const width = continuousChartWidth - margin.left - margin.right - paddingRight;
export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
export const width =
continuousChartWidth - margin.left - margin.right - paddingRight;
export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2;
export const devicePixelRatio = window.devicePixelRatio || 1;
export const createDimensions = (data) => {
const newArr = []
export const createDimensions = data => {
const newArr = [];
_.each(data, (value, key) => {
if (value.range) {
newArr.push({
key: key, /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */
key: key /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */,
type: {
within: (d, extent, dim) => {
return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1];
},
}
},
scale: d3.scaleSqrt().range([innerHeight, 0]).domain([0, value.range.max])
})
scale: d3
.scaleSqrt()
.range([innerHeight, 0])
.domain([0, value.range.max])
});
}
})
});
return newArr;
}
};
export const yAxis = d3.axisLeft();
export const brushstart = () => {
d3.event.sourceEvent.stopPropagation();
}
};
export const d3_functor = (v) => {
return typeof v === "function" ? v : () => { return v; };
export const d3_functor = v => {
return typeof v === "function"
? v
: () => {
return v;
};
};
export const project = (d, dimensions, xscale) => {
return dimensions.map((p,i) => {
return dimensions.map((p, i) => {
// check if data element has property and contains a value
if (
!(p.key in d) ||
d[p.key] === null
) return null;
if (!(p.key in d) || d[p.key] === null) return null;
return [xscale(i),p.scale(d[p.key])];
return [xscale(i), p.scale(d[p.key])];
});
};