mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-23 15:47:18 +01:00
221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import { SkPath, SkPoint, Skia } from '@shopify/react-native-skia';
|
|
|
|
import type { GraphPoint, GraphRange } from './LineGraphProps';
|
|
|
|
const PIXEL_RATIO = 2;
|
|
|
|
export interface GraphXRange {
|
|
min: Date;
|
|
max: Date;
|
|
}
|
|
|
|
export interface GraphYRange {
|
|
min: number;
|
|
max: number;
|
|
}
|
|
|
|
export interface GraphPathRange {
|
|
x: GraphXRange;
|
|
y: GraphYRange;
|
|
}
|
|
|
|
type GraphPathConfig = {
|
|
/**
|
|
* Graph Points to use for the Path. Will be normalized and centered.
|
|
*/
|
|
pointsInRange: GraphPoint[];
|
|
/**
|
|
* Optional Padding (left, right) for the Graph to correctly round the Path.
|
|
*/
|
|
horizontalPadding: number;
|
|
/**
|
|
* Optional Padding (top, bottom) for the Graph to correctly round the Path.
|
|
*/
|
|
verticalPadding: number;
|
|
/**
|
|
* Height of the Canvas (Measured with onLayout)
|
|
*/
|
|
canvasHeight: number;
|
|
/**
|
|
* Width of the Canvas (Measured with onLayout)
|
|
*/
|
|
canvasWidth: number;
|
|
/**
|
|
* Range of the graph's x and y-axis
|
|
*/
|
|
range: GraphPathRange;
|
|
};
|
|
|
|
type GraphPathConfigWithGradient = GraphPathConfig & {
|
|
shouldFillGradient: true;
|
|
};
|
|
type GraphPathConfigWithoutGradient = GraphPathConfig & {
|
|
shouldFillGradient: false;
|
|
};
|
|
|
|
export function getGraphPathRange(points: GraphPoint[], range?: GraphRange): GraphPathRange {
|
|
const minValueX = range?.x?.min ?? points[0]?.date ?? new Date();
|
|
const maxValueX = range?.x?.max ?? points[points.length - 1]?.date ?? new Date();
|
|
|
|
const minValueY =
|
|
range?.y?.min ??
|
|
points.reduce(
|
|
(prev, curr) => (curr.value < prev ? curr.value : prev),
|
|
Number.MAX_SAFE_INTEGER,
|
|
);
|
|
const maxValueY =
|
|
range?.y?.max ??
|
|
points.reduce(
|
|
(prev, curr) => (curr.value > prev ? curr.value : prev),
|
|
Number.MIN_SAFE_INTEGER,
|
|
);
|
|
|
|
return {
|
|
x: { min: minValueX, max: maxValueX },
|
|
y: { min: minValueY, max: maxValueY },
|
|
};
|
|
}
|
|
|
|
export const getXPositionInRange = (date: Date, xRange: GraphXRange): number => {
|
|
const diff = xRange.max.getTime() - xRange.min.getTime();
|
|
const x = date.getTime();
|
|
|
|
return (x - xRange.min.getTime()) / diff;
|
|
};
|
|
|
|
export const getXInRange = (width: number, date: Date, xRange: GraphXRange): number =>
|
|
Math.floor(width * getXPositionInRange(date, xRange));
|
|
|
|
export const getYPositionInRange = (value: number, yRange: GraphYRange): number => {
|
|
if (yRange.min === yRange.max) return 0.5; // Prevent division by zero (NaN)
|
|
|
|
const diff = yRange.max - yRange.min;
|
|
const y = value;
|
|
|
|
return (y - yRange.min) / diff;
|
|
};
|
|
|
|
export const getYInRange = (height: number, value: number, yRange: GraphYRange): number =>
|
|
Math.floor(height * getYPositionInRange(value, yRange));
|
|
|
|
export const getPointsInRange = (allPoints: GraphPoint[], range: GraphPathRange) =>
|
|
allPoints.filter(point => {
|
|
const portionFactorX = getXPositionInRange(point.date, range.x);
|
|
|
|
return portionFactorX <= 1 && portionFactorX >= 0;
|
|
});
|
|
|
|
type GraphPathWithGradient = { path: SkPath; gradientPath: SkPath };
|
|
|
|
function createGraphPathBase(props: GraphPathConfigWithGradient): GraphPathWithGradient;
|
|
function createGraphPathBase(props: GraphPathConfigWithoutGradient): SkPath;
|
|
|
|
function createGraphPathBase({
|
|
pointsInRange: graphData,
|
|
range,
|
|
horizontalPadding,
|
|
verticalPadding,
|
|
canvasHeight: height,
|
|
canvasWidth: width,
|
|
shouldFillGradient,
|
|
}: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): SkPath | GraphPathWithGradient {
|
|
const path = Skia.Path.Make();
|
|
|
|
// Canvas width substracted by the horizontal padding => Actual drawing width
|
|
const drawingWidth = width - 2 * horizontalPadding;
|
|
// Canvas height substracted by the vertical padding => Actual drawing height
|
|
const drawingHeight = height - 2 * verticalPadding;
|
|
|
|
if (graphData[0] == null) return path;
|
|
|
|
const points: SkPoint[] = [];
|
|
|
|
const startX = getXInRange(drawingWidth, graphData[0]!.date, range.x) + horizontalPadding;
|
|
const endX =
|
|
getXInRange(drawingWidth, graphData[graphData.length - 1]!.date, range.x) +
|
|
horizontalPadding;
|
|
|
|
const getGraphDataIndex = (pixel: number) =>
|
|
Math.round(((pixel - startX) / (endX - startX)) * (graphData.length - 1));
|
|
|
|
const getNextPixelValue = (pixel: number) => {
|
|
if (pixel === endX || pixel + PIXEL_RATIO < endX) return pixel + PIXEL_RATIO;
|
|
|
|
return endX;
|
|
};
|
|
|
|
for (let pixel = startX; startX <= pixel && pixel <= endX; pixel = getNextPixelValue(pixel)) {
|
|
const index = getGraphDataIndex(pixel);
|
|
|
|
// Draw first point only on the very first pixel
|
|
if (index === 0 && pixel !== startX) continue;
|
|
// Draw last point only on the very last pixel
|
|
|
|
if (index === graphData.length - 1 && pixel !== endX) continue;
|
|
|
|
if (index !== 0 && index !== graphData.length - 1) {
|
|
// Only draw point, when the point is exact
|
|
const exactPointX =
|
|
getXInRange(drawingWidth, graphData[index]!.date, range.x) + horizontalPadding;
|
|
|
|
const isExactPointInsidePixelRatio = Array(PIXEL_RATIO)
|
|
.fill(0)
|
|
.some((_value, additionalPixel) => pixel + additionalPixel === exactPointX);
|
|
|
|
if (!isExactPointInsidePixelRatio) continue;
|
|
}
|
|
|
|
const { value } = graphData[index]!;
|
|
const y = drawingHeight - getYInRange(drawingHeight, value, range.y) + verticalPadding;
|
|
|
|
points.push({ x: pixel, y });
|
|
}
|
|
|
|
for (let i = 0; i < points.length; i++) {
|
|
const point = points[i]!;
|
|
|
|
// first point needs to start the path
|
|
if (i === 0) path.moveTo(point.x, point.y);
|
|
|
|
const prev = points[i - 1];
|
|
const prevPrev = points[i - 2];
|
|
|
|
if (prev == null) continue;
|
|
|
|
const p0 = prevPrev ?? prev;
|
|
const p1 = prev;
|
|
const cp1x = (2 * p0.x + p1.x) / 3;
|
|
const cp1y = (2 * p0.y + p1.y) / 3;
|
|
const cp2x = (p0.x + 2 * p1.x) / 3;
|
|
const cp2y = (p0.y + 2 * p1.y) / 3;
|
|
const cp3x = (p0.x + 4 * p1.x + point.x) / 6;
|
|
const cp3y = (p0.y + 4 * p1.y + point.y) / 6;
|
|
|
|
path.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y);
|
|
|
|
if (i === points.length - 1) {
|
|
path.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y);
|
|
}
|
|
}
|
|
|
|
if (!shouldFillGradient) return path;
|
|
|
|
const gradientPath = path.copy();
|
|
|
|
gradientPath.lineTo(endX, height + verticalPadding);
|
|
gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding);
|
|
|
|
return { path, gradientPath };
|
|
}
|
|
|
|
export function createGraphPath(props: GraphPathConfig): SkPath {
|
|
return createGraphPathBase({ ...props, shouldFillGradient: false });
|
|
}
|
|
|
|
export function createGraphPathWithGradient(props: GraphPathConfig): GraphPathWithGradient {
|
|
return createGraphPathBase({
|
|
...props,
|
|
shouldFillGradient: true,
|
|
});
|
|
}
|