mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-18 18:38:11 +08:00
Merge pull request #42 from chanzuckerberg/bkmartinjr-perf2
performance - graph, scatterplot and brush select
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// jshint esversion: 6
|
||||
import * as globals from "../globals";
|
||||
import store from "../reducers";
|
||||
import URI from "urijs";
|
||||
import _ from "lodash";
|
||||
|
||||
@@ -81,6 +82,30 @@ const initialize = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// This code defends against the case where /expression returns a cellname
|
||||
// never seen before (ie, not returned by /cells). This should not happen
|
||||
// (see https://github.com/chanzuckerberg/cellxgene-rest-api/issues/34) but
|
||||
// occasionally does.
|
||||
//
|
||||
function cleanupExpressionResponse(data) {
|
||||
const s = store.getState();
|
||||
const metadata = s.controls.currentCellSelectionMap;
|
||||
let errorOccured = false;
|
||||
const newcells = _.filter(data.data.cells, cell => {
|
||||
const found = metadata[cell.cellname];
|
||||
errorOccured = errorOccured || !found;
|
||||
return found;
|
||||
});
|
||||
|
||||
if (errorOccured) {
|
||||
console.error(
|
||||
"Warning: /expression REST API returned unexpected cell names -- discarding surprises."
|
||||
);
|
||||
data.data.cells = newcells;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const requestGeneExpressionCounts = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "get expression started" });
|
||||
@@ -91,6 +116,7 @@ const requestGeneExpressionCounts = () => {
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data => dispatch({ type: "get expression success", data }),
|
||||
error => dispatch({ type: "get expression error", error })
|
||||
@@ -112,6 +138,7 @@ const requestSingleGeneExpressionCountsForColoringPOST = gene => {
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data =>
|
||||
dispatch({
|
||||
@@ -142,6 +169,7 @@ const requestGeneExpressionCountsPOST = genes => {
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data => dispatch({ type: "get expression success", data }),
|
||||
error => dispatch({ type: "get expression error", error })
|
||||
|
||||
@@ -13,6 +13,7 @@ import fit from "canvas-fit";
|
||||
import _camera from "../../util/camera.js";
|
||||
import _regl from "regl";
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { scaleLinear } from "../../util/scaleLinear";
|
||||
|
||||
import FaCrosshair from "react-icons/lib/fa/crosshairs";
|
||||
import FaZoom from "react-icons/lib/fa/search-plus";
|
||||
@@ -41,7 +42,7 @@ import FaSave from "react-icons/lib/fa/download";
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
continuousSelection: state.controls.continuousSelection,
|
||||
graphMap: state.controls.graphMap,
|
||||
graphVec: state.controls.graphVec,
|
||||
currentCellSelection: state.controls.currentCellSelection,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells
|
||||
@@ -103,72 +104,43 @@ class Graph extends React.Component {
|
||||
sizeBuffer
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
/* maybe should do a check here to confirm ref exists and pass it? */
|
||||
// if (
|
||||
// this.state.ctx &&
|
||||
// nextProps.vertices
|
||||
// // nextProps.expressions &&
|
||||
// // nextProps.expressionsCountsMap &&
|
||||
// ) {
|
||||
// drawGraphUsingRenderQueue(
|
||||
// this.state.ctx,
|
||||
// nextProps.expressionsCountsMap,
|
||||
// nextProps.colorAccessor,
|
||||
// nextProps.ranges, /* assumption that this exists if vertices does both are on cells */
|
||||
// nextProps.metadata,
|
||||
// nextProps.currentCellSelection,
|
||||
// nextProps.graphBrushSelection,
|
||||
// nextProps.colorScale,
|
||||
// nextProps.graphMap,
|
||||
// nextProps.opacityForDeselectedCells,
|
||||
// )
|
||||
// }
|
||||
if (this.state.regl && nextProps.vertices) {
|
||||
const _currentCellSelectionMap = _.keyBy(
|
||||
nextProps.currentCellSelection,
|
||||
"CellName"
|
||||
); /* move me to the reducer */
|
||||
const vertices = nextProps.currentCellSelection;
|
||||
const vertexCount = vertices.length;
|
||||
const positions = new Float32Array(2 * vertexCount);
|
||||
const colors = new Float32Array(3 * vertexCount);
|
||||
const sizes = new Float32Array(vertexCount);
|
||||
|
||||
const positions = [];
|
||||
positions.length = nextProps.currentCellSelection.length;
|
||||
const colors = [];
|
||||
colors.length = nextProps.currentCellSelection.length;
|
||||
const sizes = [];
|
||||
sizes.length = nextProps.currentCellSelection.length;
|
||||
|
||||
const glScaleX = d3
|
||||
.scaleLinear()
|
||||
.domain([0, 1])
|
||||
.range([-1, 1]); /* padding */
|
||||
|
||||
const glScaleY = d3
|
||||
.scaleLinear()
|
||||
.domain([0, 1])
|
||||
.range([1, -1]); /* padding */
|
||||
// d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||
// d3.scaleLinear().domain([0,1]).range([1,-1])
|
||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||
|
||||
/*
|
||||
Construct Vectors
|
||||
*/
|
||||
_.each(nextProps.currentCellSelection, (cell, i) => {
|
||||
if (nextProps.graphMap[cell["CellName"]]) {
|
||||
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
|
||||
positions[i] = [
|
||||
glScaleX(nextProps.graphMap[cell["CellName"]][0]),
|
||||
glScaleY(nextProps.graphMap[cell["CellName"]][1])
|
||||
];
|
||||
const graphVec = nextProps.graphVec;
|
||||
for (var i = 0; i < vertexCount; i++) {
|
||||
const cell = vertices[i];
|
||||
const cellIdx = cell.__cellIndex__;
|
||||
const x = glScaleX(graphVec[2 * cellIdx]);
|
||||
const y = glScaleY(graphVec[2 * cellIdx + 1]);
|
||||
positions[2 * i] = x;
|
||||
positions[2 * i + 1] = y;
|
||||
|
||||
colors[i] = cell.__colorRGB__;
|
||||
sizes[i] = cell["__selected__"]
|
||||
? 4
|
||||
: 0.2; /* make this a function of the number of total cells, including regraph */
|
||||
}
|
||||
});
|
||||
colors.set(cell.__colorRGB__, 3 * i);
|
||||
|
||||
this.state.pointBuffer(positions);
|
||||
this.state.colorBuffer(colors);
|
||||
this.state.sizeBuffer(sizes);
|
||||
this.count = positions.length;
|
||||
sizes[i] = cell.__selected__
|
||||
? 4
|
||||
: 0.2; /* make this a function of the number of total cells, including regraph */
|
||||
}
|
||||
|
||||
this.state.pointBuffer({ data: positions, dimension: 2 });
|
||||
this.state.colorBuffer({ data: colors, dimension: 3 });
|
||||
this.state.sizeBuffer({ data: sizes, dimension: 1 });
|
||||
this.count = vertexCount;
|
||||
}
|
||||
}
|
||||
handleBrushSelectAction() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import fit from "canvas-fit";
|
||||
import _camera from "../../util/camera.js";
|
||||
import _regl from "regl";
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { scaleLinear } from "../../util/scaleLinear";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@@ -39,6 +40,7 @@ import { margin, width, height, createDimensions } from "./util";
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
currentCellSelection: state.controls.currentCellSelection,
|
||||
currentCellSelectionMap: state.controls.currentCellSelectionMap,
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
@@ -107,12 +109,12 @@ class Scatterplot extends React.Component {
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.state.xScale &&
|
||||
this.state.yScale &&
|
||||
(this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor) &&
|
||||
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor) || // was CLU now FTH1 etc
|
||||
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor
|
||||
this.state.xScale &&
|
||||
this.state.yScale &&
|
||||
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);
|
||||
}
|
||||
@@ -130,67 +132,54 @@ class Scatterplot extends React.Component {
|
||||
this.state.xScale &&
|
||||
this.state.yScale
|
||||
) {
|
||||
const _currentCellSelectionMap = _.keyBy(
|
||||
this.props.currentCellSelection,
|
||||
"CellName"
|
||||
); /* move me to the reducer */
|
||||
const currentCellSelectionMap = this.props.currentCellSelectionMap;
|
||||
|
||||
const positions = [];
|
||||
const colors = [];
|
||||
const sizes = [];
|
||||
const data = this.props.expression.data;
|
||||
const cells = data.cells;
|
||||
const genes = data.genes;
|
||||
const cellCount = cells.length;
|
||||
const positions = new Float32Array(2 * cellCount);
|
||||
const colors = new Float32Array(3 * cellCount);
|
||||
const sizes = new Float32Array(cellCount);
|
||||
|
||||
const glScaleX = d3
|
||||
.scaleLinear()
|
||||
.domain([0, width])
|
||||
.range([-0.95, 0.95]); /* padding */
|
||||
// d3.scaleLinear().domain([0, width]).range([-0.95, 0.95])
|
||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
||||
|
||||
const glScaleY = d3
|
||||
.scaleLinear()
|
||||
.domain([0, height])
|
||||
.range([-1, 1]);
|
||||
// d3.scaleLinear().domain([0, height]).range([-1, 1])
|
||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
||||
|
||||
const geneXXaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotXXaccessor
|
||||
);
|
||||
const geneYYaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
|
||||
/*
|
||||
Construct Vectors
|
||||
*/
|
||||
_.each(this.props.expression.data.cells, (cell, i) => {
|
||||
/*
|
||||
this if 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]) {
|
||||
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
|
||||
positions.push([
|
||||
glScaleX(
|
||||
this.state.xScale(
|
||||
cell.e[
|
||||
this.props.expression.data.genes.indexOf(
|
||||
this.props.scatterplotXXaccessor
|
||||
)
|
||||
]
|
||||
)
|
||||
) /* scale each point first to the window as we calculate extents separately below, so no need to repeat */,
|
||||
glScaleY(
|
||||
this.state.yScale(
|
||||
cell.e[
|
||||
this.props.expression.data.genes.indexOf(
|
||||
this.props.scatterplotYYaccessor
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]);
|
||||
for (var i = 0; i < cellCount; i++) {
|
||||
const cell = cells[i];
|
||||
const cellMetadata = currentCellSelectionMap[cell.cellname];
|
||||
|
||||
colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"]);
|
||||
sizes.push(
|
||||
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 4 : 0.2
|
||||
); /* make this a function of the number of total cells, including regraph */
|
||||
}
|
||||
});
|
||||
positions[2 * i] = glScaleX(
|
||||
this.state.xScale(cell.e[geneXXaccessorIndex])
|
||||
); /* scale each point first to the window as we calculate extents separately below, so no need to repeat */
|
||||
positions[2 * i + 1] = glScaleY(
|
||||
this.state.yScale(cell.e[geneYYaccessorIndex])
|
||||
);
|
||||
|
||||
this.state.pointBuffer(positions);
|
||||
this.state.colorBuffer(colors);
|
||||
this.state.sizeBuffer(sizes);
|
||||
this.count = positions.length;
|
||||
colors.set(cellMetadata.__colorRGB__, 3 * i);
|
||||
|
||||
sizes[i] = cellMetadata.__selected__
|
||||
? 4
|
||||
: 0.2; /* make this a function of the number of total cells, including regraph */
|
||||
}
|
||||
|
||||
this.state.pointBuffer({ data: positions, dimension: 2 });
|
||||
this.state.colorBuffer({ data: colors, dimension: 3 });
|
||||
this.state.sizeBuffer({ data: sizes, dimension: 1 });
|
||||
this.count = cellCount;
|
||||
}
|
||||
}
|
||||
maybeSetupScalesAndDrawAxes(nextProps) {
|
||||
|
||||
@@ -72,21 +72,25 @@ export const graphMargin = { top: 20, right: 10, bottom: 30, left: 40 };
|
||||
export const graphWidth = 960;
|
||||
export const graphHeight = 960;
|
||||
|
||||
export const graphXScale = d3
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
0,
|
||||
1
|
||||
]) /* while this is the default for d3, our data is normalized so better to be explicit */
|
||||
.range([0 + graphMargin.left, graphWidth - graphMargin.right]);
|
||||
|
||||
export const graphYScale = d3
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
0,
|
||||
1
|
||||
]) /* while this is the default for d3, our data is normalized so better to be explicit */
|
||||
.range([graphHeight - graphMargin.bottom, 0 + graphMargin.top]);
|
||||
import { scaleLinear } from "./util/scaleLinear";
|
||||
// d3.scaleLinear().domain([0,1]).range([0 + graphMargin.left, graphWidth - graphMargin.right])
|
||||
export const graphXScale = scaleLinear(
|
||||
[0, 1],
|
||||
[0 + graphMargin.left, graphWidth - graphMargin.right]
|
||||
);
|
||||
graphXScale.invert = scaleLinear(
|
||||
[0 + graphMargin.left, graphWidth - graphMargin.right],
|
||||
[0, 1]
|
||||
);
|
||||
// d3.scaleLinear().domain([0,1]).range([graphHeight - graphMargin.bottom, 0 + graphMargin.top])
|
||||
export const graphYScale = scaleLinear(
|
||||
[0, 1],
|
||||
[graphHeight - graphMargin.bottom, 0 + graphMargin.top]
|
||||
);
|
||||
graphYScale.invert = scaleLinear(
|
||||
[graphHeight - graphMargin.bottom, 0 + graphMargin.top],
|
||||
[0, 1]
|
||||
);
|
||||
|
||||
export const ordinalColors = [
|
||||
"#0ac115",
|
||||
|
||||
@@ -54,11 +54,12 @@ const updateCellSelectionMiddleware = store => {
|
||||
if (action.type === "color by categorical metadata") {
|
||||
colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
|
||||
|
||||
_.each(currentSelectionWithUpdatedColors, (cell, i) => {
|
||||
for (let i = 0; i < currentSelectionWithUpdatedColors.length; i++) {
|
||||
const cell = currentSelectionWithUpdatedColors[i];
|
||||
let c = colorScale(cell[action.colorAccessor]);
|
||||
currentSelectionWithUpdatedColors[i]["__color__"] = c;
|
||||
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
|
||||
});
|
||||
cell.__color__ = c;
|
||||
cell.__colorRGB__ = parseRGB(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === "color by continuous metadata") {
|
||||
|
||||
@@ -37,7 +37,7 @@ const updateCellSelectionMiddleware = store => {
|
||||
if (
|
||||
!filterJustChanged ||
|
||||
!s.controls.allCellsOnClient
|
||||
/* graphMap is set at the same time as allCells, so we assume it exists */
|
||||
/* graph is set at the same time as allCells, so we assume it exists */
|
||||
) {
|
||||
return next(
|
||||
action
|
||||
@@ -46,12 +46,11 @@ const updateCellSelectionMiddleware = store => {
|
||||
|
||||
/*
|
||||
- make a FRESH copy of all of the cells
|
||||
- metadata has cellname, and that's all we ever need (is a key to graphMap)
|
||||
- metadata has cellname and index, and that's all we ever need to reference cell info
|
||||
*/
|
||||
let newSelection = s.controls.currentCellSelection.slice(0);
|
||||
_.each(newSelection, cell => {
|
||||
cell["__selected__"] = true;
|
||||
});
|
||||
_.forEach(newSelection, cell => (cell.__selected__ = true));
|
||||
|
||||
/*
|
||||
in plain language...
|
||||
|
||||
@@ -74,26 +73,36 @@ const updateCellSelectionMiddleware = store => {
|
||||
? action.brushCoords
|
||||
: s.controls.graphBrushSelection;
|
||||
|
||||
_.each(newSelection, (cell, i) => {
|
||||
if (!s.controls.graphMap[cell["CellName"]]) {
|
||||
newSelection[i][
|
||||
"__selected__"
|
||||
] = false; /* make a toggle in future */
|
||||
return;
|
||||
}
|
||||
const northwestX = globals.graphXScale.invert(
|
||||
graphBrushSelection.northwestX
|
||||
);
|
||||
const southeastX = globals.graphXScale.invert(
|
||||
graphBrushSelection.southeastX
|
||||
);
|
||||
const northwestY = globals.graphYScale.invert(
|
||||
graphBrushSelection.northwestY
|
||||
);
|
||||
const southeastY = globals.graphYScale.invert(
|
||||
graphBrushSelection.southeastY
|
||||
);
|
||||
|
||||
const coords = s.controls.graphMap[cell["CellName"]]; // [0.08005009151334168, 0.6907652173913044]
|
||||
const graphVec = s.controls.graphVec;
|
||||
for (let i = 0; i < newSelection.length; i++) {
|
||||
const cell = newSelection[i];
|
||||
const cellId = cell.__cellIndex__;
|
||||
const x = graphVec[2 * cellId];
|
||||
const y = graphVec[2 * cellId + 1];
|
||||
|
||||
const pointIsInsideBrushBounds =
|
||||
globals.graphXScale(coords[0]) >= graphBrushSelection.northwestX &&
|
||||
globals.graphXScale(coords[0]) <= graphBrushSelection.southeastX &&
|
||||
globals.graphYScale(coords[1]) >= graphBrushSelection.northwestY &&
|
||||
globals.graphYScale(coords[1]) <= graphBrushSelection.southeastY;
|
||||
x >= northwestX &&
|
||||
x <= southeastX &&
|
||||
y <= northwestY &&
|
||||
y >= southeastY;
|
||||
|
||||
if (!pointIsInsideBrushBounds) {
|
||||
newSelection[i]["__selected__"] = false;
|
||||
cell.__selected__ = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
(action.type ===
|
||||
|
||||
33
src/reducers/controls.js
vendored
33
src/reducers/controls.js
vendored
@@ -8,8 +8,9 @@ const Controls = (
|
||||
allGeneNames: null,
|
||||
allCellsOnClient: null /* this comes from cells endpoint, this is world */,
|
||||
currentCellSelection: null /* this comes from user actions, all draw components use this, it is created by middleware */,
|
||||
graphMap: null,
|
||||
graphVec: null,
|
||||
categoricalAsBooleansMap: null,
|
||||
categoricalAsCellsMap: null,
|
||||
colorAccessor: null,
|
||||
colorScale: null,
|
||||
opacityForDeselectedCells: 0.2,
|
||||
@@ -33,12 +34,22 @@ const Controls = (
|
||||
allGeneNames: action.data.data.genes
|
||||
});
|
||||
case "request cells success":
|
||||
const graphMap = {};
|
||||
const currentCellSelection = action.data.data.metadata.slice(0);
|
||||
_.each(action.data.data.graph, g => {
|
||||
graphMap[g[0]] = [g[1], g[2]];
|
||||
// Store the graph in a linear array for fast access. Index into
|
||||
// the array by "cell index", which is stored as metadata field
|
||||
// __cellIndex__.
|
||||
//
|
||||
// Code below relies on the REST API guarantee that the graph and
|
||||
// metadata are returned as arrays with the same order and length.
|
||||
//
|
||||
const graphVec = new Float32Array(2 * action.data.data.graph.length);
|
||||
_.each(action.data.data.graph, (g, i) => {
|
||||
graphVec[2 * i] = g[1];
|
||||
graphVec[2 * i + 1] = g[2];
|
||||
});
|
||||
|
||||
const currentCellSelection = action.data.data.metadata.slice(0);
|
||||
const currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName");
|
||||
|
||||
/*
|
||||
construct a copy of the ranges object that only has categorical
|
||||
replace all counts with bool flags
|
||||
@@ -66,11 +77,12 @@ const Controls = (
|
||||
}
|
||||
});
|
||||
|
||||
_.each(currentCellSelection, cell => {
|
||||
cell["__selected__"] = true;
|
||||
cell["__color__"] =
|
||||
_.each(currentCellSelection, (cell, idx) => {
|
||||
cell.__cellIndex__ = idx;
|
||||
cell.__selected__ = true;
|
||||
cell.__color__ =
|
||||
"rgba(0,0,0,1)"; /* initial color for all cells in all charts */
|
||||
cell["__colorRGB__"] = parseRGB(cell["__color__"]);
|
||||
cell.__colorRGB__ = parseRGB(cell.__color__);
|
||||
|
||||
// Add each cell to its categorical metadata set.
|
||||
_.forEach(cell, (_value, key) => {
|
||||
@@ -87,7 +99,8 @@ const Controls = (
|
||||
return Object.assign({}, state, {
|
||||
allCellsOnClient: action.data.data,
|
||||
currentCellSelection,
|
||||
graphMap,
|
||||
currentCellSelectionMap,
|
||||
graphVec,
|
||||
categoricalAsBooleansMap,
|
||||
categoricalAsCellsMap,
|
||||
continuousUserDefinedRanges,
|
||||
|
||||
18
src/util/scaleLinear.js
Normal file
18
src/util/scaleLinear.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
// Substitute for a d3 linear scale - less flexible, more performant.
|
||||
// Returns a function which will scale a value.
|
||||
//
|
||||
// Example will scale [0,1] to [-1,1]
|
||||
// var myScale = scaleLinear([0, 1], [-1, 1]);
|
||||
// myScale(0) === -1
|
||||
// this is is equivalent to d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||
|
||||
export const scaleLinear = (domain, range) => {
|
||||
const offsetD = domain[0];
|
||||
const scale = (range[1] - range[0]) / (domain[1] - domain[0]);
|
||||
const offsetR = range[0];
|
||||
return function(v) {
|
||||
return (v - offsetD) * scale + offsetR;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user