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