chore(lint): convert class components to function components

Convert all remaining React class components to function components
using hooks (useState, useCallback, useEffect, useRef, useMemo) to
satisfy the react-prefer-function-component ESLint rule.

Key changes:
- Converted components in dashboard, explore, SqlLab, and Chart areas
- Updated associated test files with proper typing
- Fixed JSX.Element return types for components used as JSX
- Added explicit ControlHeader props where needed
- Fixed shouldFocus callback signature in WithPopoverMenu usage

Notable exceptions (not converted):
- ErrorBoundary (uses componentDidCatch)
- DragDroppable (react-dnd requires class instances)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-02-11 09:15:51 -08:00
parent 63f1d9eb98
commit 5dffbc26ed
76 changed files with 11874 additions and 11869 deletions

View File

@@ -16,9 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable react/jsx-sort-default-props, react/sort-prop-types */
/* eslint-disable react/forbid-prop-types, react/require-default-props */
import { Component } from 'react';
import { useState, useCallback, useMemo, memo } from 'react';
import MapGL from 'react-map-gl';
import { WebMercatorViewport } from '@math.gl/web-mercator';
import ScatterPlotGlowOverlay from './ScatterPlotGlowOverlay';
@@ -63,30 +61,24 @@ interface MapBoxProps {
bounds?: [[number, number], [number, number]]; // May be undefined for empty datasets
}
interface MapBoxState {
viewport: Viewport;
}
const defaultProps: Partial<MapBoxProps> = {
width: 400,
height: 400,
globalOpacity: 1,
onViewportChange: NOOP,
pointRadius: DEFAULT_POINT_RADIUS,
pointRadiusUnit: 'Pixels',
};
class MapBox extends Component<MapBoxProps, MapBoxState> {
static defaultProps = defaultProps;
constructor(props: MapBoxProps) {
super(props);
const { width = 400, height = 400, bounds } = this.props;
// Get a viewport that fits the given bounds, which all marks to be clustered.
// Derive lat, lon and zoom from this viewport. This is only done on initial
// render as the bounds don't update as we pan/zoom in the current design.
function MapBox({
width = 400,
height = 400,
aggregatorName,
clusterer,
globalOpacity = 1,
hasCustomMetric,
mapStyle,
mapboxApiKey,
onViewportChange = NOOP,
pointRadius = DEFAULT_POINT_RADIUS,
pointRadiusUnit = 'Pixels',
renderWhileDragging,
rgb,
bounds,
}: MapBoxProps) {
// Compute initial viewport from bounds
const initialViewport = useMemo((): Viewport => {
let latitude = 0;
let longitude = 0;
let zoom = 1;
@@ -100,92 +92,72 @@ class MapBox extends Component<MapBoxProps, MapBoxState> {
({ latitude, longitude, zoom } = mercator);
}
this.state = {
viewport: {
longitude,
latitude,
zoom,
},
};
this.handleViewportChange = this.handleViewportChange.bind(this);
}
return { longitude, latitude, zoom };
}, []); // Only compute once on mount - bounds don't update as we pan/zoom
handleViewportChange(viewport: Viewport) {
this.setState({ viewport });
const { onViewportChange } = this.props;
onViewportChange!(viewport);
}
const [viewport, setViewport] = useState<Viewport>(initialViewport);
render() {
const {
width,
height,
aggregatorName,
clusterer,
globalOpacity,
mapStyle,
mapboxApiKey,
pointRadius,
pointRadiusUnit,
renderWhileDragging,
rgb,
hasCustomMetric,
bounds,
} = this.props;
const { viewport } = this.state;
const isDragging =
viewport.isDragging === undefined ? false : viewport.isDragging;
const handleViewportChange = useCallback(
(newViewport: Viewport) => {
setViewport(newViewport);
onViewportChange(newViewport);
},
[onViewportChange],
);
// Compute the clusters based on the original bounds and current zoom level. Note when zoom/pan
// to an area outside of the original bounds, no additional queries are made to the backend to
// retrieve additional data.
// add this variable to widen the visible area
const offsetHorizontal = ((width ?? 400) * 0.5) / 100;
const offsetVertical = ((height ?? 400) * 0.5) / 100;
const isDragging =
viewport.isDragging === undefined ? false : viewport.isDragging;
// Guard against empty datasets where bounds may be undefined
const bbox =
bounds && bounds[0] && bounds[1]
? [
bounds[0][0] - offsetHorizontal,
bounds[0][1] - offsetVertical,
bounds[1][0] + offsetHorizontal,
bounds[1][1] + offsetVertical,
]
: [-180, -90, 180, 90]; // Default to world bounds
// Compute the clusters based on the original bounds and current zoom level. Note when zoom/pan
// to an area outside of the original bounds, no additional queries are made to the backend to
// retrieve additional data.
// add this variable to widen the visible area
const offsetHorizontal = (width * 0.5) / 100;
const offsetVertical = (height * 0.5) / 100;
const clusters = clusterer.getClusters(bbox, Math.round(viewport.zoom));
// Guard against empty datasets where bounds may be undefined
const bbox =
bounds && bounds[0] && bounds[1]
? [
bounds[0][0] - offsetHorizontal,
bounds[0][1] - offsetVertical,
bounds[1][0] + offsetHorizontal,
bounds[1][1] + offsetVertical,
]
: [-180, -90, 180, 90]; // Default to world bounds
return (
<MapGL
const clusters = clusterer.getClusters(bbox, Math.round(viewport.zoom));
const lngLatAccessor = useCallback((location: GeoJSONLocation) => {
const { coordinates } = location.geometry;
return [coordinates[0], coordinates[1]] as [number, number];
}, []);
return (
<MapGL
{...viewport}
mapStyle={mapStyle}
width={width}
height={height}
mapboxApiAccessToken={mapboxApiKey}
onViewportChange={handleViewportChange}
preserveDrawingBuffer
>
<ScatterPlotGlowOverlay
{...viewport}
mapStyle={mapStyle}
width={width}
height={height}
mapboxApiAccessToken={mapboxApiKey}
onViewportChange={this.handleViewportChange}
preserveDrawingBuffer
>
<ScatterPlotGlowOverlay
{...viewport}
isDragging={isDragging}
locations={clusters}
dotRadius={pointRadius}
pointRadiusUnit={pointRadiusUnit}
rgb={rgb}
globalOpacity={globalOpacity}
compositeOperation="screen"
renderWhileDragging={renderWhileDragging}
aggregation={hasCustomMetric ? aggregatorName : undefined}
lngLatAccessor={(location: GeoJSONLocation) => {
const { coordinates } = location.geometry;
return [coordinates[0], coordinates[1]];
}}
/>
</MapGL>
);
}
isDragging={isDragging}
locations={clusters}
dotRadius={pointRadius}
pointRadiusUnit={pointRadiusUnit}
rgb={rgb}
globalOpacity={globalOpacity}
compositeOperation="screen"
renderWhileDragging={renderWhileDragging}
aggregation={hasCustomMetric ? aggregatorName : undefined}
lngLatAccessor={lngLatAccessor}
/>
</MapGL>
);
}
export default MapBox;
export default memo(MapBox);

View File

@@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable react/require-default-props */
import { PureComponent } from 'react';
import { memo, useCallback } from 'react';
import { CanvasOverlay } from 'react-map-gl';
import { kmToPixels, MILES_PER_KM } from './utils/geo';
import roundDecimal from './utils/roundDecimal';
@@ -61,16 +60,10 @@ interface ScatterPlotGlowOverlayProps {
isDragging?: boolean;
}
const defaultProps: Partial<ScatterPlotGlowOverlayProps> = {
// Same as browser default.
compositeOperation: 'source-over',
dotRadius: 4,
lngLatAccessor: (location: GeoJSONLocation) => [
location.geometry.coordinates[0],
location.geometry.coordinates[1],
],
renderWhileDragging: true,
};
const defaultLngLatAccessor = (location: GeoJSONLocation): [number, number] => [
location.geometry.coordinates[0],
location.geometry.coordinates[1],
];
const computeClusterLabel = (
properties: Record<string, number | string | boolean | null | undefined>,
@@ -101,65 +94,293 @@ const computeClusterLabel = (
return count;
};
class ScatterPlotGlowOverlay extends PureComponent<ScatterPlotGlowOverlayProps> {
static defaultProps = defaultProps;
function ScatterPlotGlowOverlay({
aggregation,
compositeOperation = 'source-over',
dotRadius = 4,
globalOpacity,
lngLatAccessor = defaultLngLatAccessor,
locations,
pointRadiusUnit,
renderWhileDragging = true,
rgb,
zoom,
}: ScatterPlotGlowOverlayProps) {
const drawText = useCallback(
(
ctx: CanvasRenderingContext2D,
pixel: [number, number],
options: DrawTextOptions = {},
) => {
const IS_DARK_THRESHOLD = 110;
const {
fontHeight = 0,
label = '',
radius = 0,
rgb: rgbOption = [0, 0, 0],
shadow = false,
} = options;
const maxWidth = radius * 1.8;
const luminance = luminanceFromRGB(
rgbOption[1] as number,
rgbOption[2] as number,
rgbOption[3] as number,
);
constructor(props: ScatterPlotGlowOverlayProps) {
super(props);
this.redraw = this.redraw.bind(this);
}
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
ctx.font = `${fontHeight}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (shadow) {
ctx.shadowBlur = 15;
ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
}
drawText(
ctx: CanvasRenderingContext2D,
pixel: [number, number],
options: DrawTextOptions = {},
) {
const IS_DARK_THRESHOLD = 110;
const {
fontHeight = 0,
label = '',
radius = 0,
rgb = [0, 0, 0],
shadow = false,
} = options;
const maxWidth = radius * 1.8;
const luminance = luminanceFromRGB(
rgb[1] as number,
rgb[2] as number,
rgb[3] as number,
);
const textWidth = ctx.measureText(String(label)).width;
if (textWidth > maxWidth) {
const scale = fontHeight / textWidth;
ctx.font = `${scale * maxWidth}px sans-serif`;
}
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
ctx.font = `${fontHeight}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (shadow) {
ctx.shadowBlur = 15;
ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
}
ctx.fillText(String(label), pixel[0], pixel[1]);
ctx.globalCompositeOperation = (compositeOperation ??
'source-over') as GlobalCompositeOperation;
ctx.shadowBlur = 0;
ctx.shadowColor = '';
},
[compositeOperation],
);
const textWidth = ctx.measureText(String(label)).width;
if (textWidth > maxWidth) {
const scale = fontHeight / textWidth;
ctx.font = `${scale * maxWidth}px sans-serif`;
}
const redraw = useCallback(
({ width, height, ctx, isDragging, project }: RedrawParams) => {
const radius = dotRadius ?? 4;
const clusterLabelMap: (number | string)[] = [];
const { compositeOperation } = this.props;
locations.forEach((location, i) => {
if (location.properties.cluster) {
clusterLabelMap[i] = computeClusterLabel(
location.properties,
aggregation,
);
}
});
ctx.fillText(String(label), pixel[0], pixel[1]);
ctx.globalCompositeOperation = (compositeOperation ??
'source-over') as GlobalCompositeOperation;
ctx.shadowBlur = 0;
ctx.shadowColor = '';
}
const filteredLabels = clusterLabelMap.filter(
v => !Number.isNaN(v),
) as number[];
// Guard against empty array or zero max to prevent NaN from division
const maxLabel =
filteredLabels.length > 0 ? Math.max(...filteredLabels) : 1;
const safeMaxLabel = maxLabel > 0 ? maxLabel : 1;
// Modified: https://github.com/uber/react-map-gl/blob/master/overlays/scatterplot.react.js
redraw({ width, height, ctx, isDragging, project }: RedrawParams) {
const {
// Calculate min/max radius values for Pixels mode scaling
let minRadiusValue = Infinity;
let maxRadiusValue = -Infinity;
if (pointRadiusUnit === 'Pixels') {
locations.forEach(location => {
// Accept both null and undefined as "no value" and coerce potential numeric strings
if (
!location.properties.cluster &&
location.properties.radius != null
) {
const radiusValueRaw = location.properties.radius;
const radiusValue = Number(radiusValueRaw);
if (Number.isFinite(radiusValue)) {
minRadiusValue = Math.min(minRadiusValue, radiusValue);
maxRadiusValue = Math.max(maxRadiusValue, radiusValue);
}
}
});
}
ctx.clearRect(0, 0, width, height);
ctx.globalCompositeOperation = (compositeOperation ??
'source-over') as GlobalCompositeOperation;
if ((renderWhileDragging || !isDragging) && locations) {
locations.forEach((location: GeoJSONLocation, i: number) => {
const pixel = project(lngLatAccessor(location)) as [number, number];
const pixelRounded: [number, number] = [
roundDecimal(pixel[0], 1),
roundDecimal(pixel[1], 1),
];
if (
pixelRounded[0] + radius >= 0 &&
pixelRounded[0] - radius < width &&
pixelRounded[1] + radius >= 0 &&
pixelRounded[1] - radius < height
) {
ctx.beginPath();
if (location.properties.cluster) {
const clusterLabel = clusterLabelMap[i];
// Validate clusterLabel is a finite number before using it for radius calculation
const numericLabel = Number(clusterLabel);
const safeNumericLabel = Number.isFinite(numericLabel)
? numericLabel
: 0;
const scaledRadius = roundDecimal(
(safeNumericLabel / safeMaxLabel) ** 0.5 * radius,
1,
);
const fontHeight = roundDecimal(scaledRadius * 0.5, 1);
const [x, y] = pixelRounded;
const gradient = ctx.createRadialGradient(
x,
y,
scaledRadius,
x,
y,
0,
);
gradient.addColorStop(
1,
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${0.8 * (globalOpacity ?? 1)})`,
);
gradient.addColorStop(
0,
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, 0)`,
);
ctx.arc(
pixelRounded[0],
pixelRounded[1],
scaledRadius,
0,
Math.PI * 2,
);
ctx.fillStyle = gradient;
ctx.fill();
if (Number.isFinite(safeNumericLabel)) {
let label: string | number = clusterLabel;
if (safeNumericLabel >= 10000) {
label = `${Math.round(safeNumericLabel / 1000)}k`;
} else if (safeNumericLabel >= 1000) {
label = `${Math.round(safeNumericLabel / 100) / 10}k`;
}
drawText(ctx, pixelRounded, {
fontHeight,
label,
radius: scaledRadius,
rgb,
shadow: true,
});
}
} else {
const defaultRadius = radius / 6;
const rawRadius = location.properties.radius;
const radiusProperty =
typeof rawRadius === 'number' ? rawRadius : null;
const pointMetric = location.properties.metric ?? null;
let pointRadius: number = radiusProperty ?? defaultRadius;
let pointLabel: string | number | undefined;
if (radiusProperty != null) {
const pointLatitude = lngLatAccessor(location)[1];
if (pointRadiusUnit === 'Kilometers') {
pointLabel = `${roundDecimal(pointRadius, 2)}km`;
pointRadius = kmToPixels(
pointRadius,
pointLatitude,
zoom ?? 0,
);
} else if (pointRadiusUnit === 'Miles') {
pointLabel = `${roundDecimal(pointRadius, 2)}mi`;
pointRadius = kmToPixels(
pointRadius * MILES_PER_KM,
pointLatitude,
zoom ?? 0,
);
} else if (pointRadiusUnit === 'Pixels') {
// Scale pixel values to a reasonable range (radius/6 to radius/3)
// This ensures points are visible and proportional to their values
const MIN_POINT_RADIUS = radius / 6;
const MAX_POINT_RADIUS = radius / 3;
if (
Number.isFinite(minRadiusValue) &&
Number.isFinite(maxRadiusValue) &&
maxRadiusValue > minRadiusValue
) {
// Normalize the value to 0-1 range, then scale to pixel range
const numericPointRadius = Number(pointRadius);
if (!Number.isFinite(numericPointRadius)) {
// fallback to minimum visible size when the value is not a finite number
pointRadius = MIN_POINT_RADIUS;
} else {
const normalizedValueRaw =
(numericPointRadius - minRadiusValue) /
(maxRadiusValue - minRadiusValue);
const normalizedValue = Math.max(
0,
Math.min(1, normalizedValueRaw),
);
pointRadius =
MIN_POINT_RADIUS +
normalizedValue * (MAX_POINT_RADIUS - MIN_POINT_RADIUS);
}
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
} else if (
Number.isFinite(minRadiusValue) &&
minRadiusValue === maxRadiusValue
) {
// All values are the same, use a fixed medium size
pointRadius = (MIN_POINT_RADIUS + MAX_POINT_RADIUS) / 2;
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
} else {
// Use raw pixel values if they're already in a reasonable range
pointRadius = Math.max(
MIN_POINT_RADIUS,
Math.min(pointRadius, MAX_POINT_RADIUS),
);
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
}
}
}
if (pointMetric !== null) {
const numericMetric = parseFloat(String(pointMetric));
pointLabel = Number.isFinite(numericMetric)
? roundDecimal(numericMetric, 2)
: String(pointMetric);
}
// Fall back to default points if pointRadius wasn't a numerical column
if (!pointRadius) {
pointRadius = defaultRadius;
}
ctx.arc(
pixelRounded[0],
pixelRounded[1],
roundDecimal(pointRadius, 1),
0,
Math.PI * 2,
);
ctx.fillStyle = `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${globalOpacity})`;
ctx.fill();
if (pointLabel !== undefined) {
drawText(ctx, pixelRounded, {
fontHeight: roundDecimal(pointRadius, 1),
label: pointLabel,
radius: pointRadius,
rgb,
shadow: false,
});
}
}
}
});
}
},
[
aggregation,
compositeOperation,
dotRadius,
drawText,
globalOpacity,
lngLatAccessor,
locations,
@@ -167,234 +388,10 @@ class ScatterPlotGlowOverlay extends PureComponent<ScatterPlotGlowOverlayProps>
renderWhileDragging,
rgb,
zoom,
} = this.props;
],
);
const radius = dotRadius ?? 4;
const clusterLabelMap: (number | string)[] = [];
locations.forEach((location, i) => {
if (location.properties.cluster) {
clusterLabelMap[i] = computeClusterLabel(
location.properties,
aggregation,
);
}
});
const filteredLabels = clusterLabelMap.filter(
v => !Number.isNaN(v),
) as number[];
// Guard against empty array or zero max to prevent NaN from division
const maxLabel =
filteredLabels.length > 0 ? Math.max(...filteredLabels) : 1;
const safeMaxLabel = maxLabel > 0 ? maxLabel : 1;
// Calculate min/max radius values for Pixels mode scaling
let minRadiusValue = Infinity;
let maxRadiusValue = -Infinity;
if (pointRadiusUnit === 'Pixels') {
locations.forEach(location => {
// Accept both null and undefined as "no value" and coerce potential numeric strings
if (
!location.properties.cluster &&
location.properties.radius != null
) {
const radiusValueRaw = location.properties.radius;
const radiusValue = Number(radiusValueRaw);
if (Number.isFinite(radiusValue)) {
minRadiusValue = Math.min(minRadiusValue, radiusValue);
maxRadiusValue = Math.max(maxRadiusValue, radiusValue);
}
}
});
}
ctx.clearRect(0, 0, width, height);
ctx.globalCompositeOperation = (compositeOperation ??
'source-over') as GlobalCompositeOperation;
if ((renderWhileDragging || !isDragging) && locations) {
locations.forEach(function _forEach(
this: ScatterPlotGlowOverlay,
location: GeoJSONLocation,
i: number,
) {
const pixel = project(lngLatAccessor!(location)) as [number, number];
const pixelRounded: [number, number] = [
roundDecimal(pixel[0], 1),
roundDecimal(pixel[1], 1),
];
if (
pixelRounded[0] + radius >= 0 &&
pixelRounded[0] - radius < width &&
pixelRounded[1] + radius >= 0 &&
pixelRounded[1] - radius < height
) {
ctx.beginPath();
if (location.properties.cluster) {
const clusterLabel = clusterLabelMap[i];
// Validate clusterLabel is a finite number before using it for radius calculation
const numericLabel = Number(clusterLabel);
const safeNumericLabel = Number.isFinite(numericLabel)
? numericLabel
: 0;
const scaledRadius = roundDecimal(
(safeNumericLabel / safeMaxLabel) ** 0.5 * radius,
1,
);
const fontHeight = roundDecimal(scaledRadius * 0.5, 1);
const [x, y] = pixelRounded;
const gradient = ctx.createRadialGradient(
x,
y,
scaledRadius,
x,
y,
0,
);
gradient.addColorStop(
1,
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${0.8 * (globalOpacity ?? 1)})`,
);
gradient.addColorStop(
0,
`rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, 0)`,
);
ctx.arc(
pixelRounded[0],
pixelRounded[1],
scaledRadius,
0,
Math.PI * 2,
);
ctx.fillStyle = gradient;
ctx.fill();
if (Number.isFinite(safeNumericLabel)) {
let label: string | number = clusterLabel;
if (safeNumericLabel >= 10000) {
label = `${Math.round(safeNumericLabel / 1000)}k`;
} else if (safeNumericLabel >= 1000) {
label = `${Math.round(safeNumericLabel / 100) / 10}k`;
}
this.drawText(ctx, pixelRounded, {
fontHeight,
label,
radius: scaledRadius,
rgb,
shadow: true,
});
}
} else {
const defaultRadius = radius / 6;
const rawRadius = location.properties.radius;
const radiusProperty =
typeof rawRadius === 'number' ? rawRadius : null;
const pointMetric = location.properties.metric ?? null;
let pointRadius: number = radiusProperty ?? defaultRadius;
let pointLabel: string | number | undefined;
if (radiusProperty != null) {
const pointLatitude = lngLatAccessor!(location)[1];
if (pointRadiusUnit === 'Kilometers') {
pointLabel = `${roundDecimal(pointRadius, 2)}km`;
pointRadius = kmToPixels(pointRadius, pointLatitude, zoom ?? 0);
} else if (pointRadiusUnit === 'Miles') {
pointLabel = `${roundDecimal(pointRadius, 2)}mi`;
pointRadius = kmToPixels(
pointRadius * MILES_PER_KM,
pointLatitude,
zoom ?? 0,
);
} else if (pointRadiusUnit === 'Pixels') {
// Scale pixel values to a reasonable range (radius/6 to radius/3)
// This ensures points are visible and proportional to their values
const MIN_POINT_RADIUS = radius / 6;
const MAX_POINT_RADIUS = radius / 3;
if (
Number.isFinite(minRadiusValue) &&
Number.isFinite(maxRadiusValue) &&
maxRadiusValue > minRadiusValue
) {
// Normalize the value to 0-1 range, then scale to pixel range
const numericPointRadius = Number(pointRadius);
if (!Number.isFinite(numericPointRadius)) {
// fallback to minimum visible size when the value is not a finite number
pointRadius = MIN_POINT_RADIUS;
} else {
const normalizedValueRaw =
(numericPointRadius - minRadiusValue) /
(maxRadiusValue - minRadiusValue);
const normalizedValue = Math.max(
0,
Math.min(1, normalizedValueRaw),
);
pointRadius =
MIN_POINT_RADIUS +
normalizedValue * (MAX_POINT_RADIUS - MIN_POINT_RADIUS);
}
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
} else if (
Number.isFinite(minRadiusValue) &&
minRadiusValue === maxRadiusValue
) {
// All values are the same, use a fixed medium size
pointRadius = (MIN_POINT_RADIUS + MAX_POINT_RADIUS) / 2;
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
} else {
// Use raw pixel values if they're already in a reasonable range
pointRadius = Math.max(
MIN_POINT_RADIUS,
Math.min(pointRadius, MAX_POINT_RADIUS),
);
pointLabel = `${roundDecimal(radiusProperty, 2)}`;
}
}
}
if (pointMetric !== null) {
const numericMetric = parseFloat(String(pointMetric));
pointLabel = Number.isFinite(numericMetric)
? roundDecimal(numericMetric, 2)
: String(pointMetric);
}
// Fall back to default points if pointRadius wasn't a numerical column
if (!pointRadius) {
pointRadius = defaultRadius;
}
ctx.arc(
pixelRounded[0],
pixelRounded[1],
roundDecimal(pointRadius, 1),
0,
Math.PI * 2,
);
ctx.fillStyle = `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${globalOpacity})`;
ctx.fill();
if (pointLabel !== undefined) {
this.drawText(ctx, pixelRounded, {
fontHeight: roundDecimal(pointRadius, 1),
label: pointLabel,
radius: pointRadius,
rgb,
shadow: false,
});
}
}
}
}, this);
}
}
render() {
return <CanvasOverlay redraw={this.redraw} />;
}
return <CanvasOverlay redraw={redraw} />;
}
export default ScatterPlotGlowOverlay;
export default memo(ScatterPlotGlowOverlay);