mirror of
https://github.com/zoriya/flood.git
synced 2026-06-05 03:39:24 +00:00
TransferRateGraph: migrate to FC
This commit is contained in:
@@ -1,32 +1,17 @@
|
||||
import {FC, useState, useRef, useEffect} from 'react';
|
||||
import {FC, useState, useRef} from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import {observer} from 'mobx-react';
|
||||
import {reaction} from 'mobx';
|
||||
|
||||
import ClientStatusStore from '../../stores/ClientStatusStore';
|
||||
import TransferDataStore from '../../stores/TransferDataStore';
|
||||
import TransferRateDetails from './TransferRateDetails';
|
||||
import TransferRateGraph from './TransferRateGraph';
|
||||
|
||||
import type {TransferRateGraphInspectorPoint} from './TransferRateGraph';
|
||||
import type {TransferRateGraphEventHandlers, TransferRateGraphInspectorPoint} from './TransferRateGraph';
|
||||
|
||||
const TransferData: FC = observer(() => {
|
||||
const [graphInspectorPoint, setGraphInspectorPoint] = useState<TransferRateGraphInspectorPoint | null>(null);
|
||||
const [sidebarWidth, setSidebarWidth] = useState<number>(0);
|
||||
const rateGraphRef = useRef<TransferRateGraph>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const dispose = reaction(
|
||||
() => TransferDataStore.transferRates,
|
||||
() => {
|
||||
if (rateGraphRef.current != null) {
|
||||
rateGraphRef.current.handleTransferHistoryChange();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return dispose;
|
||||
}, []);
|
||||
const rateGraphHandlerRefs = useRef<TransferRateGraphEventHandlers>(null);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
@@ -42,13 +27,13 @@ const TransferData: FC = observer(() => {
|
||||
className="client-stats"
|
||||
onMouseMove={(event) => {
|
||||
if (event?.nativeEvent?.clientX != null) {
|
||||
rateGraphRef.current?.handleMouseMove(event.nativeEvent.clientX);
|
||||
rateGraphHandlerRefs.current?.handleMouseMove(event.nativeEvent.clientX);
|
||||
}
|
||||
}}
|
||||
onMouseOver={() => rateGraphRef.current?.handleMouseOver()}
|
||||
onMouseOut={() => rateGraphRef.current?.handleMouseOut()}
|
||||
onFocus={() => rateGraphRef.current?.handleMouseOver()}
|
||||
onBlur={() => rateGraphRef.current?.handleMouseOut()}>
|
||||
onMouseOver={() => rateGraphHandlerRefs.current?.handleMouseOver()}
|
||||
onMouseOut={() => rateGraphHandlerRefs.current?.handleMouseOut()}
|
||||
onFocus={() => rateGraphHandlerRefs.current?.handleMouseOver()}
|
||||
onBlur={() => rateGraphHandlerRefs.current?.handleMouseOut()}>
|
||||
<TransferRateDetails inspectorPoint={graphInspectorPoint} />
|
||||
{ClientStatusStore.isConnected && (
|
||||
<TransferRateGraph
|
||||
@@ -60,7 +45,7 @@ const TransferData: FC = observer(() => {
|
||||
onHover={(inspectorPoint) => {
|
||||
setGraphInspectorPoint(inspectorPoint);
|
||||
}}
|
||||
ref={rateGraphRef}
|
||||
handlerRefs={rateGraphHandlerRefs}
|
||||
width={sidebarWidth}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ const icons = {
|
||||
};
|
||||
|
||||
interface TransferRateDetailsProps {
|
||||
inspectorPoint: TransferRateGraphInspectorPoint | null;
|
||||
inspectorPoint?: TransferRateGraphInspectorPoint | null;
|
||||
}
|
||||
|
||||
const TransferRateDetails: FC<TransferRateDetailsProps> = observer(({inspectorPoint}: TransferRateDetailsProps) => {
|
||||
@@ -49,8 +49,8 @@ const TransferRateDetails: FC<TransferRateDetailsProps> = observer(({inspectorPo
|
||||
|
||||
if (inspectorPoint != null) {
|
||||
transferRates = {
|
||||
upload: inspectorPoint.uploadSpeed,
|
||||
download: inspectorPoint.downloadSpeed,
|
||||
upload: inspectorPoint.upload,
|
||||
download: inspectorPoint.download,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,13 +63,10 @@ const TransferRateDetails: FC<TransferRateDetailsProps> = observer(({inspectorPo
|
||||
});
|
||||
|
||||
let timestamp = null;
|
||||
if (inspectorPoint?.nearestTimestamp != null) {
|
||||
if (inspectorPoint?.timestamp != null) {
|
||||
timestamp = (
|
||||
<div className={timestampClasses}>
|
||||
<Duration
|
||||
suffix={i18n._('general.ago')}
|
||||
value={Math.trunc((Date.now() - inspectorPoint.nearestTimestamp) / 1000)}
|
||||
/>
|
||||
<Duration suffix={i18n._('general.ago')} value={Math.trunc((Date.now() - inspectorPoint.timestamp) / 1000)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {area, curveMonotoneX, line} from 'd3-shape';
|
||||
import {Component, FC, ReactNode} from 'react';
|
||||
import {FC, MutableRefObject, useRef, useState} from 'react';
|
||||
import {max} from 'd3-array';
|
||||
import {ScaleLinear, scaleLinear} from 'd3-scale';
|
||||
import {Selection, select} from 'd3-selection';
|
||||
import {observer} from 'mobx-react';
|
||||
import {scaleLinear} from 'd3-scale';
|
||||
|
||||
import type {TransferDirection} from '@shared/types/TransferData';
|
||||
import type {TransferDirection, TransferHistory} from '@shared/types/TransferData';
|
||||
|
||||
import TransferDataStore, {TRANSFER_DIRECTIONS} from '../../stores/TransferDataStore';
|
||||
|
||||
@@ -15,244 +15,118 @@ const TransferRateGraphGradient: FC<{direction: TransferDirection}> = ({directio
|
||||
</linearGradient>
|
||||
);
|
||||
|
||||
export interface TransferRateGraphEventHandlers {
|
||||
handleMouseMove: (mouseX: number) => void;
|
||||
handleMouseOut: () => void;
|
||||
handleMouseOver: () => void;
|
||||
}
|
||||
|
||||
export interface TransferRateGraphInspectorPoint {
|
||||
uploadSpeed: number;
|
||||
downloadSpeed: number;
|
||||
nearestTimestamp: number;
|
||||
upload: number;
|
||||
download: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface TransferRateGraphProps {
|
||||
id: string;
|
||||
height: number;
|
||||
width: number;
|
||||
handlerRefs: MutableRefObject<TransferRateGraphEventHandlers | null>;
|
||||
onHover: (inspectorPoint: TransferRateGraphInspectorPoint) => void;
|
||||
onMouseOut: () => void;
|
||||
}
|
||||
|
||||
class TransferRateGraph extends Component<TransferRateGraphProps> {
|
||||
lastMouseX?: number;
|
||||
xScale?: ScaleLinear<number, number>;
|
||||
yScale?: ScaleLinear<number, number>;
|
||||
graphRefs: {
|
||||
graph: SVGSVGElement | null;
|
||||
areDefined: boolean;
|
||||
isHovered: boolean;
|
||||
} & Record<
|
||||
TransferDirection,
|
||||
{
|
||||
graphArea?: Selection<SVGPathElement, unknown, HTMLElement, unknown>;
|
||||
inspectPoint?: Selection<SVGCircleElement, unknown, HTMLElement, unknown>;
|
||||
rateLine?: Selection<SVGPathElement, unknown, HTMLElement, unknown>;
|
||||
}
|
||||
> = {
|
||||
graph: null,
|
||||
areDefined: false,
|
||||
isHovered: false,
|
||||
download: {},
|
||||
upload: {},
|
||||
};
|
||||
const getSpeedAtHoverPoint = (history: TransferHistory, direction: TransferDirection, hoverPoint: number) => {
|
||||
const upperSpeed = history[direction][Math.ceil(hoverPoint)];
|
||||
const lowerSpeed = history[direction][Math.floor(hoverPoint)];
|
||||
return lowerSpeed + (upperSpeed - lowerSpeed) * (hoverPoint % 1);
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
width: 240,
|
||||
};
|
||||
const TransferRateGraph: FC<TransferRateGraphProps> = observer(
|
||||
({id, height, width, handlerRefs, onHover, onMouseOut}: TransferRateGraphProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
|
||||
componentDidMount(): void {
|
||||
this.renderGraphData();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.renderGraphData();
|
||||
}
|
||||
|
||||
private setInspectorCoordinates(slug: TransferDirection, hoverPoint: number): number {
|
||||
const {
|
||||
graphRefs: {
|
||||
[slug]: {inspectPoint},
|
||||
},
|
||||
xScale,
|
||||
yScale,
|
||||
} = this;
|
||||
|
||||
if (xScale == null || yScale == null || inspectPoint == null) {
|
||||
return 0;
|
||||
}
|
||||
const hoverPoint = useRef<number>(0);
|
||||
const inspectorPoint = useRef<TransferRateGraphInspectorPoint>({download: 0, upload: 0, timestamp: 0});
|
||||
|
||||
const historicalData = TransferDataStore.transferRates;
|
||||
const upperSpeed = historicalData[slug][Math.ceil(hoverPoint)];
|
||||
const lowerSpeed = historicalData[slug][Math.floor(hoverPoint)];
|
||||
|
||||
const delta = upperSpeed - lowerSpeed;
|
||||
const speedAtHoverPoint = lowerSpeed + delta * (hoverPoint % 1);
|
||||
|
||||
const coordinates = {x: xScale(hoverPoint), y: yScale(speedAtHoverPoint)};
|
||||
|
||||
inspectPoint.attr('transform', `translate(${coordinates.x},${coordinates.y})`);
|
||||
|
||||
return speedAtHoverPoint;
|
||||
}
|
||||
|
||||
handleMouseMove = (mouseX: number): void => {
|
||||
this.lastMouseX = mouseX;
|
||||
this.renderPrecisePointInspectors();
|
||||
};
|
||||
|
||||
handleMouseOut = (): void => {
|
||||
const {graphRefs, props} = this;
|
||||
|
||||
graphRefs.isHovered = false;
|
||||
|
||||
TRANSFER_DIRECTIONS.forEach(<T extends TransferDirection>(direction: T) => {
|
||||
const {inspectPoint} = graphRefs[direction];
|
||||
if (inspectPoint != null) {
|
||||
graphRefs[direction].inspectPoint = inspectPoint.style('opacity', 0);
|
||||
}
|
||||
});
|
||||
|
||||
if (props.onMouseOut) {
|
||||
props.onMouseOut();
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseOver = (): void => {
|
||||
this.graphRefs.isHovered = true;
|
||||
|
||||
TRANSFER_DIRECTIONS.forEach(<T extends TransferDirection>(direction: T) => {
|
||||
const {inspectPoint} = this.graphRefs[direction];
|
||||
if (inspectPoint != null) {
|
||||
this.graphRefs[direction].inspectPoint = inspectPoint.style('opacity', 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleTransferHistoryChange = (): void => {
|
||||
this.updateGraph();
|
||||
};
|
||||
|
||||
private initGraph(): void {
|
||||
if (this.graphRefs.areDefined === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {id} = this.props;
|
||||
|
||||
const graph = select(`#${id}`);
|
||||
TRANSFER_DIRECTIONS.forEach(<T extends TransferDirection>(direction: T) => {
|
||||
// appendEmptyGraphShapes
|
||||
this.graphRefs[direction].graphArea = graph
|
||||
.append('path')
|
||||
.attr('class', 'graph__area')
|
||||
.attr('fill', `url('#graph__gradient--${direction}')`);
|
||||
|
||||
// appendEmptyGraphLines
|
||||
this.graphRefs[direction].rateLine = graph.append('path').attr('class', `graph__line graph__line--${direction}`);
|
||||
|
||||
// appendGraphCircles
|
||||
this.graphRefs[direction].inspectPoint = graph
|
||||
.append('circle')
|
||||
.attr('class', `graph__circle graph__circle--${direction}`)
|
||||
.attr('r', 2.5);
|
||||
});
|
||||
|
||||
this.graphRefs.areDefined = true;
|
||||
}
|
||||
|
||||
private updateGraph() {
|
||||
this.renderGraphData();
|
||||
|
||||
if (this.graphRefs.isHovered) {
|
||||
this.renderPrecisePointInspectors();
|
||||
}
|
||||
}
|
||||
|
||||
private renderPrecisePointInspectors(): void {
|
||||
const {
|
||||
lastMouseX,
|
||||
props: {onHover},
|
||||
xScale,
|
||||
} = this;
|
||||
|
||||
if (xScale == null || lastMouseX == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const historicalData = TransferDataStore.transferRates;
|
||||
const hoverPoint = xScale.invert(lastMouseX);
|
||||
const uploadSpeed = this.setInspectorCoordinates('upload', hoverPoint);
|
||||
const downloadSpeed = this.setInspectorCoordinates('download', hoverPoint);
|
||||
const nearestTimestamp = historicalData.timestamps[Math.round(hoverPoint)];
|
||||
|
||||
if (onHover) {
|
||||
onHover({
|
||||
uploadSpeed,
|
||||
downloadSpeed,
|
||||
nearestTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderGraphData(): void {
|
||||
const historicalData = TransferDataStore.transferRates;
|
||||
const {height, width} = this.props;
|
||||
const margin = {bottom: 10, top: 10};
|
||||
|
||||
this.xScale = scaleLinear()
|
||||
const xScale = scaleLinear()
|
||||
.domain([0, historicalData.download.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
this.yScale = scaleLinear()
|
||||
const yScale = scaleLinear()
|
||||
.domain([
|
||||
0,
|
||||
max(historicalData.download, (dataPoint, index) => Math.max(dataPoint, historicalData.upload[index])) as number,
|
||||
])
|
||||
.range([height - margin.top, margin.bottom]);
|
||||
.range([height - 10, 10]);
|
||||
|
||||
this.initGraph();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
handlerRefs.current = {
|
||||
handleMouseMove: (mouseX: number) => {
|
||||
hoverPoint.current = xScale.invert(mouseX);
|
||||
onHover(inspectorPoint.current);
|
||||
},
|
||||
handleMouseOut: () => {
|
||||
setIsHovered(false);
|
||||
onMouseOut();
|
||||
},
|
||||
handleMouseOver: () => {
|
||||
setIsHovered(true);
|
||||
},
|
||||
};
|
||||
|
||||
if (isHovered) {
|
||||
inspectorPoint.current = {
|
||||
download: getSpeedAtHoverPoint(historicalData, 'download', hoverPoint.current),
|
||||
upload: getSpeedAtHoverPoint(historicalData, 'upload', hoverPoint.current),
|
||||
timestamp: historicalData.timestamps[Math.round(hoverPoint.current)],
|
||||
};
|
||||
}
|
||||
|
||||
const interpolation = curveMonotoneX;
|
||||
TRANSFER_DIRECTIONS.forEach(<T extends TransferDirection>(direction: T) => {
|
||||
const {xScale, yScale} = this;
|
||||
const {graphArea, rateLine} = this.graphRefs[direction];
|
||||
|
||||
if (rateLine == null || graphArea == null || xScale == null || yScale == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.graphRefs[direction].rateLine = rateLine.attr(
|
||||
'd',
|
||||
line<number>()
|
||||
.x((_dataPoint, index) => xScale(index) || 0)
|
||||
.y((dataPoint) => yScale(dataPoint) || 0)
|
||||
.curve(interpolation)(historicalData[direction]) as string,
|
||||
);
|
||||
|
||||
this.graphRefs[direction].graphArea = graphArea.attr(
|
||||
'd',
|
||||
area<number>()
|
||||
.x((_dataPoint, index) => xScale(index) || 0)
|
||||
.y0(height)
|
||||
.y1((dataPoint) => yScale(dataPoint) || 0)
|
||||
.curve(interpolation)(historicalData[direction]) as string,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const {id} = this.props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="graph"
|
||||
id={id}
|
||||
ref={(ref) => {
|
||||
this.graphRefs.graph = ref;
|
||||
}}>
|
||||
<svg className="graph" id={id}>
|
||||
<defs>
|
||||
<TransferRateGraphGradient direction="upload" />
|
||||
<TransferRateGraphGradient direction="download" />
|
||||
</defs>
|
||||
{TRANSFER_DIRECTIONS.map((direction) => [
|
||||
<path
|
||||
className="graph__area"
|
||||
key={`area-${direction}`}
|
||||
fill={`url('#graph__gradient--${direction}')`}
|
||||
d={
|
||||
area<number>()
|
||||
.x((_dataPoint, index) => xScale(index) || 0)
|
||||
.y0(height)
|
||||
.y1((dataPoint) => yScale(dataPoint) || 0)
|
||||
.curve(interpolation)(historicalData[direction]) as string
|
||||
}
|
||||
/>,
|
||||
<path
|
||||
className={`graph__line graph__line--${direction}`}
|
||||
key={`line-${direction}`}
|
||||
d={
|
||||
line<number>()
|
||||
.x((_dataPoint, index) => xScale(index) || 0)
|
||||
.y((dataPoint) => yScale(dataPoint) || 0)
|
||||
.curve(interpolation)(historicalData[direction]) as string
|
||||
}
|
||||
/>,
|
||||
<circle
|
||||
className={`graph__circle graph__circle--${direction}`}
|
||||
key={`point-${direction}`}
|
||||
r={2.5}
|
||||
style={{opacity: isHovered ? 1 : 0}}
|
||||
transform={`translate(${xScale(hoverPoint.current)},${yScale(inspectorPoint.current[direction])})`}
|
||||
/>,
|
||||
])}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default TransferRateGraph;
|
||||
|
||||
Reference in New Issue
Block a user