mirror of
https://github.com/XFox111/MuiCharts.git
synced 2026-04-22 06:51:05 +03:00
- Moved frontend to a separate folder
- Updated README.md
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { grey } from "@mui/material/colors";
|
||||
import { styled } from "@mui/system";
|
||||
import { useDrawingArea, useXScale, useYScale } from "@mui/x-charts";
|
||||
import { ScaleLinear } from "d3-scale";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
interface IProps
|
||||
{
|
||||
/** The maximum Y value. */
|
||||
maxY: number;
|
||||
/** The X ticks. */
|
||||
xTicks: number[];
|
||||
}
|
||||
|
||||
/** The Cartesian grid. */
|
||||
function CartesianGrid(props: IProps): JSX.Element
|
||||
{
|
||||
const [yTicks, setYTicks] = React.useState<number[]>([]);
|
||||
const xTicks = props.xTicks;
|
||||
|
||||
// Get the drawing area bounding box
|
||||
const { left, top, width, height } = useDrawingArea();
|
||||
|
||||
// Get the two scale
|
||||
const yAxisScale = useYScale() as ScaleLinear<unknown, number>;
|
||||
const xAxisScale = useXScale() as ScaleLinear<unknown, number>;
|
||||
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const ticks: number[] = [];
|
||||
|
||||
for (let i = 1; i <= Math.ceil(props.maxY / 20) + 1; i++)
|
||||
ticks.push(i * 20);
|
||||
|
||||
setYTicks(ticks);
|
||||
}, [props.maxY]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
{ yTicks.map((value) =>
|
||||
<StyledPath key={ value } d={ `M ${left} ${yAxisScale(value)} l ${width} 0` } />
|
||||
) }
|
||||
|
||||
{ xTicks.map((value) =>
|
||||
<StyledPath key={ value } d={ `M ${xAxisScale(value)} ${top} l 0 ${height}` } />
|
||||
) }
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledPath = styled("path")(
|
||||
() => ({
|
||||
fill: "none",
|
||||
stroke: grey[500],
|
||||
strokeWidth: 1,
|
||||
pointerEvents: "none",
|
||||
}),
|
||||
);
|
||||
|
||||
export default CartesianGrid;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { makeStyles } from "../Utils/makeStyles";
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
root:
|
||||
{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "grid",
|
||||
gridTemplateRows: "1fr auto",
|
||||
gap: theme.spacing(4),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
chart:
|
||||
{
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(6),
|
||||
paddingTop: theme.spacing(6),
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 1fr",
|
||||
gridTemplateRows: "1fr auto",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
xAxis:
|
||||
{
|
||||
gridColumn: 2,
|
||||
gridRow: 2,
|
||||
},
|
||||
grid:
|
||||
{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(10, 1fr)",
|
||||
gridTemplateRows: "repeat(10, 1fr)",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
controls:
|
||||
{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(4),
|
||||
marginBottom: theme.spacing(4),
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Box, Container, Skeleton } from "@mui/material";
|
||||
import { useStyles } from "./ChartSkeleton.styles";
|
||||
|
||||
function ChartSkeleton(): JSX.Element
|
||||
{
|
||||
const sx = useStyles();
|
||||
|
||||
return (
|
||||
<Box sx={ sx.root }>
|
||||
<Box sx={ sx.chart }>
|
||||
<Skeleton variant="rounded" width="24px" height="100%" />
|
||||
<Skeleton sx={ sx.xAxis } variant="rounded" width="100%" height="24px" />
|
||||
|
||||
<Box sx={ sx.grid }>
|
||||
{ Array.from({ length: 100 }).map((_, i) =>
|
||||
<Skeleton key={ i } variant="rounded" width="100%" height="100%" />
|
||||
) }
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Container sx={ sx.controls }>
|
||||
<Skeleton variant="rounded" width="100%" height="24px" />
|
||||
<Skeleton variant="rounded" width="150px" height="38px" />
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChartSkeleton;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { SxProps } from "@mui/system";
|
||||
import * as xc from "@mui/x-charts";
|
||||
import { useEffect, useState } from "react";
|
||||
import IChartPoint from "../Data/IChartPoint";
|
||||
import IPoint from "../Data/IPoint";
|
||||
import ITrack from "../Data/ITrack";
|
||||
import MaxSpeed from "../Data/MaxSpeed";
|
||||
import { TooltipProps, TracklineSeries } from "../Data/TrackChartDataProps";
|
||||
import CartesianGrid from "./CartesianGrid";
|
||||
import TrackLinePlot from "./TrackLinePlot";
|
||||
import TrackSurfacePlot from "./TrackSurfacePlot";
|
||||
|
||||
interface IProps
|
||||
{
|
||||
/** The tracks data. */
|
||||
tracks: ITrack[];
|
||||
/** The points data. */
|
||||
points: IPoint[];
|
||||
/** The zoom levels (start, end). */
|
||||
zoom: number[];
|
||||
/** A callback for when the processing is complete. */
|
||||
onProcessingComplete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chart of the track.
|
||||
*/
|
||||
function TrackChart({ tracks, points, zoom, ...props }: IProps): JSX.Element
|
||||
{
|
||||
const [dataset, setDataset] = useState<IChartPoint[]>([]);
|
||||
const [xTicks, setXTicks] = useState<number[]>([]);
|
||||
const [zoomedDataset, setZoomedDataset] = useState<IChartPoint[]>([]);
|
||||
const [zoomedXTicks, setZoomedXTicks] = useState<number[]>([]);
|
||||
const [maskStyles, setMasks] = useState<SxProps>({});
|
||||
const maxPointHeight: number = Math.max(...points.map(i => i.height));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
for (let i = 0; i < dataset.length; i++)
|
||||
{
|
||||
const selector: string = `.MuiMarkElement-series-${MaxSpeed[dataset[i].maxSpeed]}`;
|
||||
const element: SVGPathElement | undefined = document.querySelectorAll<SVGPathElement>(selector)[i];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
element?.style.setProperty("display", "inline");
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (tracks.length < 1 || points.length < 1)
|
||||
return;
|
||||
|
||||
const data: IChartPoint[] = [];
|
||||
let currentDistance: number = 0;
|
||||
|
||||
for (const track of tracks)
|
||||
{
|
||||
const firstPoint = points.find(p => p.id === track.firstId)!;
|
||||
|
||||
data.push({
|
||||
distance: currentDistance,
|
||||
length: track.distance,
|
||||
surface: track.surface,
|
||||
maxSpeed: track.maxSpeed,
|
||||
name: `${firstPoint.name} (ID#${firstPoint.id})`,
|
||||
height: firstPoint.height,
|
||||
});
|
||||
|
||||
currentDistance += track.distance;
|
||||
}
|
||||
|
||||
const lastPoint = points.find(p => p.id === tracks[tracks.length - 1].secondId)!;
|
||||
|
||||
data.push({
|
||||
distance: currentDistance,
|
||||
length: -1,
|
||||
surface: tracks[tracks.length - 1].surface,
|
||||
maxSpeed: tracks[tracks.length - 1].maxSpeed,
|
||||
name: `${lastPoint.name} (ID#${lastPoint.id})`,
|
||||
height: lastPoint.height
|
||||
});
|
||||
|
||||
setDataset(data);
|
||||
setXTicks(data.map(i => i.distance));
|
||||
|
||||
console.warn("Reflow!");
|
||||
|
||||
setTimeout(() => props.onProcessingComplete?.(), 500);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tracks, points, props.onProcessingComplete]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setZoomedDataset(dataset.slice(zoom[0], zoom[1]));
|
||||
setZoomedXTicks(xTicks.slice(zoom[0], zoom[1]));
|
||||
}, [dataset, xTicks, zoom]);
|
||||
|
||||
const getSx = (): SxProps => ({
|
||||
"& .MuiMarkElement-root":
|
||||
{
|
||||
display: "none",
|
||||
},
|
||||
...maskStyles,
|
||||
});
|
||||
|
||||
return (
|
||||
<xc.ResponsiveChartContainer
|
||||
dataset={ zoomedDataset }
|
||||
sx={ getSx() }
|
||||
yAxis={ [{ max: (Math.ceil(maxPointHeight / 20) + 1) * 20, min: 0 }] }
|
||||
xAxis={ [{ dataKey: "distance", scaleType: "point" }] }
|
||||
series={ TracklineSeries }>
|
||||
|
||||
<TrackSurfacePlot dataset={ zoomedDataset } />
|
||||
<CartesianGrid maxY={ maxPointHeight } xTicks={ zoomedXTicks } />
|
||||
<TrackLinePlot dataset={ zoomedDataset } onStylesUpdated={ styles => setMasks(styles) } strokeWidth={ 4 } />
|
||||
|
||||
<xc.ChartsXAxis />
|
||||
<xc.ChartsYAxis />
|
||||
<xc.ChartsAxisHighlight x="line" />
|
||||
<xc.MarkPlot />
|
||||
<xc.LineHighlightPlot />
|
||||
|
||||
<xc.ChartsTooltip
|
||||
slotProps={ {
|
||||
axisContent: TooltipProps(zoomedDataset)
|
||||
} } />
|
||||
|
||||
</xc.ResponsiveChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackChart;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { SxProps } from "@mui/system";
|
||||
import { LinePlot, useDrawingArea, useXScale } from "@mui/x-charts";
|
||||
import { ScaleLinear } from "d3-scale";
|
||||
import { useEffect } from "react";
|
||||
import MaxSpeed from "../Data/MaxSpeed";
|
||||
import IChartPoint from "../Data/IChartPoint";
|
||||
|
||||
// Remarks:
|
||||
// Even though this component adds nothing to the chart and only calculates masks,
|
||||
// it is still required since a) I can't use `useDrawingArea` inside `TrackChart`
|
||||
// and b) I can't pass calculates styles here
|
||||
|
||||
interface IProps
|
||||
{
|
||||
/** Data to plot. */
|
||||
dataset: IChartPoint[];
|
||||
/** Callback to update the styles. */
|
||||
onStylesUpdated: (styles: SxProps) => void;
|
||||
/** The width of the line. */
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
/** A plot of the track line. */
|
||||
function TrackLinePlot({ dataset, onStylesUpdated, strokeWidth }: IProps): JSX.Element
|
||||
{
|
||||
const area = useDrawingArea();
|
||||
const xAxisScale = useXScale() as ScaleLinear<unknown, number>;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const masks: string[] = [];
|
||||
|
||||
for (let i = 0; i < Object.keys(MaxSpeed).length / 2; i++)
|
||||
masks.push("M 0 0 ");
|
||||
|
||||
for (const point of dataset)
|
||||
{
|
||||
if (point.length < 1)
|
||||
continue;
|
||||
|
||||
const xStart: number = xAxisScale(point.distance) - area.left;
|
||||
const xEnd: number = xAxisScale(point.distance + point.length) - area.left;
|
||||
|
||||
masks[point.maxSpeed] += `H ${xStart} V ${area.height} H ${isNaN(xEnd) ? area.width : xEnd} V 0 `;
|
||||
}
|
||||
|
||||
let sx: SxProps = {};
|
||||
|
||||
for (let i = 0; i < masks.length; i++)
|
||||
sx = {
|
||||
...sx,
|
||||
[`& .MuiLineElement-series-${MaxSpeed[i]}`]:
|
||||
{
|
||||
clipPath: `path("${masks[i]} Z")`,
|
||||
strokeWidth: strokeWidth ?? 2,
|
||||
}
|
||||
};
|
||||
|
||||
onStylesUpdated(sx);
|
||||
|
||||
// Suppressing warning, since adding useXScale to the dependency array would cause an infinite component reflow
|
||||
// `area` dependency is sufficient.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [area, dataset, strokeWidth]);
|
||||
|
||||
return (
|
||||
<LinePlot />
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackLinePlot;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { styled } from "@mui/material";
|
||||
import { useDrawingArea, useXScale } from "@mui/x-charts";
|
||||
import { ScaleLinear } from "d3-scale";
|
||||
import React from "react";
|
||||
import IChartPoint from "../Data/IChartPoint";
|
||||
import { SurfaceColors } from "../Data/TrackChartDataProps";
|
||||
|
||||
interface IProps
|
||||
{
|
||||
/** Data to plot. */
|
||||
dataset: IChartPoint[];
|
||||
}
|
||||
|
||||
/** A plot of the track surface. */
|
||||
function TrackSurfacePlot({ dataset }: IProps): JSX.Element
|
||||
{
|
||||
// Get the drawing area bounding box
|
||||
const { top, height } = useDrawingArea();
|
||||
const xAxisScale = useXScale() as ScaleLinear<unknown, number>;
|
||||
|
||||
// Calculate the width of each track
|
||||
const getWidth = (item: IChartPoint): number =>
|
||||
{
|
||||
const width = xAxisScale(item.distance + item.length) - xAxisScale(item.distance);
|
||||
return isNaN(width) ? 0 : width;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ dataset.map((i, index) => i.length > 0 &&
|
||||
<StyledRect key={ index }
|
||||
x={ xAxisScale(i.distance) } y={ top }
|
||||
width={ getWidth(i) } height={ height }
|
||||
color={ SurfaceColors[i.surface] } />
|
||||
) }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledRect = styled("rect")<{ color: string; }>(
|
||||
({ color }) => ({
|
||||
fill: color,
|
||||
pointerEvents: "none"
|
||||
})
|
||||
);
|
||||
|
||||
export default TrackSurfacePlot;
|
||||
Reference in New Issue
Block a user