This commit is contained in:
Michael Zhang 2024-06-27 19:03:02 -05:00
commit c79fb7de78
11 changed files with 229 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

17
biome.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentWidth": 2,
"indentStyle": "space"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

BIN
bun.lockb Executable file

Binary file not shown.

2
index.html Normal file
View file

@ -0,0 +1,2 @@
<script type="module" src="./src/index.tsx"></script>
<div id="root"></div>

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"dependencies": {
"d3": "^7.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@types/d3": "^7.4.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"sass": "^1.77.6",
"vite": "^5.3.2"
}
}

21
src/App.module.scss Normal file
View file

@ -0,0 +1,21 @@
.container {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
circle {
fill: cadetblue;
}
line {
stroke: #ccc;
}
text {
text-anchor: middle;
font-family: "Helvetica Neue", Helvetica, sans-serif;
fill: #666;
font-size: 16px;
}

148
src/App.tsx Normal file
View file

@ -0,0 +1,148 @@
import * as d3 from "d3";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import styles from "./App.module.scss";
import { createPortal } from "react-dom";
interface NodeInfo extends d3.SimulationNodeDatum {
name: string;
}
const nodes: NodeInfo[] = [
{ name: "A" },
{ name: "B" },
{ name: "C" },
{ name: "D" },
{ name: "E" },
{ name: "F" },
{ name: "G" },
{ name: "H" },
];
const links = [
{ source: 0, target: 1 },
{ source: 0, target: 2 },
{ source: 0, target: 3 },
{ source: 1, target: 6 },
{ source: 3, target: 4 },
{ source: 3, target: 7 },
{ source: 4, target: 5 },
{ source: 4, target: 7 },
];
export default function App() {
const svgRef = useRef();
const simulationRef = useRef();
// Resize listener
const [rect, setRect] = useState<DOMRectReadOnly | null>(null);
useEffect(() => {
new ResizeObserver((entries) => {
for (const entry of entries) {
setRect(entry.contentRect);
}
}).observe(document.body);
}, []);
const [simulationNodes, setSimulationNodes] = useState(() => nodes);
const ticked = useCallback((simulation: d3.Simulation<NodeInfo, any>) => {
const nodes = simulation.nodes();
// setSimulationNodes(nodes);
// d3.select("svg .links")
// .selectAll("line")
// .data(links)
// .join("line")
// .attr("x1", (d) => d.source.x)
// .attr("x2", (d) => d.target.x)
// .attr("y1", (d) => d.source.y)
// .attr("y2", (d) => d.target.y);
d3.select("svg .nodes")
.selectAll("rect")
.data(nodes)
.join("rect")
// .append("foreignObject")
.text((d) => d.name)
.attr("x", (d) => d.x)
.attr("y", (d) => d.y)
.attr("dy", (d) => 5);
}, []);
useEffect(() => {
if (!rect) return;
const simulation = d3
.forceSimulation<NodeInfo>(nodes)
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink(links))
.force("center", d3.forceCenter(rect.width / 2, rect.height / 2));
simulation.on("tick", () => ticked(simulation));
simulationRef.current = simulation;
}, [ticked, rect]);
return (
<div className={styles.container}>
{rect && (
<svg ref={svgRef} width={rect.width} height={rect.height}>
<title>Hello</title>
<g className="links" />
<g className="nodes">
{simulationNodes.map((node, idx) => {
console.log("node", node);
return (
<text
key={idx}
x={(node.x ?? 0).toString()}
y={(node.y ?? 0).toString()}
>
{node.name}
</text>
);
})}
</g>
</svg>
)}
</div>
);
}
function ForeignObjectWrapper({ id }) {
const el = useMemo(() => {
return document.getElementById(id);
}, [id]);
return createPortal(<Counter />, el);
}
function Counter() {
const [counter, setCounter] = useState(0);
//increase counter
const increase = () => {
setCounter((count) => count + 1);
};
//decrease counter
const decrease = () => {
setCounter((count) => count - 1);
};
//reset counter
const reset = () => {
setCounter(0);
};
return (
<div className="btn__container">
<button className="control__btn" onClick={increase}>
+
</button>
<button className="control__btn" onClick={decrease}>
-
</button>
<button className="reset" onClick={reset}>
Reset
</button>
</div>
);
}

8
src/global.scss Normal file
View file

@ -0,0 +1,8 @@
html,
body,
#root {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

5
src/index.tsx Normal file
View file

@ -0,0 +1,5 @@
import App from "./App";
import { createRoot } from "react-dom/client";
import "./global.scss";
createRoot(document.getElementById("root")).render(<App />);

5
tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"compilerOptions": {
"jsx": "react-jsx"
}
}

6
vite.config.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
defineConfig({
plugins: [react()],
});