react-plot
Version:
Library of React components to render SVG 2D plots.
264 lines (246 loc) • 7.43 kB
text/typescript
import { euclidean } from 'ml-distance-euclidean';
import type { Scales } from './components/Axis/types.js';
import { useLegend } from './contexts/legendContext.js';
import { usePlotContext } from './contexts/plotContext.js';
import type { ScalarValue } from './types.js';
import { validateAxis } from './utils.js';
// annotation hooks
interface UsePositionConfig {
x: ScalarValue;
y: ScalarValue;
xAxis: string;
yAxis: string;
}
export function usePosition(config: UsePositionConfig) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { x, y, xAxis, yAxis } = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
return {
x: convertValue(x, plotWidth, xScale),
y: convertValue(y, plotHeight, yScale),
};
}
interface UsePointsPositionConfig {
xAxis: string;
yAxis: string;
points: Array<{ x: ScalarValue; y: ScalarValue }>;
}
export function usePointsPosition(config: UsePointsPositionConfig) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { points, xAxis, yAxis } = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
return points
.map(
(point) =>
`${convertValue(point.x, plotWidth, xScale)},${convertValue(
point.y,
plotHeight,
yScale,
)}`,
)
.join(' ');
}
interface UseRectanglePositionConfig {
xAxis: string;
yAxis: string;
x1: ScalarValue;
y1: ScalarValue;
x2: ScalarValue;
y2: ScalarValue;
}
export function useRectanglePosition(config: UseRectanglePositionConfig) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { x1, y1, x2, y2, xAxis, yAxis } = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
return {
x: convertMinValue(x1, x2, plotWidth, xScale),
y: convertMinValue(y1, y2, plotHeight, yScale),
width: convertDimensions(x1, x2, plotWidth, xScale),
height: convertDimensions(y1, y2, plotHeight, yScale),
};
}
interface UseEllipsePositionConfig {
xAxis: string;
yAxis: string;
cx: ScalarValue;
cy: ScalarValue;
rx: ScalarValue;
ry: ScalarValue;
}
export function useEllipsePosition(config: UseEllipsePositionConfig) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { cx, cy, rx, ry, xAxis, yAxis } = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
return {
cx: convertValue(cx, plotWidth, xScale),
cy: convertValue(cy, plotHeight, yScale),
rx: convertValueAbs(rx, plotWidth, xScale),
ry: convertValueAbs(ry, plotHeight, yScale),
};
}
interface UseBoxPlotPositionConfig {
xAxis: string;
yAxis: string;
min: ScalarValue;
max: ScalarValue;
q1: ScalarValue;
median: ScalarValue;
q3: ScalarValue;
width: ScalarValue;
y: ScalarValue;
}
export function useBoxPlotPosition(config: UseBoxPlotPositionConfig) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { min, max, q1, median, q3, width, y, xAxis, yAxis } = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
const horizontal = ['top', 'bottom'].includes(axisContext[xAxis]?.position);
return {
min: convertValue(min, plotWidth, xScale),
max: convertValue(max, plotWidth, xScale),
q1: convertValue(q1, plotWidth, xScale),
median: convertValue(median, plotWidth, xScale),
q3: convertValue(q3, plotWidth, xScale),
y: convertValue(y, plotHeight, yScale),
width: convertValueAbs(width, plotHeight, yScale),
horizontal,
};
}
interface UseDirectedEllipsePositionConfig {
xAxis: string;
yAxis: string;
x1: ScalarValue;
y1: ScalarValue;
x2: ScalarValue;
y2: ScalarValue;
width: ScalarValue;
}
export function useDirectedEllipsePosition(
config: UseDirectedEllipsePositionConfig,
) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const {
x1: oldX1,
y1: oldY1,
x2: oldX2,
y2: oldY2,
width,
xAxis,
yAxis,
} = config;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
const { x1, y1, x2, y2 } = {
x1: convertValue(oldX1, plotWidth, xScale),
x2: convertValue(oldX2, plotWidth, xScale),
y1: convertValue(oldY1, plotWidth, yScale),
y2: convertValue(oldY2, plotWidth, yScale),
};
const { cx, cy } = {
cx: (x1 + x2) / 2,
cy: (y1 + y2) / 2,
};
const rotation =
(y1 > y2 ? -1 : 1) *
(x1 > x2 ? -1 : 1) *
Math.asin(euclidean([x1, y1], [x1, cy]) / euclidean([x1, y1], [cx, cy]));
const { widthX, widthY } = {
widthX:
(Math.sin(rotation) * convertValueAbs(width, plotHeight, xScale)) / 2,
widthY:
(Math.cos(rotation) * convertValueAbs(width, plotHeight, yScale)) / 2,
};
return {
cx,
cy,
rx: euclidean([x1, y1], [x2, y2]) / 2,
ry: euclidean([0, 0], [widthX, widthY]),
rotation: radsToDegs(rotation),
};
}
function radsToDegs(rad: number) {
return (rad * 180) / Math.PI;
}
// convert functions
function convertString(value: string, total: number) {
return value.endsWith('%')
? (Number(value.slice(0, -1)) * total) / 100
: Number(value);
}
function convertValue(value: ScalarValue, total: number, scale?: Scales) {
if (scale === undefined) return 0;
return typeof value === 'number' ? scale(value) : convertString(value, total);
}
function convertMinValue(
value1: ScalarValue,
value2: ScalarValue,
total: number,
scale?: Scales,
) {
if (scale === undefined) return 0;
return Math.min(
typeof value2 === 'number' ? scale(value2) : convertString(value2, total),
typeof value1 === 'number' ? scale(value1) : convertString(value1, total),
);
}
function convertValueAbs(value: ScalarValue, total: number, scale?: Scales) {
if (scale === undefined) return 0;
return typeof value === 'number'
? Math.abs(scale(0) - scale(value))
: Math.abs(convertString(value, total));
}
function convertToPx(value: ScalarValue, total: number, scale?: Scales) {
if (scale === undefined) return 0;
return typeof value === 'number'
? scale(value) - scale(0)
: convertString(value, total);
}
function convertDimensions(
value1: ScalarValue,
value2: ScalarValue,
total: number,
scale?: Scales,
) {
if (scale === undefined) return 0;
return Math.abs(
(typeof value2 === 'number'
? scale(value2)
: convertString(value2, total)) -
(typeof value1 === 'number'
? scale(value1)
: convertString(value1, total)),
);
}
// other hooks
export function useIsSeriesVisible(id: string) {
const [legendState] = useLegend();
const value = legendState.labels.find((label) => label.id === id);
return value ? value.isVisible : true;
}
interface UseShiftOptions {
xAxis: string;
yAxis: string;
xShift: ScalarValue;
yShift: ScalarValue;
}
export function useShift(options: UseShiftOptions) {
const { axisContext, plotWidth, plotHeight } = usePlotContext();
const { xAxis, yAxis, xShift, yShift } = options;
const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis, {
onlyOrthogonal: true,
});
return {
xShift: convertToPx(xShift, plotWidth, xScale),
yShift: convertToPx(yShift, plotHeight, yScale),
};
}