start
This commit is contained in:
commit
c79fb7de78
11 changed files with 229 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
17
biome.json
Normal file
17
biome.json
Normal 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
BIN
bun.lockb
Executable file
Binary file not shown.
2
index.html
Normal file
2
index.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
|
<div id="root"></div>
|
16
package.json
Normal file
16
package.json
Normal 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
21
src/App.module.scss
Normal 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
148
src/App.tsx
Normal 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
8
src/global.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
5
src/index.tsx
Normal file
5
src/index.tsx
Normal 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
5
tsconfig.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
|
||||||
|
defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
Loading…
Reference in a new issue