interactive scatterplot

This commit is contained in:
Colin Megill
2018-02-21 14:49:43 -08:00
parent 7fcd49951e
commit 921b2ad2af
5 changed files with 300 additions and 0 deletions
@@ -0,0 +1,57 @@
import _ from "lodash";
import {
margin,
width,
height,
createDimensions,
} from "./util";
const drawScatterplotCanvas = (
context,
xScale,
yScale,
currentCellSelection,
opacityForDeselectedCells,
expression,
scatterplotXXaccessor,
scatterplotYYaccessor
) => {
/* clear canvas */
context.clearRect(0, 0, width, height);
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */
expression.data.cells.forEach((cell, i) => {
/*
this is necessary until we are no longer getting expression for all cells, but only for 'world'
...which will mean refetching when we regraph, or 'go back up to all cells'
*/
if (!_currentCellSelectionMap[cell.cellname]) { return }
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
xScale(cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]), /* x */
yScale(cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]), /* y */
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5, /* r */
0, /* sAngle */
2 * Math.PI /* eAngle */
);
context.fillStyle = _currentCellSelectionMap[cell.cellname]["__color__"]
if (_currentCellSelectionMap[cell.cellname]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
context.fill();
});
}
export default _.debounce(drawScatterplotCanvas, 50)
@@ -0,0 +1,15 @@
.scatterplot {
display: block;
}
.scatterplot svg,
.scatterplot canvas {
font: 10px sans-serif;
position: absolute;
}
.scatterplot canvas {
opacity: 0.9;
pointer-events: none;
}
+175
View File
@@ -0,0 +1,175 @@
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
import React from 'react';
import _ from "lodash";
import { connect } from "react-redux";
import scatterplot from "./scatterplot";
import setupScatterplot from "./setupScatterplot";
import styles from './scatterplot.css';
import drawScatterplotCanvas from "./drawScatterplotCanvas";
import {
margin,
width,
height,
createDimensions,
} from "./util";
@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 initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null;
return {
ranges,
metadata,
initializeRanges,
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
currentCellSelection: state.controls.currentCellSelection,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
differential: state.differential,
expression: state.expression,
}
})
class Scatterplot extends React.Component {
constructor(props) {
super(props);
this.state = {
svg: null,
ctx: null,
axes: null,
dimensions: null,
xScale: null,
yScale: null,
};
}
componentDidMount() {
const {svg, ctx} = setupScatterplot(
width,
height,
margin
);
this.setState({svg, ctx})
}
componentWillReceiveProps(nextProps) {
this.maybeSetupScalesAndDrawAxes(nextProps);
}
componentDidUpdate(prevProps) {
console.log('scatter sees color as', this.props.colorAccessor)
console.log('scatter sees scatter acess as', this.props.scatterplotXXaccessor, this.props.scatterplotYYaccessor)
if (
(this.props.scatterplotXXaccessor && this.props.scatterplotYYaccessor) &&
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor
) {
this.drawAxesSVG(this.state.xScale, this.state.yScale);
}
if (
this.state.xScale &&
this.state.yScale
) {
drawScatterplotCanvas(
this.state.ctx,
this.state.xScale,
this.state.yScale,
this.props.currentCellSelection,
this.props.opacityForDeselectedCells,
this.props.expression,
this.props.scatterplotXXaccessor,
this.props.scatterplotYYaccessor,
)
}
}
maybeSetupScalesAndDrawAxes(nextProps) {
if (
nextProps.expression &&
nextProps.scatterplotXXaccessor &&
nextProps.scatterplotYYaccessor
) {
console.log('expression in scatterplot: ', nextProps)
const xScale = d3.scaleLinear()
.domain(d3.extent(nextProps.expression.data.cells, (cell, i) => {
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotXXaccessor)]
}))
.range([0, width])
const yScale = d3.scaleLinear()
.domain(d3.extent(nextProps.expression.data.cells, (cell) => {
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotYYaccessor)]
}))
.range([height, 0])
this.setState({
xScale,
yScale
})
}
}
drawAxesSVG(xScale, yScale) {
this.state.svg.selectAll("*").remove();
// the axes are much cleaner and easier now. No need to rotate and orient the axis, just call axisBottom, axisLeft etc.
var xAxis = d3.axisBottom()
.scale(xScale);
var yAxis = d3.axisLeft()
.scale(yScale);
// adding axes is also simpler now, just translate x-axis to (0,height) and it's alread defined to be a bottom axis.
this.state.svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
.call(xAxis);
// y-axis is translated to (0,0)
this.state.svg.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis')
.call(yAxis);
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
this.state.svg.append('text')
.attr('x', 10)
.attr('y', 10)
.attr('class', 'label')
.text(this.props.scatterplotYYaccessor);
this.state.svg.append('text')
.attr('x', width)
.attr('y', height - 10)
.attr('text-anchor', 'end')
.attr('class', 'label')
.text(this.props.scatterplotXXaccessor);
}
render() {
return (
<div id="scatterplot_wrapper">
<div
className={styles.scatterplot}
id="scatterplot"
style={{
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px",
marginBottom: 60
}}
>scatter</div>
</div>
)
}
};
export default Scatterplot;
// <SectionHeader text="Continuous Metadata"/>
@@ -0,0 +1,42 @@
/*****************************************
******************************************
Setup SVG & Canvas elements
******************************************
******************************************/
const setupScatterplot = (
width,
height,
margin
) => {
var container = d3.select("#scatterplot")
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 + ")");
var canvas = container.append("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
.style("height", height + "px")
.style("margin-left", 10 + 3 + "px") /* magic number: this seems to be visually correct when it is equal to the amount axis is translated by plus or minus... a little? */
.style("margin-top", margin.top + margin.bottom - 2 + "px")
var ctx = canvas.node().getContext("2d");
ctx.globalCompositeOperation = 'darken';
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
return {
svg,
ctx,
}
}
export default setupScatterplot;
+11
View File
@@ -0,0 +1,11 @@
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 height = 680 - margin.top - margin.bottom;
export const innerHeight = height - 2;
export const devicePixelRatio = window.devicePixelRatio || 1;