Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: network graph #357

Merged
merged 37 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8e50488
simplified jsx
DimaDemchenko Mar 28, 2024
761ee3a
chart draft with 1 value
DimaDemchenko Mar 29, 2024
9b5d068
chart draft
DimaDemchenko Mar 29, 2024
bb7047c
draft stacked area chart
DimaDemchenko Mar 29, 2024
7ead649
draft of chart with fake data
DimaDemchenko Mar 31, 2024
4b0d93c
implemented chart with fake data
DimaDemchenko Mar 31, 2024
17f6d95
chart with real stat
DimaDemchenko Apr 1, 2024
e24cb93
added legend with stored data
DimaDemchenko Apr 1, 2024
6ad1412
colors moved to constants
DimaDemchenko Apr 1, 2024
d408ab8
added percentage calculation
DimaDemchenko Apr 1, 2024
5cf0c2e
Chart legend moved to separate component
DimaDemchenko Apr 1, 2024
6541a98
implemented chart legends
DimaDemchenko Apr 1, 2024
5e54f1d
refactored naming
DimaDemchenko Apr 2, 2024
e3c9b98
resolved all TS-type-errors
DimaDemchenko Apr 2, 2024
37f27d7
spacing for readability
DimaDemchenko Apr 2, 2024
216532b
renamed chart component
DimaDemchenko Apr 2, 2024
5dd52c8
fixed margin of svg container
DimaDemchenko Apr 2, 2024
5f77b15
d3 types moved to dev deps
DimaDemchenko Apr 2, 2024
1ea67fa
improvements
DimaDemchenko Apr 2, 2024
ffed8df
mbit/s
DimaDemchenko Apr 2, 2024
d40df0b
removed unused deps
DimaDemchenko Apr 3, 2024
3813fed
button to create new peer
DimaDemchenko Apr 3, 2024
0c481f8
draft of graph network
DimaDemchenko Apr 3, 2024
99d717f
draft of graph
DimaDemchenko Apr 4, 2024
1854a21
GraphNetwork with fake data
DimaDemchenko Apr 5, 2024
1d5065d
NetworkGraph with real data
DimaDemchenko Apr 5, 2024
135e76c
types
DimaDemchenko Apr 5, 2024
8fff2d4
component moved to folder
DimaDemchenko Apr 5, 2024
dd0b5fc
removed custom data generation
DimaDemchenko Apr 5, 2024
c04934b
changed transition time on exit
DimaDemchenko Apr 5, 2024
f060725
pnpm-lock
DimaDemchenko Apr 5, 2024
df1bb26
Merge branch 'v1' into refactor/network-graph
DimaDemchenko Apr 5, 2024
5c55980
Merge branch 'v1' into refactor/network-graph
DimaDemchenko Apr 5, 2024
2b58fbc
pnpm
DimaDemchenko Apr 5, 2024
30d4cf9
improvements
DimaDemchenko Apr 5, 2024
e2a82bb
type improvements
DimaDemchenko Apr 8, 2024
fd38f8c
changed link type
DimaDemchenko Apr 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/p2p-media-loader-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
"d3": "^7.9.0",
"hls.js": "^1.5.7",
"p2p-media-loader-core": "workspace:*",
"p2p-media-loader-hlsjs": "workspace:*",
"vis-network": "^9.1.9"
"p2p-media-loader-hlsjs": "workspace:*"
},
"devDependencies": {
"@types/d3": "^7.4.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/p2p-media-loader-demo/src/components/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { PlaybackOptions } from "./PlaybackOptions";
import { PLAYERS } from "../constants";
import { useQueryParams } from "../hooks/useQueryParams";
import { HlsjsPlayer } from "./players/Hlsjs";
import { GraphNetwork } from "./GraphNetwork";
import { useCallback, useRef, useState } from "react";
import { DownloadStatsChart } from "./chart/DownloadStatsChart";
import "./demo.css";
import { NodeNetwork } from "./nodeNetwork/NodeNetwork";

declare global {
interface Window {
Expand Down Expand Up @@ -100,7 +100,7 @@ export const Demo = () => {
streamUrl={queryParams.streamUrl}
/>
</div>
<GraphNetwork peers={peers} />
<NodeNetwork peers={peers} />
<DownloadStatsChart downloadStatsRef={data} />
</>
);
Expand Down
47 changes: 0 additions & 47 deletions packages/p2p-media-loader-demo/src/components/GraphNetwork.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export const PlaybackOptions = ({
></input>
</div>
<button onClick={handleApply}>Apply</button>
<button
onClick={() => {
window.open(window.location.href, "_blank");
}}
>
Create new peer
</button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect, useRef } from "react";
import * as d3 from "d3";
import {
Link,
updateGraph,
Node,
prepareGroups,
createSimulation,
} from "./network";

type GraphNetworkProps = {
peers: string[];
};

const DEFAULT_PEER_ID = "You";
const DEFAULT_NODE: Node = { id: DEFAULT_PEER_ID, isMain: true };
const DEFAULT_GRAPH_DATA = {
nodes: [DEFAULT_NODE],
links: [] as Link[],
};

export const NodeNetwork = ({ peers }: GraphNetworkProps) => {
const svgRef = useRef<SVGSVGElement>(null);
const networkDataRef = useRef(DEFAULT_GRAPH_DATA);
const simulationRef = useRef<d3.Simulation<Node, Link> | null>(null);

useEffect(() => {
if (!svgRef.current) return;

const width = svgRef.current.clientWidth;
const height = svgRef.current.clientHeight;
const simulation = createSimulation(width, height);

simulationRef.current = simulation;

prepareGroups(svgRef.current);

return () => {
simulation.stop();
};
}, []);

useEffect(() => {
const allNodes = [
...peers.map((peerId) => ({ id: peerId, isMain: false })),
DEFAULT_NODE,
];

const allLinks = peers.map((peerId) => ({
source: DEFAULT_NODE,
target: allNodes.find((n) => n.id === peerId)!,
linkId: `${DEFAULT_PEER_ID}-${peerId}`,
}));

const networkData = networkDataRef.current;

const nodesToAdd = allNodes.filter(
(an) => !networkData.nodes.find((n) => n.id === an.id),
);
const nodesToRemove = networkData.nodes.filter(
(n) => !allNodes.find((an) => an.id === n.id),
);
const linksToAdd = allLinks.filter(
(al) => !networkData.links.find((l) => l.linkId === al.linkId),
);
const linksToRemove = networkData.links.filter(
(l) => !allLinks.find((al) => al.linkId === l.linkId),
);

const updatedNodes = networkData.nodes.filter(
(n) => !nodesToRemove.find((rn) => rn.id === n.id),
);
const updatedLinks = networkData.links.filter(
(l) => !linksToRemove.find((rl) => rl.linkId === l.linkId),
);

const newNetworkData = {
nodes: [...updatedNodes, ...nodesToAdd],
links: [...updatedLinks, ...linksToAdd],
};

networkDataRef.current = newNetworkData;

updateGraph(
newNetworkData.nodes,
newNetworkData.links,
simulationRef.current,
svgRef,
);
}, [peers]);

return (
<>
<svg
ref={svgRef}
width="380"
height="400"
style={{ border: "1px solid black" }}
></svg>
</>
);
};
196 changes: 196 additions & 0 deletions packages/p2p-media-loader-demo/src/components/nodeNetwork/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import * as d3 from "d3";

export interface Node extends d3.SimulationNodeDatum {
id: string;
isMain?: boolean;
group?: number;
}

export interface Link extends d3.SimulationLinkDatum<Node> {
source: Node;
target: Node;
linkId: string;
}

const COLORS = {
links: "#C8C8C8",
nodeHover: "#A9A9A9",
node: (d: { isMain?: boolean }) => {
return d.isMain ? "hsl(210, 70%, 72.5%)" : "hsl(55, 70%, 72.5%)";
},
};

function handleNodeMouseOver(this: SVGCircleElement) {
d3.select(this).style("fill", COLORS.nodeHover);
}

function handleNodeMouseOut(this: SVGCircleElement, _event: unknown, d: Node) {
d3.select(this).style("fill", COLORS.node(d));
}

function getLinkText(d: Link) {
return `${d.source.id}-${d.target.id}`;
}

function getNodeId(d: Node) {
return d.id;
}

function removeD3Item(this: d3.BaseType) {
d3.select(this).remove();
}

export const updateGraph = (
newNodes: Node[],
newLinks: Link[],
simulation: d3.Simulation<Node, Link> | null,
svgRef: React.MutableRefObject<SVGSVGElement | null>,
) => {
if (!simulation || !svgRef.current) return;

simulation.nodes(newNodes);
simulation.force<d3.ForceLink<Node, Link>>("link")?.links(newLinks);
simulation.alpha(0.5).restart();

const link = d3
.select(svgRef.current)
.select(".links")
.selectAll<SVGLineElement, Link>("line")
.data(newLinks, getLinkText);

link
.enter()
.append("line")
.merge(link)
.attr("stroke", COLORS.links)
.transition()
.duration(500)
.attr("stroke-width", 0.5);

link
.exit()
.transition()
.duration(200)
.style("opacity", 0)
.on("end", removeD3Item);

const node = d3
.select(svgRef.current)
.select(".nodes")
.selectAll<SVGCircleElement, Node>("circle")
.data(newNodes, getNodeId);

node
.enter()
.append("circle")
.merge(node)
.attr("r", (d) => (d.isMain ? 15 : 13))
.attr("fill", (d) => COLORS.node(d))
.on("mouseover", handleNodeMouseOver)
.on("mouseout", handleNodeMouseOut)
.call(drag(simulation));

node.exit().transition().duration(200).attr("r", 0).remove();

const text = d3
.select(svgRef.current)
.select(".nodes")
.selectAll<SVGTextElement, Node>("text")
.data(newNodes, getNodeId);

text
.enter()
.append("text")
.style("fill-opacity", 0)
.merge(text)
.text(getNodeId)
.style("text-anchor", "middle")
.style("font-size", "12px")
.style("font-family", "sans-serif")
.transition()
.duration(500)
.style("fill-opacity", 1);

text
.exit()
.transition()
.duration(200)
.style("fill-opacity", 0)
.on("end", removeD3Item);

simulation.on("tick", () => {
d3.select(svgRef.current)
.select(".links")
.selectAll<SVGLineElement, Link>("line")
.attr("x1", (d) => d.source.x ?? 0)
.attr("y1", (d) => d.source.y ?? 0)
.attr("x2", (d) => d.target.x ?? 0)
.attr("y2", (d) => d.target.y ?? 0);

d3.select(svgRef.current)
.select(".nodes")
.selectAll<SVGCircleElement, Node>("circle")
.attr("cx", (d) => d.x ?? 0)
.attr("cy", (d) => d.y ?? 0);

d3.select(svgRef.current)
.select(".nodes")
.selectAll<SVGTextElement, Node>("text")
.attr("x", (d) => d.x ?? 0)
.attr("y", (d) => (d.y === undefined ? 0 : d.y - 20));
});
};

const drag = (simulation: d3.Simulation<Node, Link>) => {
const dragStarted = (
event: d3.D3DragEvent<SVGCircleElement, Node, Node>,
d: Node,
) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};

const dragged = (
event: d3.D3DragEvent<SVGCircleElement, Node, Node>,
d: Node,
) => {
d.fx = event.x;
d.fy = event.y;
};

const dragEnded = (
event: d3.D3DragEvent<SVGCircleElement, Node, Node>,
d: Node,
) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};

return d3
.drag<SVGCircleElement, Node>()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded);
};

export const prepareGroups = (svg: SVGElement) => {
d3.select(svg).append("g").attr("class", "links");
d3.select(svg).append("g").attr("class", "nodes");
};

export const createSimulation = (width: number, height: number) => {
return d3
.forceSimulation<Node, Link>()
.force("link", d3.forceLink<Node, Link>().id(getNodeId).distance(110))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force(
"collide",
d3
.forceCollide<Node>()
.radius((d) => (d.isMain ? 20 : 15))
.iterations(2),
);
};
Loading