import React from "react"; import { connect } from "react-redux"; import { Button, ButtonGroup } from "@blueprintjs/core"; import _regl from "regl"; import * as d3 from "d3"; import { mat3 } from "gl-matrix"; import memoize from "memoize-one"; import { isTypedArray } from "../../util/typeHelpers"; import * as globals from "../../globals"; import setupScatterplot from "./setupScatterplot"; import styles from "./scatterplot.css"; import _drawPoints from "./drawPointsRegl"; import { margin, width, height } from "./util"; import finiteExtent from "../../util/finiteExtent"; function createProjectionTF(viewportWidth, viewportHeight) { /* the projection transform accounts for the screen size & other layout */ const m = mat3.create(); return mat3.projection(m, viewportWidth, viewportHeight); } @connect(state => { const { world, crossfilter, universe } = state; const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls; const expressionX = scatterplotXXaccessor && world.varData.col(scatterplotXXaccessor)?.asArray(); const expressionY = scatterplotYYaccessor && world.varData.col(scatterplotYYaccessor)?.asArray(); return { world, universe, colorRGB: state.colors.rgb, colorScale: state.colors.scale, colorAccessor: state.colors.colorAccessor, // Accessors are var/gene names (strings) scatterplotXXaccessor, scatterplotYYaccessor, opacityForDeselectedCells: state.controls.opacityForDeselectedCells, differential: state.differential, expressionX, expressionY, crossfilter }; }) class Scatterplot extends React.Component { computePointPositions = memoize((X, Y, xScale, yScale) => { const positions = new Float32Array(2 * X.length); for (let i = 0, len = X.length; i < len; i += 1) { positions[2 * i] = xScale(X[i]); positions[2 * i + 1] = yScale(Y[i]); } return positions; }); computePointColors = memoize(rgb => { /* compute webgl colors for each point */ const colors = new Float32Array(3 * rgb.length); for (let i = 0, len = rgb.length; i < len; i += 1) { colors.set(rgb[i], 3 * i); } return colors; }); computeSelectedFlags = memoize( (crossfilter, flagSelected, flagUnselected) => { const x = crossfilter.fillByIsSelected( new Float32Array(crossfilter.size()), flagSelected, flagUnselected ); return x; } ); computePointFlags = memoize((world, crossfilter, colorAccessor) => { const flagSelected = 1; const flagNaN = 2; // XXX - coming soon. // const flagHighlight = 4; const flags = this.computeSelectedFlags( crossfilter, flagSelected, 0 ).slice(); const colorByColumn = colorAccessor ? world.obsAnnotations.col(colorAccessor)?.asArray() || world.varData.col(colorAccessor)?.asArray() : null; const colorByData = colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null; if (colorByData) { for (let i = 0, len = flags.length; i < len; i += 1) { flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN; } } return flags; }); constructor(props) { super(props); this.count = 0; this.axes = false; this.renderCache = { positions: null, colors: null, flags: null, xScale: null, yScale: null }; this.state = { svg: null, minimized: null }; } componentDidMount() { const { svg } = setupScatterplot(width, height, margin); let scales; const { expressionX, expressionY } = this.props; if (svg && expressionX && expressionY) { scales = Scatterplot.setupScales(expressionX, expressionY); this.drawAxesSVG(scales.xScale, scales.yScale, svg); this.renderCache = { ...this.renderCache, ...scales }; } const regl = _regl(this.reglCanvas); const drawPoints = _drawPoints(regl); // Create render transform const projectionTF = createProjectionTF( this.reglCanvas.width, this.reglCanvas.height ); // preallocate buffers const pointBuffer = regl.buffer(); const colorBuffer = regl.buffer(); const flagBuffer = regl.buffer(); this.renderPoints( regl, drawPoints, flagBuffer, colorBuffer, pointBuffer, projectionTF ); this.setState({ regl, flagBuffer, pointBuffer, colorBuffer, svg, drawPoints, projectionTF }); } componentDidUpdate(prevProps) { const { world, crossfilter, scatterplotXXaccessor, scatterplotYYaccessor, expressionX, expressionY, colorRGB, colorAccessor } = this.props; const { regl, pointBuffer, colorBuffer, flagBuffer, svg, drawPoints, projectionTF } = this.state; if ( scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || world !== prevProps.world // shape or clip of world changed ) { const scales = Scatterplot.setupScales(expressionX, expressionY); this.drawAxesSVG(scales.xScale, scales.yScale, svg); this.renderCache = { ...this.renderCache, ...scales }; } if (world && regl) { const { renderCache } = this; const { xScale, yScale } = this.renderCache; let needsRepaint = false; const newPositions = this.computePointPositions( expressionX, expressionY, xScale, yScale ); if (renderCache.positions !== newPositions) { renderCache.positions = newPositions; pointBuffer({ data: renderCache.positions, dimension: 2 }); needsRepaint = true; } /* colors for each point */ const newColors = this.computePointColors(colorRGB); if (renderCache.colors !== newColors) { renderCache.colors = newColors; colorBuffer({ data: renderCache.colors, dimension: 3 }); needsRepaint = true; } const newFlags = this.computePointFlags( world, crossfilter, colorAccessor ); if (renderCache.flags !== newFlags) { renderCache.flags = newFlags; flagBuffer({ data: renderCache.flags, dimension: 1 }); needsRepaint = true; } this.count = expressionX.length; if (needsRepaint) { this.renderPoints( regl, drawPoints, flagBuffer, colorBuffer, pointBuffer, projectionTF ); } } } static setupScales(expressionX, expressionY) { const xScale = d3 .scaleLinear() .domain(finiteExtent(expressionX)) .range([0, width]); const yScale = d3 .scaleLinear() .domain(finiteExtent(expressionY)) .range([height, 0]); return { xScale, yScale }; } drawAxesSVG(xScale, yScale, svg) { const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props; svg.selectAll("*").remove(); // the axes are much cleaner and easier now. No need to rotate and orient // the axis, just call axisBottom, axisLeft etc. const xAxis = d3 .axisBottom() .ticks(7) .scale(xScale); const yAxis = d3 .axisLeft() .ticks(7) .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. svg .append("g") .attr("transform", `translate(0,${height})`) .attr("class", "x axis") .call(xAxis); // y-axis is translated to (0,0) 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). svg .append("text") .attr("x", 10) .attr("y", 10) .attr("class", "label") .style("font-style", "italic") .text(scatterplotYYaccessor); svg .append("text") .attr("x", width) .attr("y", height - 10) .attr("text-anchor", "end") .attr("class", "label") .style("font-style", "italic") .text(scatterplotXXaccessor); } renderPoints( regl, drawPoints, flagBuffer, colorBuffer, pointBuffer, projectionTF ) { if (!this.reglCanvas) return; const { universe } = this.props; const { width: cvWidth, height: cvHeight } = this.reglCanvas; regl.poll(); regl.clear({ depth: 1, color: [1, 1, 1, 1] }); drawPoints({ flag: flagBuffer, color: colorBuffer, position: pointBuffer, projection: projectionTF, count: this.count, nPoints: universe.nObs, minViewportDimension: Math.min(cvWidth || width, cvHeight || height) }); regl._gl.flush(); } render() { const { dispatch } = this.props; const { minimized } = this.state; return (
{ this.reglCanvas = canvas; }} />
); } } export default Scatterplot;