From 921b2ad2afc2842b60e408c9651d83c52cfd4b68 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Wed, 21 Feb 2018 14:49:43 -0800 Subject: [PATCH] interactive scatterplot --- .../scatterplot/drawScatterplotCanvas.js | 57 ++++++ src/components/scatterplot/scatterplot.css | 15 ++ src/components/scatterplot/scatterplot.js | 175 ++++++++++++++++++ .../scatterplot/setupScatterplot.js | 42 +++++ src/components/scatterplot/util.js | 11 ++ 5 files changed, 300 insertions(+) create mode 100644 src/components/scatterplot/drawScatterplotCanvas.js create mode 100644 src/components/scatterplot/scatterplot.css create mode 100644 src/components/scatterplot/scatterplot.js create mode 100644 src/components/scatterplot/setupScatterplot.js create mode 100644 src/components/scatterplot/util.js diff --git a/src/components/scatterplot/drawScatterplotCanvas.js b/src/components/scatterplot/drawScatterplotCanvas.js new file mode 100644 index 00000000..f47a79e9 --- /dev/null +++ b/src/components/scatterplot/drawScatterplotCanvas.js @@ -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) \ No newline at end of file diff --git a/src/components/scatterplot/scatterplot.css b/src/components/scatterplot/scatterplot.css new file mode 100644 index 00000000..b567ed53 --- /dev/null +++ b/src/components/scatterplot/scatterplot.css @@ -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; +} diff --git a/src/components/scatterplot/scatterplot.js b/src/components/scatterplot/scatterplot.js new file mode 100644 index 00000000..54ea4013 --- /dev/null +++ b/src/components/scatterplot/scatterplot.js @@ -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 ( +
+
scatter
+
+ ) + } +}; + +export default Scatterplot; + + +// diff --git a/src/components/scatterplot/setupScatterplot.js b/src/components/scatterplot/setupScatterplot.js new file mode 100644 index 00000000..31f4ac7a --- /dev/null +++ b/src/components/scatterplot/setupScatterplot.js @@ -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; diff --git a/src/components/scatterplot/util.js b/src/components/scatterplot/util.js new file mode 100644 index 00000000..d9045ab4 --- /dev/null +++ b/src/components/scatterplot/util.js @@ -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;