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).
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 (selfloops).
The UI renders the graph in an SVG canvas with a forcedirected 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
View File
@@ -1,46 +1,50 @@
**Что реализовано**
- Полностью JavaScript‑решение: класс `Graph` в `src/index.js` реализует граф с рефлексией (каждый узел автоматически получает самуюдуру).
- Встроенный набор публичных методов (`addNode`, `addEdge`, `hasEdge`, `getNeighbors`, `nodes`, `edges`).
- Тесты в `tests/graph.test.js` покрывают все основные сценарии: добавление узлов, добавление рёбер, автоматическое добавление недостающих узлов, получение соседей и список всех рёбер.
**SOLUTION.md**
**Почему это удовлетворяет требованиям**
- **Единый стек технологий** – проект использует только Node.js и Jest, без PythonLangGraph и JavaScript‑микса.
- **Рефлексия** реализована через `addNode`, где сразу добавляется `node → node`.
- **Автоматическое добавление узлов** при добавлении ребра гарантирует, что граф всегда корректен.
- **Тесты** подтверждают, что все публичные методы работают как ожидается, что соответствует требованиям задания.
### Что реализовано
- **PureJS граф** (`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.
+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 @@
/**
* 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;
// 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);
}
/**
* 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;
}
/**
* 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
View File
@@ -1,93 +1,24 @@
/**
* Graph with reflexivity (selfloops 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');
});
+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;
}
}