From ad0bbb579d6572b42c2be0baf3a203d690cd4dec Mon Sep 17 00:00:00 2001 From: monomono Date: Sun, 16 Aug 2026 03:10:57 -0500 Subject: [PATCH] #49 feat: grafico unificado de provenance con filtro por niveles y re-estabilizacion --- AGENTS.md | 2 +- src/components/graph/VisChart.vue | 38 +- .../ProductSupplierOrganizationChart.vue | 59 ---- .../provenance/ProductTerritoryChart.vue | 59 ---- src/components/provenance/ProvenanceGraph.vue | 95 +++++ .../provenance/ProvenanceSection.vue | 11 +- src/components/provenance/provenance-graph.js | 113 +++--- src/components/provenance/provenance-vis.js | 9 +- tests/unit/components/graph/VisChart.spec.js | 40 ++- .../ProductSupplierOrganizationChart.spec.js | 182 ---------- .../provenance/ProductTerritoryChart.spec.js | 146 -------- .../provenance/ProvenanceGraph.spec.js | 154 +++++++++ .../provenance/ProvenanceSection.spec.js | 22 +- .../provenance/provenance-graph.spec.js | 325 ++++++------------ 14 files changed, 508 insertions(+), 747 deletions(-) delete mode 100644 src/components/provenance/ProductSupplierOrganizationChart.vue delete mode 100644 src/components/provenance/ProductTerritoryChart.vue create mode 100644 src/components/provenance/ProvenanceGraph.vue delete mode 100644 tests/unit/components/provenance/ProductSupplierOrganizationChart.spec.js delete mode 100644 tests/unit/components/provenance/ProductTerritoryChart.spec.js create mode 100644 tests/unit/components/provenance/ProvenanceGraph.spec.js diff --git a/AGENTS.md b/AGENTS.md index 14db67a..90d43e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,7 +177,7 @@ No hay un estilo mayoritario. El código histórico está partido: - Payload: `[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]`. - **Genérico reutilizable:** - `src/components/graph/VisChart.vue`: wrapper de vis-network (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto -- **Específico público** (`src/components/provenance/`): builders puros en `provenance-graph.js` (`buildSupplierOrganizationGraph`, `buildTerritoryGraph`, `hasAnySupplier`, `hasAnyTerritory`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). El gráfico de territorio va solo hasta departamento (sin país, por ahora Colombia). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. Los charts (que muestran "próximamente estará disponible" cuando no hay relaciones), `ProvenanceDetailModal.vue` y `ProvenanceSection.vue` +- **Específico público** (`src/components/provenance/`): builder puro en `provenance-graph.js` (`buildProvenanceGraph(provenance, kinds)`, `hasAnySupplier`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. `buildProvenanceGraph` acepta los niveles a graficar (`product`, `supplier`, `organization`, `municipality`, `department`, `country`); los niveles omitidos se saltan conectando el nivel previo con el siguiente. `ProvenanceGraph.vue` unifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores) y columnas por nivel (x fijo por tipo; la física ordena la y); muestra "próximamente estará disponible" cuando no hay relaciones. `ProvenanceDetailModal.vue` y `ProvenanceSection.vue` - **Admin CRUD** (`src/components/provenance/admin/`): `OrganizationsManagement.vue`, `SuppliersManagement.vue`, `GeographyManagement.vue` (tabs países/departamentos/municipios), `SupplierLinkDialog.vue` (vincula productos↔proveedores, abierto desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue` - **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products//` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products//` - Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }` diff --git a/src/components/graph/VisChart.vue b/src/components/graph/VisChart.vue index 8957898..35a4853 100644 --- a/src/components/graph/VisChart.vue +++ b/src/components/graph/VisChart.vue @@ -27,6 +27,10 @@ type: Object, default: () => ({}), }, + minVerticalSpacing: { + type: Number, + default: null, + }, }) const emit = defineEmits(['select']) @@ -87,7 +91,28 @@ network.setData({ nodes: nodesDataSet, edges: new DataSet(edges) }) } - onMounted(() => { + function applyMinVerticalSpacing (minSpacing) { + if (!minSpacing || minSpacing <= 0) return + const columns = new Map() + for (const [id, position] of Object.entries(network.getPositions())) { + const x = Math.round(position.x) + if (!columns.has(x)) columns.set(x, []) + columns.get(x).push({ id, x, y: position.y }) + } + for (const column of columns.values()) { + column.sort((a, b) => a.y - b.y) + let previousY = null + for (const item of column) { + if (previousY !== null && item.y - previousY < minSpacing) { + network.moveNode(item.id, item.x, previousY + minSpacing) + } + previousY = network.getPositions()[item.id].y + } + } + } + + function setupNetwork () { + if (network) network.destroy() network = new Network(containerRef.value, { nodes: [], edges: [] }, mergeOptions()) render(props.nodes, props.edges) @@ -100,15 +125,18 @@ network.once('stabilizationIterationsDone', () => { network.setOptions({ physics: { enabled: false } }) + applyMinVerticalSpacing(props.minVerticalSpacing) network.fit() }) - }) + } + + onMounted(setupNetwork) watch( () => [props.nodes, props.edges], - ([nodes, edges]) => { - if (!network) return - render(nodes, edges) + () => { + if (!containerRef.value) return + setupNetwork() }, { deep: true } ) diff --git a/src/components/provenance/ProductSupplierOrganizationChart.vue b/src/components/provenance/ProductSupplierOrganizationChart.vue deleted file mode 100644 index e87a7ad..0000000 --- a/src/components/provenance/ProductSupplierOrganizationChart.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/src/components/provenance/ProductTerritoryChart.vue b/src/components/provenance/ProductTerritoryChart.vue deleted file mode 100644 index 12289f5..0000000 --- a/src/components/provenance/ProductTerritoryChart.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/src/components/provenance/ProvenanceGraph.vue b/src/components/provenance/ProvenanceGraph.vue new file mode 100644 index 0000000..a6c8682 --- /dev/null +++ b/src/components/provenance/ProvenanceGraph.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/components/provenance/ProvenanceSection.vue b/src/components/provenance/ProvenanceSection.vue index efae78d..4f6da26 100644 --- a/src/components/provenance/ProvenanceSection.vue +++ b/src/components/provenance/ProvenanceSection.vue @@ -6,19 +6,12 @@ Conoce quiénes producen los productos que compras y de qué territorios provienen.

-

Proveedores y organizaciones

- - - - -

Departamento y municipio

- +