/* https://bl.ocks.org/mbostock/4341954 https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172 https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771 */ // jshint esversion: 6 import React from "react"; import _ from "lodash"; import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core"; import { connect } from "react-redux"; import * as d3 from "d3"; import memoize from "memoize-one"; import { kvCache } from "../../util/stateManager"; import * as globals from "../../globals"; import actions from "../../actions"; @connect(state => ({ world: state.controls.world, scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor, crossfilter: state.controls.crossfilter, differential: state.differential, initializeRanges: _.get(state.controls.world, "summary.obs"), colorAccessor: state.controls.colorAccessor, colorScale: state.controls.colorScale, obsAnnotations: _.get(state.controls.world, "obsAnnotations", null) })) class HistogramBrush extends React.Component { calcHistogramCache = memoize((obsAnnotations, field, ranges) => { const { world } = this.props; const histogramCache = {}; histogramCache.y = d3 .scaleLinear() .range([this.height - this.marginBottom, 0]); if (obsAnnotations[0][field]) { // recalculate expensive stuff const allValuesForContinuousFieldAsArray = _.map(obsAnnotations, field); histogramCache.x = d3 .scaleLinear() .domain([ranges.min, ranges.max]) .range([0, this.width]); histogramCache.bins = d3 .histogram() .domain(histogramCache.x.domain()) .thresholds(40)(allValuesForContinuousFieldAsArray); histogramCache.numValues = allValuesForContinuousFieldAsArray.length; } else if (kvCache.get(world.varDataCache, field)) { /* it's not in observations, so it's a gene, but let's check to make sure */ const varValues = kvCache.get(world.varDataCache, field); histogramCache.x = d3 .scaleLinear() .domain( d3.extent(varValues) ) /* replace this if we have ranges for genes back from server like we do for annotations on cells */ .range([0, this.width]); histogramCache.bins = d3 .histogram() .domain(histogramCache.x.domain()) .thresholds(40)(varValues); histogramCache.numValues = varValues.length; } return histogramCache; }); constructor(props) { super(props); this.width = 340; this.height = 100; this.marginBottom = 20; } componentDidMount() { const { field } = this.props; const { x, y, bins, numValues, svgRef } = this._histogram; this.renderAxesBrushBins(x, y, bins, numValues, svgRef, field); } componentDidUpdate(prevProps) { const { field, obsAnnotations } = this.props; const { x, y, bins, numValues, svgRef } = this._histogram; if (obsAnnotations !== prevProps.obsAnnotations) { this.renderAxesBrushBins(x, y, bins, numValues, svgRef, field); } } onBrush(selection, x) { return () => { const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props; if (d3.event.selection) { dispatch({ type: "continuous metadata histogram brush", selection: field, continuousNamespace: { isObs, isUserDefined, isDiffExp }, range: [x(d3.event.selection[0]), x(d3.event.selection[1])] }); } else { dispatch({ type: "continuous metadata histogram brush", selection: field, continuousNamespace: { isObs, isUserDefined, isDiffExp }, range: null }); } }; } drawHistogram(svgRef) { const { obsAnnotations, field, ranges } = this.props; const histogramCache = this.calcHistogramCache( obsAnnotations, field, ranges ); const { x, y, bins, numValues } = histogramCache; this._histogram = { x, y, bins, numValues, svgRef }; } handleColorAction() { const { obsAnnotations, dispatch, field, world, initializeRanges } = this.props; if (obsAnnotations[0][field]) { dispatch({ type: "color by continuous metadata", colorAccessor: field, rangeMaxForColorAccessor: initializeRanges[field].range.max }); } else if (kvCache.get(world.varDataCache, field)) { dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field)); } } removeHistogram() { const { dispatch, field, colorAccessor } = this.props; dispatch({ type: "clear user defined gene", data: field }); if (field === colorAccessor) { dispatch({ type: "reset colorscale" }); } } handleSetGeneAsScatterplotX() { return () => { const { dispatch, field } = this.props; dispatch({ type: "set scatterplot x", data: field }); }; } handleSetGeneAsScatterplotY() { return () => { const { dispatch, field } = this.props; dispatch({ type: "set scatterplot y", data: field }); }; } renderAxesBrushBins(x, y, bins, numValues, svgRef, field) { /* Remove everything */ d3.select(svgRef) .selectAll("*") .remove(); /* BINS */ d3.select(svgRef) .insert("g", "*") .attr("fill", "#bbb") .selectAll("rect") .data(bins) .enter() .append("rect") .attr("class", "bar") .attr("x", d => x(d.x0) + 1) .attr("y", d => y(d.length / numValues)) .attr("width", d => Math.abs(x(d.x1) - x(d.x0) - 1)) .attr("height", d => y(0) - y(d.length / numValues)); /* BRUSH */ d3.select(svgRef) .append("g") .attr("class", "brush") .call( d3 .brushX() .on("brush", this.onBrush(field, x.invert).bind(this)) .on("end", this.onBrush(field, x.invert).bind(this)) ); /* AXIS */ d3.select(svgRef) .append("g") .attr("class", "axis axis--x") .attr("transform", `translate(0,${this.height - this.marginBottom})`) .call(d3.axisBottom(x).ticks(5)); d3.select(svgRef) .selectAll(".axis--x text") .style("fill", "rgb(80,80,80)"); d3.select(svgRef) .selectAll(".axis--x path") .style("stroke", "rgb(230,230,230)"); d3.select(svgRef) .selectAll(".axis--x line") .style("stroke", "rgb(230,230,230)"); } render() { const { field, colorAccessor, isUserDefined, isDiffExp, logFoldChange, pval, pvalAdj, scatterplotXXaccessor, scatterplotYYaccessor, zebra } = this.props; return (