{"id":35432524,"url":"https://github.com/justdeko/kuiver","last_synced_at":"2026-01-16T03:39:29.841Z","repository":{"id":331234061,"uuid":"1111308132","full_name":"justdeko/kuiver","owner":"justdeko","description":"A Compose Multiplatform library for visualizing and interacting with directed graphs","archived":false,"fork":false,"pushed_at":"2026-01-12T21:00:09.000Z","size":6346,"stargazers_count":53,"open_issues_count":1,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-13T19:58:39.039Z","etag":null,"topics":["android","compose-multiplatform","graph-visualization","ios","kotlin","kotlin-multiplatform"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/justdeko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-06T17:21:45.000Z","updated_at":"2026-01-13T11:21:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/justdeko/kuiver","commit_stats":null,"previous_names":["justdeko/kuiver"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/justdeko/kuiver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justdeko%2Fkuiver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justdeko%2Fkuiver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justdeko%2Fkuiver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justdeko%2Fkuiver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/justdeko","download_url":"https://codeload.github.com/justdeko/kuiver/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justdeko%2Fkuiver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28477204,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T03:13:13.607Z","status":"ssl_error","status_checked_at":"2026-01-16T03:11:47.863Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","compose-multiplatform","graph-visualization","ios","kotlin","kotlin-multiplatform"],"created_at":"2026-01-02T21:13:15.349Z","updated_at":"2026-01-16T03:39:29.826Z","avatar_url":"https://github.com/justdeko.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"docs/kuiver-logo.svg\" alt=\"Kuiver Logo\" height=\"150\"\u003e\n\n# Kuiver\n\n**A Kotlin Multiplatform graph visualization library**\n\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.justdeko/kuiver?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.justdeko/kuiver)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n\u003cimg src=\"docs/images/demo-interaction.gif\" alt=\"Interactive graph visualization\" width=\"600\"\u003e\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n| **Android** | **iOS** | **Web** |\n|:---:|:---:|:---:|\n| \u003cimg src=\"docs/images/demo-android.png\" alt=\"Android\" height=\"400\"/\u003e | \u003cimg src=\"docs/images/demo-ios.png\" alt=\"iOS\" height=\"400\"/\u003e | \u003cimg src=\"docs/images/demo-web.png\" alt=\"Web\" height=\"400\"/\u003e |\n\n\u003c/div\u003e\n\n\u003e **ALPHA RELEASE** - This library is in early development. The API is subject to change and may\n\u003e contain bugs.\n\u003e Feedback and bug reports are welcome\n\u003e at [GitHub Issues](https://github.com/justdeko/kuiver/issues).\n\n## What it does\n\n- 2 built-in layout algorithms (hierarchical and force-directed) plus support for custom layouts\n- Handles both acyclic and cyclic graphs\n- Customizable nodes and edges\n- Zooming and panning\n- Resizable canvas\n- Layout animations\n\n## Installation\n\nKuiver is available on Maven Central.\n\nFor multiplatform projects, add to your common source set:\n\n```kotlin\nkotlin {\n    sourceSets {\n        commonMain.dependencies {\n            implementation(\"io.github.justdeko:kuiver:0.2.1\")\n        }\n    }\n}\n```\n\nOr for a specific platform only:\n\n```kotlin\nkotlin {\n    sourceSets {\n        androidMain.dependencies {\n            implementation(\"io.github.justdeko:kuiver-android:0.2.1\")\n        }\n        iosMain.dependencies {\n            implementation(\"io.github.justdeko:kuiver-iosarm64:0.2.1\")\n        }\n        // etc.\n    }\n}\n```\n\n### Supported Platforms\n\n- Android (minSdk 24)\n- iOS\n- JVM (Desktop)\n- Web (wasmJs/js) - experimental, see [limitations](#known-issues--limitations)\n\n## Basic Usage\n\n```kotlin\n@Composable\nfun MyGraphViewer() {\n    // Create graph structure\n    val kuiver = remember {\n        buildKuiver {\n            // Add nodes\n            nodes(\"A\", \"B\", \"C\")\n\n            // Add edges\n            edges(\n                \"A\" to \"B\",\n                \"B\" to \"C\",\n                \"A\" to \"C\"\n            )\n        }\n    }\n\n    // Configure layout\n    val layoutConfig = LayoutConfig.Hierarchical(\n        direction = LayoutDirection.HORIZONTAL\n    )\n\n    // Create viewer state\n    val viewerState = rememberKuiverViewerState(\n        initialKuiver = kuiver,\n        layoutConfig = layoutConfig\n    )\n\n    // Render the graph\n    KuiverViewer(\n        state = viewerState,\n        nodeContent = { node -\u003e\n            // Customize node appearance\n            Box(\n                modifier = Modifier\n                    .size(80.dp)\n                    .background(Color.Blue, CircleShape),\n                contentAlignment = Alignment.Center\n            ) {\n                Text(node.id, color = Color.White)\n            }\n        },\n        edgeContent = { edge, from, to -\u003e\n            // Customize edge appearance\n            EdgeContent(from, to, color = Color.Gray)\n        }\n    )\n}\n```\n\n## Customization\n\n### Node Data\n\nKuiver only handles visual graph structure using node IDs. Store your application data separately\nand look it up by node ID in your `nodeContent` composable.\n\n### Custom Edges\n\nThe `edgeContent` lambda receives the edge data and start/end positions (`from: Offset`,\n`to: Offset`). You can use built-in components or create custom rendering with Canvas:\n\n```kotlin\n// Using built-in styled edges (automatically styles FORWARD, BACK, CROSS, SELF_LOOP)\nedgeContent = { edge, from, to -\u003e\n    StyledEdgeContent(\n        edge = edge,\n        from = from,\n        to = to,\n        baseColor = Color.Black,\n        backEdgeColor = Color(0xFFFF6B6B),\n        strokeWidth = 3f\n    )\n}\n\n// Custom edge rendering\nedgeContent = { edge, from, to -\u003e\n    Canvas(modifier = Modifier.fillMaxSize()) {\n        drawLine(\n            color = Color.Blue,\n            start = from,\n            end = to,\n            strokeWidth = 2.dp.toPx()\n        )\n        // Draw custom arrows, labels, etc.\n    }\n}\n```\n\n### Node Dimensions\n\nKuiver automatically measures node dimensions from your `nodeContent`. You can also specify\ndimensions explicitly:\n\n```kotlin\nbuildKuiver {\n    // Auto-measured (recommended)\n    nodes(\"A\")\n\n    // Explicit dimensions\n    addNode(\n        KuiverNode(\n            id = \"B\",\n            dimensions = NodeDimensions(width = 120.dp, height = 80.dp)\n        )\n    )\n}\n```\n\n### Edge Anchor Points\n\nBy default, edges point and connect to the node center (with consideration of the node boundaries).\nFor precise control, you can define custom anchor points:\n\n```kotlin\nnodeContent = { node -\u003e\n    Box(modifier = Modifier.size(120.dp, 80.dp).background(Color.Blue)) {\n        // Define anchors with optional visual indicators\n        KuiverAnchor(\n            anchorId = \"left\",\n            nodeId = node.id,\n            modifier = Modifier.align(Alignment.CenterStart)\n        ) {\n            Box(\n                Modifier\n                    .size(8.dp)\n                    .background(Color.White, CircleShape)\n            )\n        }\n\n        KuiverAnchor(\n            anchorId = \"right\",\n            nodeId = node.id,\n            modifier = Modifier.align(Alignment.CenterEnd)\n        )\n\n        Text(\"Node ${node.id}\", modifier = Modifier.align(Alignment.Center))\n    }\n}\n\n// Reference anchors in edges\nbuildKuiver {\n    nodes(\"A\", \"B\")\n    edge(\n        from = \"A\",\n        to = \"B\",\n        fromAnchor = \"right\",\n        toAnchor = \"left\"\n    )\n}\n```\n\n**Things to keep in mind:**\n- Anchor IDs are scoped per-node (each node has its own namespace).\n- Missing anchors or anchors that aren't found fallback to automatic edge positioning\n\nSee `ProcessDiagramDemo.kt` for a complete example with multiple anchors per side.\n\n## Layout Algorithms\n\n\u003e **Note:** The layout algorithms are simple implementations based on established graph layouting\n\u003e techniques. While inspired by academic research, they are not direct ports of published\n\u003e implementations. Expect flaws and suboptimal layouts on complex graphs.\n\n### Hierarchical Layout\n\nBest for directed acyclic graphs (DAGs) and tree structures. Automatically handles cycles by\nclassifying back edges.\n\n```kotlin\nval layoutConfig = LayoutConfig.Hierarchical(\n    direction = LayoutDirection.HORIZONTAL,  // or VERTICAL\n    levelSpacing = 150f,      // Distance between hierarchy levels\n    nodeSpacing = 100f        // Distance between nodes in same level\n)\n```\n\n**Edge Types in Hierarchical Layout:**\n\n- `FORWARD` - Edges to descendants (typical parent-child edges)\n- `BACK` - Edges to ancestors (creates cycles, rendered as dashed by `StyledEdgeContent`)\n- `CROSS` - Edges between nodes at similar hierarchy levels\n- `SELF_LOOP` - Edges from a node to itself\n\n### Force-Directed Layout\n\nBest for understanding relationships in general graphs. Creates organic, balanced layouts using\nphysics simulation.\n\n```kotlin\nval layoutConfig = LayoutConfig.ForceDirected(\n    iterations = 200,              // Simulation steps (more = better layout, slower)\n    repulsionStrength = 500f,      // How strongly nodes push apart\n    attractionStrength = 0.02f,    // How strongly connected nodes pull together\n    damping = 0.85f                // Velocity damping (stability vs convergence speed)\n)\n```\n\n### Custom Layouts\n\nYou can provide your own layout algorithm using `LayoutConfig.Custom`. This gives you full\ncontrol over node positioning.\n\n```kotlin\n// Define a custom circular layout\nval circularLayout: LayoutProvider = { kuiver, config -\u003e\n    val nodesList = kuiver.nodes.values.toList()\n    val radius = minOf(config.width, config.height) * 0.4f\n    val centerX = config.width / 2f\n    val centerY = config.height / 2f\n\n    val updatedNodes = nodesList.mapIndexed { index, node -\u003e\n        val angle = (index.toFloat() / nodesList.size) * 2f * PI.toFloat()\n        node.copy(\n            position = Offset(\n                x = centerX + radius * cos(angle),\n                y = centerY + radius * sin(angle)\n            )\n        )\n    }\n\n    buildKuiverWithClassifiedEdges(updatedNodes, kuiver.edges)\n}\n\n// Use the custom layout\nval layoutConfig = LayoutConfig.Custom(\n    provider = circularLayout\n)\n```\n\n**Custom Layout Tips:**\n\n- Your layout function receives the `Kuiver` graph and `LayoutConfig` (use `LayoutConfig.Custom`)\n- Access canvas dimensions via `config.width` and `config.height`\n- Always use `buildKuiverWithClassifiedEdges(updatedNodes, kuiver.edges)` to construct the result\n- Handle zero dimensions gracefully (canvas might not be measured yet on first layout)\n- Use `remember` to stabilize your layout function in Compose to avoid unnecessary recompositions\n\n## Viewer Configuration\n\nCustomize viewer behavior with `KuiverViewerConfig`:\n\n```kotlin\nKuiverViewer(\n    state = viewerState,\n    config = KuiverViewerConfig(\n        // Visual\n        showDebugBounds = false,           // Show node bounding boxes for debugging\n\n        // Viewport\n        fitToContent = true,               // Auto-fit graph to viewport on load\n        contentPadding = 0.8f,             // Padding around content (0-1 scale)\n\n        // Zoom\n        minScale = 0.1f,                   // Minimum zoom level (10%)\n        maxScale = 5f,                     // Maximum zoom level (500%)\n\n        // Pan\n        panVelocity = 1.0f,                // Scroll sensitivity (platform-specific default)\n\n        // Animations\n        scaleAnimationSpec = spring(       // Zoom animation\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessLow\n        ),\n        offsetAnimationSpec = spring(), // Pan animation\n        nodeAnimationSpec = spring(),   // Node position animation (during layout)\n\n        // Desktop-specific\n        zoomConditionDesktop = { event -\u003e  // When to zoom vs pan on desktop\n            event.keyboardModifiers.isCtrlPressed\n        }\n    ),\n    nodeContent = { node -\u003e /* ... */ },\n    edgeContent = { edge, from, to -\u003e /* ... */ }\n)\n```\n\n## Interaction \u0026 State Management\n\n### Programmatic Controls\n\n```kotlin\n// Zoom and navigation\nviewerState.zoomIn()       // Zoom in (1.2x)\nviewerState.zoomOut()      // Zoom out (1/1.2x)\nviewerState.centerGraph()  // Center graph in viewport\n\n// Direct control\nviewerState.updateTransform(scale = 1.5f, offset = Offset(100f, 100f))\n\n// Access current state\nval currentScale = viewerState.scale\nval currentOffset = viewerState.offset\n```\n\n### User Interactions\n\n- **Touch/Mobile:** Drag to pan, pinch to zoom\n- **Mouse/Desktop:** Drag to pan, scroll to pan, Ctrl+Scroll to zoom\n\n### State Persistence\n\nUse `rememberSaveableKuiverViewerState` to preserve zoom/pan across process death.\n\n### Updating the Graph\n\nUpdate the graph structure by passing an updated or new `Kuiver` instance with\n`viewerState.updateKuiver(newKuiver)`.\n\n## Advanced Features\n\n### Cycle Detection\n\n```kotlin\nval kuiver = buildKuiver {\n    nodes(\"A\", \"B\", \"C\")\n    edges(\n        \"A\" to \"B\",\n        \"B\" to \"C\"\n    )\n\n    // Check before adding edge that would create a cycle\n    if (!wouldCreateCycle(from = \"C\", to = \"A\")) {\n        edge(\"C\", \"A\")\n    } else {\n        println(\"Skipping edge C -\u003e A: would create a cycle\")\n    }\n}\n\n// Check existing graph\nif (kuiver.hasCycles()) {\n    val components = kuiver.findStronglyConnectedComponents()\n    println(\"Strongly connected components: $components\")\n}\n```\n\n### Edge Classification\n\n```kotlin\nval kuiver = buildKuiver {\n    nodes(\"A\", \"B\", \"C\")\n    edges(\n        \"A\" to \"B\",\n        \"B\" to \"C\",\n        \"C\" to \"A\"  // Back edge (creates cycle)\n    )\n}\n\n// Classify all edges\nval edgeTypes = kuiver.classifyAllEdges()\nedgeTypes.forEach { (edge, type) -\u003e\n    println(\"${edge.fromId} -\u003e ${edge.toId}: $type\")\n}\n// Output:\n// A -\u003e B: FORWARD\n// B -\u003e C: FORWARD\n// C -\u003e A: BACK\n```\n\n### Topological Ordering\n\n```kotlin\n// For DAGs or graphs with back edges removed\nval order = kuiver.getTopologicalOrder()\nprintln(\"Topological order: $order\")\n// Useful for dependency resolution, task scheduling, etc.\n```\n\n## Sample Application\n\nA complete demo app is included in [`/sample`](./sample). Open the project in **IntelliJ IDEA** or **Android Studio**, sync, and select a run configuration (Desktop/Android/iOS/Web) from the dropdown.\n\nYou can also run from the command line:\n\n```bash\n./gradlew :sample:composeApp:run  # Desktop\n```\n\n## Known Issues \u0026 Limitations\n\n### Web Platform\n\nThe Web target is **experimental** and has known issues.\n\nThe library implements several web-specific adjustments to handle browser limitations:\n\n- **Reduced Pan Velocity**: Default pan velocity is `4f` (vs `30f` on native platforms) to\n  compensate for higher scroll sensitivity in browsers\n    - See: `core/src/wasmJsMain/kotlin/com/dk/kuiver/renderer/PlatformDefaults.wasmJs.kt:4`\n- **Font Loading Delay**: 100ms delay on initial node measurement to prevent text wrapping issues\n  when browser fonts haven't finished loading\n    - See: `core/src/wasmJsMain/kotlin/com/dk/kuiver/renderer/PlatformDefaults.wasmJs.kt:5`\n    - This delay only occurs once on initial render\n\n### General Limitations\n\n- **Multiple Edges**: The library does not currently support multiple edges between the same pair of\n  nodes\n- **Edge Labels**: Built-in edge label support is not yet implemented (can be added via custom\n  `edgeContent`)\n- **Large Graphs**: Performance with graphs \u003e100 nodes has not been extensively tested\n\n### API Stability\n\nAs an alpha release, the public API may change between versions. Breaking changes will be noted in\nthe changelog.\n\n## Contributing\n\nContributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:\n- Reporting bugs and requesting features\n- Contributing code and submitting pull requests\n- Development setup and testing\n\n## Why \"Kuiver\"?\n\nIn mathematics, a [*quiver*](https://en.wikipedia.org/wiki/Quiver_(mathematics)) is a directed graph\nin its most general sense.\n\n\"K\" instead of \"Q\" for Kotlin. Just pronounce it like quiver: `/ˈkwɪvər/`\n\nFrom Wikipedia:\n\n\u003e \"a quiver is another name for a multidigraph; that is, a directed graph where loops and multiple\n\u003e arrows between two vertices are allowed.\"\n\nTechnically this library is not quite a \"true\" quiver, as it doesn't support multiple edges between\nthe same two nodes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustdeko%2Fkuiver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjustdeko%2Fkuiver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustdeko%2Fkuiver/lists"}