feat: solution for 'Повторный экзамен #2: Граф с рефлексией на код'

This commit is contained in:
2026-07-01 13:27:40 +03:00
parent 9412dbf851
commit d4380cc53b
7 changed files with 325 additions and 317 deletions
+49 -61
View File
@@ -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 selfloop (reflexive edge). This project demonstrates a simple graph data structure and its visualization using D3.js.
The library is intentionally minimal and does **not** depend on any external graph libraries or Python code. The graph supports adding nodes, directed edges, and reflexive edges (selfloops).
The UI renders the graph in an SVG canvas with a forcedirected layout.
## Features ## Features
- **Automatic reflexivity** when a node is added, an edge from the node to itself is created. - Pure JavaScript implementation (no Python or other languages).
- **Directed edges** you can add edges in any direction. - Reflexive edges can be added automatically.
- **Simple API** add nodes, add edges, query edges, list nodes, list edges. - Interactive visualization with drag support.
- **Pure JavaScript** works in Node.js environments. - 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
``` ```
├── public
> If you want to run the tests or develop locally, clone the repository and run `npm install`. │ └── index.html # Entry point for the browser
├── src
## Usage │ ├── index.js # Application bootstrap
│ ├── graph.js # Graph data structure
```js │ └── ui.js # Rendering logic
const Graph = require('graph-reflexivity'); ├── __tests__
│ └── graph.test.js # Jest tests for Graph
const g = new Graph(); ├── package.json
└── README.md
// 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']]
``` ```
## 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 ## License
MIT © Your Name MIT License
---
Feel free to open issues or pull requests if you find bugs or want to add features.
+33 -29
View File
@@ -1,46 +1,50 @@
**Что реализовано** **SOLUTION.md**
- Полностью JavaScript‑решение: класс `Graph` в `src/index.js` реализует граф с рефлексией (каждый узел автоматически получает самуюдуру).
- Встроенный набор публичных методов (`addNode`, `addEdge`, `hasEdge`, `getNeighbors`, `nodes`, `edges`).
- Тесты в `tests/graph.test.js` покрывают все основные сценарии: добавление узлов, добавление рёбер, автоматическое добавление недостающих узлов, получение соседей и список всех рёбер.
**Почему это удовлетворяет требованиям** ### Что реализовано
- **Единый стек технологий** – проект использует только Node.js и Jest, без PythonLangGraph и JavaScript‑микса. - **PureJS граф** (`src/graph.js`) с поддержкой рефлексивных ребер.
- **Рефлексия** реализована через `addNode`, где сразу добавляется `node → node`. - **Визуализация** графа в браузере через 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 ```js
addNode(node) { addReflexiveEdges() {
if (!this.adj.has(node)) { for (const id of this.nodes.keys()) {
this.adj.set(node, new Set([node])); // reflexive edge this.adj.get(id).add(id);
} }
} }
``` ```
`src/index.js` – добавление ребра и авто‑добавление узлов **src/ui.js** – рендер графа в контейнер
```js ```js
addEdge(from, to) { export function renderGraph(graph, containerId) {
if (!this.adj.has(from)) this.addNode(from); const container = document.getElementById(containerId);
if (!this.adj.has(to)) this.addNode(to); ...
this.adj.get(from).add(to); 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 ```js
test('adding a node creates reflexive edge', () => { test('adds reflexive edges', () => {
g.addNode('A'); g.addReflexiveEdges();
expect(g.nodes()).toContain('A'); expect(g.neighbors('1')).toContain('1');
expect(g.hasEdge('A', 'A')).toBe(true);
}); });
``` ```
**Ограничения** ### Ограничения
- Граф хранится только в памяти; нет возможности сохранять его в файл или базу данных. - Нет серверной части – граф хранится только в памяти клиента.
- Методы работают синхронно, поэтому при больших графах могут возникнуть проблемы с производительностью. - Отсутствует экспорт/импорт графа в/из файлов (только JSON в памяти).
- Нет проверки на типы узлов – любой объект может быть использован как ключ в `Map`. - UI простая, без возможности редактирования графа пользователем.
Таким образом, решение полностью соответствует требованиям: использует один стек (JavaScript), реализует граф с рефлексией и покрыто тестами. Тем не менее, решение полностью соответствует заданию и демонстрирует работу графа с рефлексией на чистом JavaScript.
+53
View File
@@ -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']);
});
});
+19
View File
@@ -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
View File
@@ -1,153 +1,66 @@
/** // src/graph.js
* Graph implementation using adjacency list. // A simple graph implementation with reflexive edge support
* Each vertex automatically has a self-loop (reflexive edge). // This module is pure JavaScript and can be used in both Node and browser environments.
* The graph is undirected.
*/ export class Graph {
class Graph {
constructor() { constructor() {
/** @type {Map<*, Set<*>>} */ // adjacency list: nodeId -> Set of neighbor nodeIds
this.adj = new Map(); this.adj = new Map();
// node properties: nodeId -> {label, ...}
this.nodes = new Map();
} }
/** // Add a node with optional properties
* Adds a vertex to the graph. addNode(id, props = {}) {
* If the vertex already exists, nothing changes. if (this.nodes.has(id)) {
* A self-loop is automatically added to make the graph reflexive. throw new Error(`Node ${id} already exists`);
* @param {*} v }
*/ this.nodes.set(id, { id, ...props });
addVertex(v) { this.adj.set(id, new Set());
if (!this.adj.has(v)) { }
this.adj.set(v, new Set([v]));
// 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);
} }
} }
/** // Return a plain object representation (useful for serialization)
* Adds an undirected edge between u and v. toJSON() {
* Vertices are added automatically if they do not exist. const nodes = Array.from(this.nodes.values());
* @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() {
const edges = []; const edges = [];
const seen = new Set(); for (const [src, dstSet] of this.adj.entries()) {
for (const [u, neighbors] of this.adj.entries()) { for (const dst of dstSet) {
for (const v of neighbors) { edges.push({ src, dst });
const key = u < v ? `${u}-${v}` : `${v}-${u}`;
if (!seen.has(key)) {
edges.push([u, v]);
seen.add(key);
}
} }
} }
return edges; return { nodes, edges };
} }
/** // Static helper to create a graph from a JSON representation
* Returns the number of vertices. static fromJSON(json) {
* @returns {number} const g = new Graph();
*/ for (const node of json.nodes) {
size() { g.addNode(node.id, node);
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;
} }
return true; for (const edge of json.edges) {
} g.addEdge(edge.src, edge.dst);
/**
* 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);
} }
return g;
} }
} }
module.exports = Graph;
+20 -89
View File
@@ -1,93 +1,24 @@
/** // src/index.js
* Graph with reflexivity (selfloops on every node). // Entry point for the application
* import { Graph } from './graph.js';
* The graph is represented internally as an adjacency list using a Map. import { renderGraph } from './ui.js';
* 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.
*/
class Graph { document.addEventListener('DOMContentLoaded', () => {
constructor() { // Create a sample graph
/** @type {Map<any, Set<any>>} */ const g = new Graph();
this.adj = new Map(); g.addNode('A', { label: 'Node A' });
} g.addNode('B', { label: 'Node B' });
g.addNode('C', { label: 'Node C' });
g.addNode('D', { label: 'Node D' });
/** g.addEdge('A', 'B');
* Adds a node to the graph. If the node already exists, nothing changes. g.addEdge('B', 'C');
* A reflexive edge (node → node) is automatically added. g.addEdge('C', 'D');
* g.addEdge('D', 'A');
* @param {any} node
*/
addNode(node) {
if (!this.adj.has(node)) {
this.adj.set(node, new Set([node])); // reflexive edge
}
}
/** // Add reflexive edges (self-loops)
* Adds a directed edge from `from` to `to`. If either node does not exist, g.addReflexiveEdges();
* 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);
}
/** // Render the graph into the container with id "graph-container"
* Checks whether an edge from `from` to `to` exists. renderGraph(g, 'graph-container');
* });
* @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;
+100
View File
@@ -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;
}
}