feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код'
This commit is contained in:
@@ -1,72 +1,60 @@
|
||||
# Graph with Reflexivity
|
||||
# Graph with Reflection on Code
|
||||
|
||||
A lightweight JavaScript implementation of a directed graph where every node automatically has a self‑loop (reflexive edge).
|
||||
The library is intentionally minimal and does **not** depend on any external graph libraries or Python code.
|
||||
This project demonstrates a simple graph data structure and its visualization using D3.js.
|
||||
The graph supports adding nodes, directed edges, and reflexive edges (self‑loops).
|
||||
The UI renders the graph in an SVG canvas with a force‑directed layout.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic reflexivity** – when a node is added, an edge from the node to itself is created.
|
||||
- **Directed edges** – you can add edges in any direction.
|
||||
- **Simple API** – add nodes, add edges, query edges, list nodes, list edges.
|
||||
- **Pure JavaScript** – works in Node.js environments.
|
||||
- Pure JavaScript implementation (no Python or other languages).
|
||||
- Reflexive edges can be added automatically.
|
||||
- Interactive visualization with drag support.
|
||||
- Simple test suite using Jest.
|
||||
|
||||
## Installation
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-2-graf-s-refleksiey-na.git
|
||||
cd povtornyy-ekzamen-2-graf-s-refleksiey-na
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Run the application**
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Open your browser at `http://localhost:3000` (or the port shown in the console).
|
||||
|
||||
4. **Run tests**
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```bash
|
||||
npm install graph-reflexivity
|
||||
```
|
||||
|
||||
> If you want to run the tests or develop locally, clone the repository and run `npm install`.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const Graph = require('graph-reflexivity');
|
||||
|
||||
const g = new Graph();
|
||||
|
||||
// Add nodes
|
||||
g.addNode('A');
|
||||
g.addNode('B');
|
||||
|
||||
// Add directed edge A → B
|
||||
g.addEdge('A', 'B');
|
||||
|
||||
// Reflexive edges are automatically added
|
||||
console.log(g.hasEdge('A', 'A')); // true
|
||||
console.log(g.hasEdge('B', 'B')); // true
|
||||
|
||||
// Query
|
||||
console.log(g.getNeighbors('A')); // ['A', 'B']
|
||||
console.log(g.nodes()); // ['A', 'B']
|
||||
console.log(g.edges()); // [['A', 'A'], ['B', 'B'], ['A', 'B']]
|
||||
├── public
|
||||
│ └── index.html # Entry point for the browser
|
||||
├── src
|
||||
│ ├── index.js # Application bootstrap
|
||||
│ ├── graph.js # Graph data structure
|
||||
│ └── ui.js # Rendering logic
|
||||
├── __tests__
|
||||
│ └── graph.test.js # Jest tests for Graph
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addNode(node)` | Adds a node and its reflexive edge. |
|
||||
| `addEdge(from, to)` | Adds a directed edge; missing nodes are created automatically. |
|
||||
| `hasEdge(from, to)` | Returns `true` if an edge exists. |
|
||||
| `getNeighbors(node)` | Returns an array of all neighbors of `node`. |
|
||||
| `nodes()` | Returns an array of all nodes. |
|
||||
| `edges()` | Returns an array of `[from, to]` pairs for all edges. |
|
||||
|
||||
## Testing
|
||||
|
||||
The project uses Jest for unit tests.
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests are located in the `tests/` directory.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Your Name
|
||||
|
||||
---
|
||||
|
||||
Feel free to open issues or pull requests if you find bugs or want to add features.
|
||||
MIT License
|
||||
+33
-29
@@ -1,46 +1,50 @@
|
||||
**Что реализовано**
|
||||
- Полностью JavaScript‑решение: класс `Graph` в `src/index.js` реализует граф с рефлексией (каждый узел автоматически получает самуюдуру).
|
||||
- Встроенный набор публичных методов (`addNode`, `addEdge`, `hasEdge`, `getNeighbors`, `nodes`, `edges`).
|
||||
- Тесты в `tests/graph.test.js` покрывают все основные сценарии: добавление узлов, добавление рёбер, автоматическое добавление недостающих узлов, получение соседей и список всех рёбер.
|
||||
**SOLUTION.md**
|
||||
|
||||
**Почему это удовлетворяет требованиям**
|
||||
- **Единый стек технологий** – проект использует только Node.js и Jest, без Python‑LangGraph и JavaScript‑микса.
|
||||
- **Рефлексия** реализована через `addNode`, где сразу добавляется `node → node`.
|
||||
- **Автоматическое добавление узлов** при добавлении ребра гарантирует, что граф всегда корректен.
|
||||
- **Тесты** подтверждают, что все публичные методы работают как ожидается, что соответствует требованиям задания.
|
||||
### Что реализовано
|
||||
- **Pure‑JS граф** (`src/graph.js`) с поддержкой рефлексивных ребер.
|
||||
- **Визуализация** графа в браузере через D3 (`src/ui.js`).
|
||||
- **Тесты** на Jest (`__tests__/graph.test.js`) покрывают добавление узлов, рёбер, рефлексивных связей и сериализацию.
|
||||
- **Entry point** (`src/index.js`) создаёт пример графа, добавляет рефлексивные ребра и рендерит его в `public/index.html`.
|
||||
|
||||
**Короткие фрагменты кода**
|
||||
### Почему это удовлетворяет требованиям
|
||||
- **Единый стек технологий** – всё написано на JavaScript, без смешения Python/LangGraph.
|
||||
- **Рефлексивность** реализована в методе `addReflexiveEdges()` и проверяется в тестах.
|
||||
- **Код читаемый и модульный**: `Graph` отвечает только за структуру, `ui.js` – за отображение, `index.js` – за инициализацию.
|
||||
- **Тесты** гарантируют корректность работы ключевых функций, включая ошибку при добавлении ребра к несуществующему узлу.
|
||||
|
||||
`src/index.js` – добавление узла с рефлексией
|
||||
### Ключевые фрагменты кода
|
||||
|
||||
**src/graph.js** – добавление рефлексивных ребер
|
||||
```js
|
||||
addNode(node) {
|
||||
if (!this.adj.has(node)) {
|
||||
this.adj.set(node, new Set([node])); // reflexive edge
|
||||
addReflexiveEdges() {
|
||||
for (const id of this.nodes.keys()) {
|
||||
this.adj.get(id).add(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/index.js` – добавление ребра и авто‑добавление узлов
|
||||
**src/ui.js** – рендер графа в контейнер
|
||||
```js
|
||||
addEdge(from, to) {
|
||||
if (!this.adj.has(from)) this.addNode(from);
|
||||
if (!this.adj.has(to)) this.addNode(to);
|
||||
this.adj.get(from).add(to);
|
||||
export function renderGraph(graph, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
...
|
||||
const simulation = d3.forceSimulation(Array.from(nodes))
|
||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(120))
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`tests/graph.test.js` – проверка рефлексивного ребра
|
||||
**__tests__/graph.test.js** – проверка рефлексивных ребер
|
||||
```js
|
||||
test('adding a node creates reflexive edge', () => {
|
||||
g.addNode('A');
|
||||
expect(g.nodes()).toContain('A');
|
||||
expect(g.hasEdge('A', 'A')).toBe(true);
|
||||
test('adds reflexive edges', () => {
|
||||
g.addReflexiveEdges();
|
||||
expect(g.neighbors('1')).toContain('1');
|
||||
});
|
||||
```
|
||||
|
||||
**Ограничения**
|
||||
- Граф хранится только в памяти; нет возможности сохранять его в файл или базу данных.
|
||||
- Методы работают синхронно, поэтому при больших графах могут возникнуть проблемы с производительностью.
|
||||
- Нет проверки на типы узлов – любой объект может быть использован как ключ в `Map`.
|
||||
### Ограничения
|
||||
- Нет серверной части – граф хранится только в памяти клиента.
|
||||
- Отсутствует экспорт/импорт графа в/из файлов (только JSON в памяти).
|
||||
- UI простая, без возможности редактирования графа пользователем.
|
||||
|
||||
Таким образом, решение полностью соответствует требованиям: использует один стек (JavaScript), реализует граф с рефлексией и покрыто тестами.
|
||||
Тем не менее, решение полностью соответствует заданию и демонстрирует работу графа с рефлексией на чистом JavaScript.
|
||||
@@ -0,0 +1,53 @@
|
||||
// __tests__/graph.test.js
|
||||
import { Graph } from '../src/graph.js';
|
||||
|
||||
describe('Graph', () => {
|
||||
let g;
|
||||
|
||||
beforeEach(() => {
|
||||
g = new Graph();
|
||||
g.addNode('1', { label: 'One' });
|
||||
g.addNode('2', { label: 'Two' });
|
||||
g.addNode('3', { label: 'Three' });
|
||||
});
|
||||
|
||||
test('adds nodes correctly', () => {
|
||||
expect(g.nodes.size).toBe(3);
|
||||
expect(g.nodes.get('1').label).toBe('One');
|
||||
});
|
||||
|
||||
test('adds edges correctly', () => {
|
||||
g.addEdge('1', '2');
|
||||
g.addEdge('2', '3');
|
||||
expect(g.neighbors('1')).toEqual(['2']);
|
||||
expect(g.neighbors('2')).toEqual(['3']);
|
||||
expect(g.neighbors('3')).toEqual([]);
|
||||
});
|
||||
|
||||
test('throws error when adding edge with non-existent node', () => {
|
||||
expect(() => g.addEdge('1', '4')).toThrow();
|
||||
});
|
||||
|
||||
test('adds reflexive edges', () => {
|
||||
g.addReflexiveEdges();
|
||||
expect(g.neighbors('1')).toContain('1');
|
||||
expect(g.neighbors('2')).toContain('2');
|
||||
expect(g.neighbors('3')).toContain('3');
|
||||
});
|
||||
|
||||
test('toJSON returns correct structure', () => {
|
||||
g.addEdge('1', '2');
|
||||
const json = g.toJSON();
|
||||
expect(json.nodes).toHaveLength(3);
|
||||
expect(json.edges).toHaveLength(1);
|
||||
expect(json.edges[0]).toEqual({ src: '1', dst: '2' });
|
||||
});
|
||||
|
||||
test('fromJSON recreates graph', () => {
|
||||
g.addEdge('1', '2');
|
||||
const json = g.toJSON();
|
||||
const g2 = Graph.fromJSON(json);
|
||||
expect(g2.nodes.size).toBe(3);
|
||||
expect(g2.neighbors('1')).toEqual(['2']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Graph with Reflection on Code</title>
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, sans-serif; }
|
||||
#graph-container { width: 100%; height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graph-container"></div>
|
||||
|
||||
<!-- Load D3.js from CDN -->
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<!-- Load the application bundle -->
|
||||
<script type="module" src="../src/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+50
-137
@@ -1,153 +1,66 @@
|
||||
/**
|
||||
* Graph implementation using adjacency list.
|
||||
* Each vertex automatically has a self-loop (reflexive edge).
|
||||
* The graph is undirected.
|
||||
*/
|
||||
class Graph {
|
||||
// src/graph.js
|
||||
// A simple graph implementation with reflexive edge support
|
||||
// This module is pure JavaScript and can be used in both Node and browser environments.
|
||||
|
||||
export class Graph {
|
||||
constructor() {
|
||||
/** @type {Map<*, Set<*>>} */
|
||||
// adjacency list: nodeId -> Set of neighbor nodeIds
|
||||
this.adj = new Map();
|
||||
// node properties: nodeId -> {label, ...}
|
||||
this.nodes = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a vertex to the graph.
|
||||
* If the vertex already exists, nothing changes.
|
||||
* A self-loop is automatically added to make the graph reflexive.
|
||||
* @param {*} v
|
||||
*/
|
||||
addVertex(v) {
|
||||
if (!this.adj.has(v)) {
|
||||
this.adj.set(v, new Set([v]));
|
||||
// Add a node with optional properties
|
||||
addNode(id, props = {}) {
|
||||
if (this.nodes.has(id)) {
|
||||
throw new Error(`Node ${id} already exists`);
|
||||
}
|
||||
this.nodes.set(id, { id, ...props });
|
||||
this.adj.set(id, new Set());
|
||||
}
|
||||
|
||||
// Add a directed edge from src to dst
|
||||
addEdge(src, dst) {
|
||||
if (!this.nodes.has(src) || !this.nodes.has(dst)) {
|
||||
throw new Error(`Both nodes must exist to add an edge: ${src} -> ${dst}`);
|
||||
}
|
||||
this.adj.get(src).add(dst);
|
||||
}
|
||||
|
||||
// Return array of neighbor ids for a node
|
||||
neighbors(id) {
|
||||
if (!this.adj.has(id)) return [];
|
||||
return Array.from(this.adj.get(id));
|
||||
}
|
||||
|
||||
// Add reflexive edges (self-loops) to all nodes
|
||||
addReflexiveEdges() {
|
||||
for (const id of this.nodes.keys()) {
|
||||
this.adj.get(id).add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an undirected edge between u and v.
|
||||
* Vertices are added automatically if they do not exist.
|
||||
* @param {*} u
|
||||
* @param {*} v
|
||||
*/
|
||||
addEdge(u, v) {
|
||||
this.addVertex(u);
|
||||
this.addVertex(v);
|
||||
this.adj.get(u).add(v);
|
||||
this.adj.get(v).add(u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the undirected edge between u and v.
|
||||
* If the edge does not exist, nothing happens.
|
||||
* @param {*} u
|
||||
* @param {*} v
|
||||
*/
|
||||
removeEdge(u, v) {
|
||||
if (this.adj.has(u)) this.adj.get(u).delete(v);
|
||||
if (this.adj.has(v)) this.adj.get(v).delete(u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a vertex and all incident edges.
|
||||
* @param {*} v
|
||||
*/
|
||||
removeVertex(v) {
|
||||
if (!this.adj.has(v)) return;
|
||||
for (const neighbor of this.adj.get(v)) {
|
||||
if (neighbor !== v) this.adj.get(neighbor).delete(v);
|
||||
}
|
||||
this.adj.delete(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an edge exists between u and v.
|
||||
* @param {*} u
|
||||
* @param {*} v
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasEdge(u, v) {
|
||||
return this.adj.has(u) && this.adj.get(u).has(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of neighbors of vertex v.
|
||||
* @param {*} v
|
||||
* @returns {Array<*>}
|
||||
*/
|
||||
getNeighbors(v) {
|
||||
return this.adj.has(v) ? Array.from(this.adj.get(v)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all vertices in the graph.
|
||||
* @returns {Array<*>}
|
||||
*/
|
||||
vertices() {
|
||||
return Array.from(this.adj.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all edges as [u, v] pairs.
|
||||
* Each undirected edge appears only once.
|
||||
* @returns {Array<[*, *]>}
|
||||
*/
|
||||
edges() {
|
||||
// Return a plain object representation (useful for serialization)
|
||||
toJSON() {
|
||||
const nodes = Array.from(this.nodes.values());
|
||||
const edges = [];
|
||||
const seen = new Set();
|
||||
for (const [u, neighbors] of this.adj.entries()) {
|
||||
for (const v of neighbors) {
|
||||
const key = u < v ? `${u}-${v}` : `${v}-${u}`;
|
||||
if (!seen.has(key)) {
|
||||
edges.push([u, v]);
|
||||
seen.add(key);
|
||||
}
|
||||
for (const [src, dstSet] of this.adj.entries()) {
|
||||
for (const dst of dstSet) {
|
||||
edges.push({ src, dst });
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of vertices.
|
||||
* @returns {number}
|
||||
*/
|
||||
size() {
|
||||
return this.adj.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of undirected edges.
|
||||
* @returns {number}
|
||||
*/
|
||||
edgesCount() {
|
||||
return this.edges().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the graph is reflexive (every vertex has a self-loop).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReflexive() {
|
||||
for (const [v, neighbors] of this.adj.entries()) {
|
||||
if (!neighbors.has(v)) return false;
|
||||
// Static helper to create a graph from a JSON representation
|
||||
static fromJSON(json) {
|
||||
const g = new Graph();
|
||||
for (const node of json.nodes) {
|
||||
g.addNode(node.id, node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds self-loops to all vertices, making the graph reflexive.
|
||||
*/
|
||||
makeReflexive() {
|
||||
for (const v of this.adj.keys()) {
|
||||
this.adj.get(v).add(v);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes self-loops from all vertices.
|
||||
*/
|
||||
removeReflexive() {
|
||||
for (const [v, neighbors] of this.adj.entries()) {
|
||||
neighbors.delete(v);
|
||||
for (const edge of json.edges) {
|
||||
g.addEdge(edge.src, edge.dst);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Graph;
|
||||
+20
-89
@@ -1,93 +1,24 @@
|
||||
/**
|
||||
* Graph with reflexivity (self‑loops on every node).
|
||||
*
|
||||
* The graph is represented internally as an adjacency list using a Map.
|
||||
* Each node automatically has an edge to itself when it is added.
|
||||
*
|
||||
* Public API:
|
||||
* - addNode(node): Adds a node and its reflexive edge.
|
||||
* - addEdge(from, to): Adds a directed edge from `from` to `to`.
|
||||
* - hasEdge(from, to): Returns true if an edge exists.
|
||||
* - getNeighbors(node): Returns an array of all neighbors of `node`.
|
||||
* - nodes(): Returns an array of all nodes in the graph.
|
||||
* - edges(): Returns an array of [from, to] pairs representing all edges.
|
||||
*/
|
||||
// src/index.js
|
||||
// Entry point for the application
|
||||
import { Graph } from './graph.js';
|
||||
import { renderGraph } from './ui.js';
|
||||
|
||||
class Graph {
|
||||
constructor() {
|
||||
/** @type {Map<any, Set<any>>} */
|
||||
this.adj = new Map();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Create a sample graph
|
||||
const g = new Graph();
|
||||
g.addNode('A', { label: 'Node A' });
|
||||
g.addNode('B', { label: 'Node B' });
|
||||
g.addNode('C', { label: 'Node C' });
|
||||
g.addNode('D', { label: 'Node D' });
|
||||
|
||||
/**
|
||||
* Adds a node to the graph. If the node already exists, nothing changes.
|
||||
* A reflexive edge (node → node) is automatically added.
|
||||
*
|
||||
* @param {any} node
|
||||
*/
|
||||
addNode(node) {
|
||||
if (!this.adj.has(node)) {
|
||||
this.adj.set(node, new Set([node])); // reflexive edge
|
||||
}
|
||||
}
|
||||
g.addEdge('A', 'B');
|
||||
g.addEdge('B', 'C');
|
||||
g.addEdge('C', 'D');
|
||||
g.addEdge('D', 'A');
|
||||
|
||||
/**
|
||||
* Adds a directed edge from `from` to `to`. If either node does not exist,
|
||||
* it is automatically added (with its reflexive edge).
|
||||
*
|
||||
* @param {any} from
|
||||
* @param {any} to
|
||||
*/
|
||||
addEdge(from, to) {
|
||||
if (!this.adj.has(from)) this.addNode(from);
|
||||
if (!this.adj.has(to)) this.addNode(to);
|
||||
this.adj.get(from).add(to);
|
||||
}
|
||||
// Add reflexive edges (self-loops)
|
||||
g.addReflexiveEdges();
|
||||
|
||||
/**
|
||||
* Checks whether an edge from `from` to `to` exists.
|
||||
*
|
||||
* @param {any} from
|
||||
* @param {any} to
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasEdge(from, to) {
|
||||
return this.adj.has(from) && this.adj.get(from).has(to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all neighbors of the given node.
|
||||
*
|
||||
* @param {any} node
|
||||
* @returns {any[]}
|
||||
*/
|
||||
getNeighbors(node) {
|
||||
return this.adj.has(node) ? Array.from(this.adj.get(node)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all nodes in the graph.
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
nodes() {
|
||||
return Array.from(this.adj.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all edges in the graph as [from, to] pairs.
|
||||
*
|
||||
* @returns {[any, any][]}
|
||||
*/
|
||||
edges() {
|
||||
const edges = [];
|
||||
for (const [from, neighbors] of this.adj.entries()) {
|
||||
for (const to of neighbors) {
|
||||
edges.push([from, to]);
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Graph;
|
||||
// Render the graph into the container with id "graph-container"
|
||||
renderGraph(g, 'graph-container');
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
// src/ui.js
|
||||
// Simple UI rendering using D3.js
|
||||
// Assumes D3 is loaded globally (e.g., via CDN in index.html)
|
||||
|
||||
export function renderGraph(graph, containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
throw new Error(`Container with id "${containerId}" not found`);
|
||||
}
|
||||
|
||||
// Clear previous content
|
||||
container.innerHTML = '';
|
||||
|
||||
const width = container.clientWidth || 600;
|
||||
const height = container.clientHeight || 400;
|
||||
|
||||
const svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
const nodes = graph.nodes.values();
|
||||
const edges = [];
|
||||
for (const [src, dstSet] of graph.adj.entries()) {
|
||||
for (const dst of dstSet) {
|
||||
edges.push({ source: src, target: dst });
|
||||
}
|
||||
}
|
||||
|
||||
// Simple force simulation for layout
|
||||
const simulation = d3.forceSimulation(Array.from(nodes))
|
||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(120))
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
|
||||
const link = svg.append('g')
|
||||
.attr('class', 'links')
|
||||
.selectAll('line')
|
||||
.data(edges)
|
||||
.enter()
|
||||
.append('line')
|
||||
.attr('stroke', '#999')
|
||||
.attr('stroke-width', 1.5);
|
||||
|
||||
const node = svg.append('g')
|
||||
.attr('class', 'nodes')
|
||||
.selectAll('circle')
|
||||
.data(Array.from(nodes))
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('r', 20)
|
||||
.attr('fill', '#69b3a2')
|
||||
.call(d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended));
|
||||
|
||||
const label = svg.append('g')
|
||||
.attr('class', 'labels')
|
||||
.selectAll('text')
|
||||
.data(Array.from(nodes))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('dy', 4)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d => d.label || d.id);
|
||||
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
node
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y);
|
||||
|
||||
label
|
||||
.attr('x', d => d.x)
|
||||
.attr('y', d => d.y);
|
||||
});
|
||||
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user