mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 23:18:12 +08:00
* Added TS. Updated build and linting config. Added types. * [ts-migrate][.] Rename files from JS/JSX to TS/TSX Co-authored-by: ts-migrate <> * [ts-migrate][.] Run TS Migrate Co-authored-by: ts-migrate <> * Corrected files mangled by ts-migrate. * Updated lint config, minor linting. * Re-enabled Husky. * Updated tests and config. * Reverted webpack devtool config. * Removed obsolete snapshots. * Added annotations snap. * Updated tsconfig includes wrt linting. * Removed ts-migrate. Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
This commit is contained in:
co-authored by
ts-migrate
Timmy Huang
parent
7328cbdbd5
commit
934cc5c69b
@@ -0,0 +1,669 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { connect, shallowEqual } 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 Async from "react-async";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './scatterplot.css' or its corr... Remove this comment to see the full error message
|
||||
import styles from "./scatterplot.css";
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { margin, width, height } from "./util";
|
||||
import {
|
||||
createColorTable,
|
||||
createColorQuery,
|
||||
} from "../../util/stateManager/colorHelpers";
|
||||
import renderThrottle from "../../util/renderThrottle";
|
||||
import {
|
||||
flagBackground,
|
||||
flagSelected,
|
||||
flagHighlight,
|
||||
} from "../../util/glHelpers";
|
||||
|
||||
function createProjectionTF(viewportWidth: any, viewportHeight: any) {
|
||||
/*
|
||||
the projection transform accounts for the screen size & other layout
|
||||
*/
|
||||
const m = mat3.create();
|
||||
return mat3.projection(m, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
function getScale(col: any, rangeMin: any, rangeMax: any) {
|
||||
if (!col) return null;
|
||||
const { min, max } = col.summarize();
|
||||
return d3.scaleLinear().domain([min, max]).range([rangeMin, rangeMax]);
|
||||
}
|
||||
const getXScale = memoize(getScale);
|
||||
const getYScale = memoize(getScale);
|
||||
|
||||
type State = any;
|
||||
|
||||
// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message
|
||||
@connect((state) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'obsCrossfilter' does not exist on type '... Remove this comment to see the full error message
|
||||
const { obsCrossfilter: crossfilter } = state;
|
||||
const {
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
} = (state as any).controls;
|
||||
|
||||
return {
|
||||
annoMatrix: (state as any).annoMatrix,
|
||||
colors: (state as any).colors,
|
||||
pointDilation: (state as any).pointDilation,
|
||||
// Accessors are var/gene names (strings)
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
crossfilter,
|
||||
genesets: (state as any).genesets.genesets,
|
||||
};
|
||||
})
|
||||
class Scatterplot extends React.PureComponent<{}, State> {
|
||||
static createReglState(canvas: any) {
|
||||
/*
|
||||
Must be created for each canvas
|
||||
*/
|
||||
|
||||
// regl will create a top-level, full-screen canvas if we pass it a null.
|
||||
// canvas should never be null, so protect against that.
|
||||
if (!canvas) return {};
|
||||
|
||||
// setup canvas, webgl draw function and camera
|
||||
const regl = _regl(canvas);
|
||||
const drawPoints = _drawPoints(regl);
|
||||
|
||||
// preallocate webgl buffers
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const pointBuffer = regl.buffer();
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const colorBuffer = regl.buffer();
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const flagBuffer = regl.buffer();
|
||||
|
||||
return {
|
||||
regl,
|
||||
drawPoints,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
flagBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
static watchAsync(props: any, prevProps: any) {
|
||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||
}
|
||||
|
||||
axes: any;
|
||||
|
||||
reglCanvas: any;
|
||||
|
||||
renderCache: any;
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
computeHighlightFlags = memoize(
|
||||
(nObs, pointDilationData, pointDilationLabel) => {
|
||||
const flags = new Float32Array(nObs);
|
||||
if (pointDilationData) {
|
||||
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||
if (pointDilationData[i] === pointDilationLabel) {
|
||||
flags[i] = flagHighlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
);
|
||||
|
||||
computeColorByFlags = memoize((nObs, colorByData) => {
|
||||
const flags = new Float32Array(nObs);
|
||||
if (colorByData) {
|
||||
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||
const val = colorByData[i];
|
||||
if (typeof val === "number" && !Number.isFinite(val)) {
|
||||
flags[i] = flagBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
});
|
||||
|
||||
computePointFlags = memoize(
|
||||
(crossfilter, colorByData, pointDilationData, pointDilationLabel) => {
|
||||
/*
|
||||
We communicate with the shader using three flags:
|
||||
- isNaN -- the value is a NaN. Only makes sense when we have a colorAccessor
|
||||
- isSelected -- the value is selected
|
||||
- isHightlighted -- the value is highlighted in the UI (orthogonal from selection highlighting)
|
||||
|
||||
Due to constraints in webgl vertex shader attributes, these are encoded in a float, "kinda"
|
||||
like bitmasks.
|
||||
|
||||
We also have separate code paths for generating flags for categorical and
|
||||
continuous metadata, as they rely on different tests, and some of the flags
|
||||
(eg, isNaN) are meaningless in the face of categorical metadata.
|
||||
*/
|
||||
const nObs = crossfilter.size();
|
||||
|
||||
const selectedFlags = this.computeSelectedFlags(
|
||||
crossfilter,
|
||||
flagSelected,
|
||||
0
|
||||
);
|
||||
const highlightFlags = this.computeHighlightFlags(
|
||||
nObs,
|
||||
pointDilationData,
|
||||
pointDilationLabel
|
||||
);
|
||||
const colorByFlags = this.computeColorByFlags(nObs, colorByData);
|
||||
|
||||
const flags = new Float32Array(nObs);
|
||||
for (let i = 0; i < nObs; i += 1) {
|
||||
flags[i] = selectedFlags[i] + highlightFlags[i] + colorByFlags[i];
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
);
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
const viewport = this.getViewportDimensions();
|
||||
this.axes = false;
|
||||
this.reglCanvas = null;
|
||||
this.renderCache = null;
|
||||
this.state = {
|
||||
regl: null,
|
||||
drawPoints: null,
|
||||
minimized: null,
|
||||
viewport,
|
||||
projectionTF: createProjectionTF(width, height),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// this affect point render size for the scatterplot
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
}
|
||||
|
||||
setReglCanvas = (canvas: any) => {
|
||||
this.reglCanvas = canvas;
|
||||
if (canvas) {
|
||||
// no need to update this state if we are detaching.
|
||||
this.setState({
|
||||
...Scatterplot.createReglState(canvas),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getViewportDimensions = () => {
|
||||
return {
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
};
|
||||
};
|
||||
|
||||
handleResize = () => {
|
||||
const { state } = this.state;
|
||||
const viewport = this.getViewportDimensions();
|
||||
this.setState({
|
||||
...state,
|
||||
viewport,
|
||||
});
|
||||
};
|
||||
|
||||
fetchAsyncProps = async (props: any) => {
|
||||
const {
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
colors: colorsProp,
|
||||
crossfilter,
|
||||
pointDilation,
|
||||
} = props.watchProps;
|
||||
|
||||
const [
|
||||
expressionXDf,
|
||||
expressionYDf,
|
||||
colorDf,
|
||||
pointDilationDf,
|
||||
] = await this.fetchData(
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
colorsProp,
|
||||
pointDilation
|
||||
);
|
||||
const colorTable = this.updateColorTable(colorsProp, colorDf);
|
||||
|
||||
const xCol = expressionXDf.icol(0);
|
||||
const yCol = expressionYDf.icol(0);
|
||||
const xScale = getXScale(xCol, 0, width);
|
||||
const yScale = getYScale(yCol, height, 0);
|
||||
const positions = this.computePointPositions(
|
||||
xCol.asArray(),
|
||||
yCol.asArray(),
|
||||
xScale,
|
||||
yScale
|
||||
);
|
||||
|
||||
const colors = this.computePointColors(colorTable.rgb);
|
||||
|
||||
const { colorAccessor } = colorsProp;
|
||||
const colorByData = colorDf?.col(colorAccessor)?.asArray();
|
||||
const {
|
||||
metadataField: pointDilationCategory,
|
||||
categoryField: pointDilationLabel,
|
||||
} = pointDilation;
|
||||
const pointDilationData = pointDilationDf
|
||||
?.col(pointDilationCategory)
|
||||
?.asArray();
|
||||
const flags = this.computePointFlags(
|
||||
crossfilter,
|
||||
colorByData,
|
||||
pointDilationData,
|
||||
pointDilationLabel
|
||||
);
|
||||
|
||||
return {
|
||||
positions,
|
||||
colors,
|
||||
flags,
|
||||
width,
|
||||
height,
|
||||
xScale,
|
||||
yScale,
|
||||
};
|
||||
};
|
||||
|
||||
createXQuery(geneName: any) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
const { annoMatrix } = this.props;
|
||||
const { schema } = annoMatrix;
|
||||
const varIndex = schema?.annotations?.var?.index;
|
||||
if (!varIndex) return null;
|
||||
return [
|
||||
"X",
|
||||
{
|
||||
where: {
|
||||
field: "var",
|
||||
column: varIndex,
|
||||
value: geneName,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
createColorByQuery(colors: any) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
const { annoMatrix, genesets } = this.props;
|
||||
const { schema } = annoMatrix;
|
||||
const { colorMode, colorAccessor } = colors;
|
||||
return createColorQuery(colorMode, colorAccessor, schema, genesets);
|
||||
}
|
||||
|
||||
updateColorTable(colors: any, colorDf: any) {
|
||||
/* update color table state */
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
const { annoMatrix } = this.props;
|
||||
const { schema } = annoMatrix;
|
||||
const { colorAccessor, userColors, colorMode } = colors;
|
||||
return createColorTable(
|
||||
colorMode,
|
||||
colorAccessor,
|
||||
colorDf,
|
||||
schema,
|
||||
userColors
|
||||
);
|
||||
}
|
||||
|
||||
async fetchData(
|
||||
scatterplotXXaccessor: any,
|
||||
scatterplotYYaccessor: any,
|
||||
colors: any,
|
||||
pointDilation: any
|
||||
) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
const { annoMatrix } = this.props;
|
||||
const { metadataField: pointDilationAccessor } = pointDilation;
|
||||
|
||||
const promises = [];
|
||||
// X and Y dimensions
|
||||
promises.push(
|
||||
// @ts-expect-error ts-migrate(2488) FIXME: Type '(string | { where: { field: string; column: ... Remove this comment to see the full error message
|
||||
annoMatrix.fetch(...this.createXQuery(scatterplotXXaccessor))
|
||||
);
|
||||
promises.push(
|
||||
annoMatrix.fetch(...this.createXQuery(scatterplotYYaccessor))
|
||||
);
|
||||
|
||||
// color
|
||||
const query = this.createColorByQuery(colors);
|
||||
if (query) {
|
||||
promises.push(annoMatrix.fetch(...query));
|
||||
} else {
|
||||
promises.push(Promise.resolve(null));
|
||||
}
|
||||
|
||||
// point highlighting
|
||||
if (pointDilationAccessor) {
|
||||
promises.push(annoMatrix.fetch("obs", pointDilationAccessor));
|
||||
} else {
|
||||
promises.push(Promise.resolve(null));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
renderCanvas = renderThrottle(() => {
|
||||
const {
|
||||
regl,
|
||||
drawPoints,
|
||||
colorBuffer,
|
||||
pointBuffer,
|
||||
flagBuffer,
|
||||
projectionTF,
|
||||
} = this.state;
|
||||
this.renderPoints(
|
||||
regl,
|
||||
drawPoints,
|
||||
flagBuffer,
|
||||
colorBuffer,
|
||||
pointBuffer,
|
||||
projectionTF
|
||||
);
|
||||
});
|
||||
|
||||
updateReglAndRender(newRenderCache: any) {
|
||||
const { positions, colors, flags } = newRenderCache;
|
||||
this.renderCache = newRenderCache;
|
||||
const { pointBuffer, colorBuffer, flagBuffer } = this.state;
|
||||
pointBuffer({ data: positions, dimension: 2 });
|
||||
colorBuffer({ data: colors, dimension: 3 });
|
||||
flagBuffer({ data: flags, dimension: 1 });
|
||||
this.renderCanvas();
|
||||
}
|
||||
|
||||
renderPoints(
|
||||
regl: any,
|
||||
drawPoints: any,
|
||||
flagBuffer: any,
|
||||
colorBuffer: any,
|
||||
pointBuffer: any,
|
||||
projectionTF: any
|
||||
) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
const { annoMatrix } = this.props;
|
||||
if (!this.reglCanvas || !annoMatrix) return;
|
||||
|
||||
const { schema } = annoMatrix;
|
||||
const { viewport } = this.state;
|
||||
regl.poll();
|
||||
regl.clear({
|
||||
depth: 1,
|
||||
color: [1, 1, 1, 1],
|
||||
});
|
||||
drawPoints({
|
||||
flag: flagBuffer,
|
||||
color: colorBuffer,
|
||||
position: pointBuffer,
|
||||
projection: projectionTF,
|
||||
count: annoMatrix.nObs,
|
||||
nPoints: schema.dataframe.nObs,
|
||||
minViewportDimension: Math.min(
|
||||
viewport.width - globals.leftSidebarWidth || width,
|
||||
viewport.height || height
|
||||
),
|
||||
});
|
||||
regl._gl.flush();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message
|
||||
dispatch,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message
|
||||
annoMatrix,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'scatterplotXXaccessor' does not exist on... Remove this comment to see the full error message
|
||||
scatterplotXXaccessor,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'scatterplotYYaccessor' does not exist on... Remove this comment to see the full error message
|
||||
scatterplotYYaccessor,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'colors' does not exist on type 'Readonly... Remove this comment to see the full error message
|
||||
colors,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on type 'Rea... Remove this comment to see the full error message
|
||||
crossfilter,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'pointDilation' does not exist on type 'R... Remove this comment to see the full error message
|
||||
pointDilation,
|
||||
} = this.props;
|
||||
const { minimized, regl, viewport } = this.state;
|
||||
const bottomToolbarGutter = 48; // gutter for bottom tool bar
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: bottomToolbarGutter,
|
||||
borderRadius: "3px 3px 0px 0px",
|
||||
left: globals.leftSidebarWidth + globals.scatterplotMarginLeft,
|
||||
padding: "0px 20px 20px 0px",
|
||||
background: "white",
|
||||
/* x y blur spread color */
|
||||
boxShadow: "0px 0px 3px 2px rgba(153,153,153,0.2)",
|
||||
zIndex: 2,
|
||||
}}
|
||||
id="scatterplot_wrapper"
|
||||
>
|
||||
<ButtonGroup
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 5,
|
||||
top: 5,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
minimal
|
||||
onClick={() => {
|
||||
this.setState({ minimized: !minimized });
|
||||
}}
|
||||
>
|
||||
{minimized ? "show scatterplot" : "hide"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
minimal
|
||||
data-testid="clear-scatterplot"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "clear scatterplot",
|
||||
})
|
||||
}
|
||||
>
|
||||
remove
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div
|
||||
className={styles.scatterplot}
|
||||
id="scatterplot"
|
||||
style={{
|
||||
width: `${width + margin.left + margin.right}px`,
|
||||
height: `${
|
||||
(minimized ? 0 : height + margin.top) + margin.bottom
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
width={width}
|
||||
height={height}
|
||||
data-testid="scatterplot"
|
||||
style={{
|
||||
marginLeft: margin.left,
|
||||
marginTop: margin.top,
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '"none" | null' is not assignable to type 'Di... Remove this comment to see the full error message
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
ref={this.setReglCanvas}
|
||||
/>
|
||||
<Async
|
||||
watchFn={Scatterplot.watchAsync}
|
||||
promiseFn={this.fetchAsyncProps}
|
||||
watchProps={{
|
||||
annoMatrix,
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
colors,
|
||||
crossfilter,
|
||||
pointDilation,
|
||||
viewport,
|
||||
}}
|
||||
>
|
||||
<Async.Pending initial>Loading...</Async.Pending>
|
||||
<Async.Rejected>{(error) => error.message}</Async.Rejected>
|
||||
<Async.Fulfilled>
|
||||
{(asyncProps) => {
|
||||
if (regl && !shallowEqual(asyncProps, this.renderCache)) {
|
||||
this.updateReglAndRender(asyncProps);
|
||||
}
|
||||
return (
|
||||
<ScatterplotAxis
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ minimized: any; scatterplotYYaccessor: any... Remove this comment to see the full error message
|
||||
minimized={minimized}
|
||||
scatterplotYYaccessor={scatterplotYYaccessor}
|
||||
scatterplotXXaccessor={scatterplotXXaccessor}
|
||||
xScale={(asyncProps as any).xScale}
|
||||
yScale={(asyncProps as any).yScale}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Async.Fulfilled>
|
||||
</Async>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Scatterplot;
|
||||
|
||||
const ScatterplotAxis = React.memo(
|
||||
({
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'minimized' does not exist on type '{ chi... Remove this comment to see the full error message
|
||||
minimized,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'scatterplotYYaccessor' does not exist on... Remove this comment to see the full error message
|
||||
scatterplotYYaccessor,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'scatterplotXXaccessor' does not exist on... Remove this comment to see the full error message
|
||||
scatterplotXXaccessor,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'xScale' does not exist on type '{ childr... Remove this comment to see the full error message
|
||||
xScale,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'yScale' does not exist on type '{ childr... Remove this comment to see the full error message
|
||||
yScale,
|
||||
}) => {
|
||||
/*
|
||||
Axis for the scatterplot, rendered with SVG/D3. Props:
|
||||
* scatterplotXXaccessor - name of X axis
|
||||
* scatterplotXXaccessor - name of Y axis
|
||||
* xScale - D3 scale for X axis (domain to range)
|
||||
* yScale - D3 scale for Y axis (domain to range)
|
||||
|
||||
This also relies on the GLOBAL width/height/margin constants. If those become
|
||||
become variables, may need to add the params.
|
||||
*/
|
||||
|
||||
const svgRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || minimized) return;
|
||||
const svg = d3.select(svgRef.current);
|
||||
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
// the axes are much cleaner and easier now. No need to rotate and orient
|
||||
// the axis, just call axisBottom, axisLeft etc.
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const xAxis = d3.axisBottom().ticks(7).scale(xScale);
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
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);
|
||||
}, [scatterplotXXaccessor, scatterplotYYaccessor, xScale, yScale]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width + margin.left + margin.right}
|
||||
height={height + margin.top + margin.bottom}
|
||||
data-testid="scatterplot-svg"
|
||||
style={{
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '"none" | null' is not assignable to type 'Di... Remove this comment to see the full error message
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
>
|
||||
<g ref={svgRef} transform={`translate(${margin.left},${margin.top})`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user