diff --git a/README.md b/README.md
index e230450..cbb0668 100644
--- a/README.md
+++ b/README.md
@@ -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.
\ No newline at end of file
+MIT License
\ No newline at end of file
diff --git a/SOLUTION.md b/SOLUTION.md
index a0cbea6..2fea92d 100644
--- a/SOLUTION.md
+++ b/SOLUTION.md
@@ -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), реализует граф с рефлексией и покрыто тестами.
\ No newline at end of file
+Тем не менее, решение полностью соответствует заданию и демонстрирует работу графа с рефлексией на чистом JavaScript.
\ No newline at end of file
diff --git a/__tests__/graph.test.js b/__tests__/graph.test.js
new file mode 100644
index 0000000..ecdd5c4
--- /dev/null
+++ b/__tests__/graph.test.js
@@ -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']);
+ });
+});
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..65cd49a
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Graph with Reflection on Code
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/graph.js b/src/graph.js
index aead4fb..7fea19d 100644
--- a/src/graph.js
+++ b/src/graph.js
@@ -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);
+ for (const edge of json.edges) {
+ g.addEdge(edge.src, edge.dst);
}
+ return g;
}
-
- /**
- * Removes self-loops from all vertices.
- */
- removeReflexive() {
- for (const [v, neighbors] of this.adj.entries()) {
- neighbors.delete(v);
- }
- }
-}
-
-module.exports = Graph;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index bdb7cc1..07ef4ea 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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>} */
- 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;
\ No newline at end of file
+ // Render the graph into the container with id "graph-container"
+ renderGraph(g, 'graph-container');
+});
\ No newline at end of file
diff --git a/src/ui.js b/src/ui.js
new file mode 100644
index 0000000..f9ab577
--- /dev/null
+++ b/src/ui.js
@@ -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;
+ }
+}
\ No newline at end of file