{"id":44022868,"url":"https://github.com/spren9er/cactuz","last_synced_at":"2026-02-20T11:00:58.812Z","repository":{"id":336279518,"uuid":"1149027295","full_name":"spren9er/cactuz","owner":"spren9er","description":"Cactus Tree \u0026 Hierarchical Edge Bundling","archived":false,"fork":false,"pushed_at":"2026-02-16T20:25:41.000Z","size":5138,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-17T01:34:19.981Z","etag":null,"topics":["cactus-tree","chart","data-visualization","hierarchical-data","hierarchical-edge-bundling","svelte"],"latest_commit_sha":null,"homepage":"https://cactuz.spren9er.de","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spren9er.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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":"2026-02-03T16:33:56.000Z","updated_at":"2026-02-16T20:25:44.000Z","dependencies_parsed_at":"2026-02-10T22:01:24.919Z","dependency_job_id":null,"html_url":"https://github.com/spren9er/cactuz","commit_stats":null,"previous_names":["spren9er/cactus","spren9er/cactus-tree"],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/spren9er/cactuz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spren9er%2Fcactuz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spren9er%2Fcactuz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spren9er%2Fcactuz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spren9er%2Fcactuz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spren9er","download_url":"https://codeload.github.com/spren9er/cactuz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spren9er%2Fcactuz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29648421,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T09:27:29.698Z","status":"ssl_error","status_checked_at":"2026-02-20T09:26:12.373Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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":["cactus-tree","chart","data-visualization","hierarchical-data","hierarchical-edge-bundling","svelte"],"created_at":"2026-02-07T17:06:14.794Z","updated_at":"2026-02-20T11:00:58.796Z","avatar_url":"https://github.com/spren9er.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cactuz\n\nA JavaScript library for visualizing hierarchical data structures using the *CactusTree* algorithm with hierarchical edge bundling.\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/spren9er/cactuz/blob/main/docs/images/cactuz.png?raw=true\" alt=\"cactus-tree\" width=\"75%\" height=\"75%\"\u003e\n\u003c/div\u003e\n\n## Overview\n\nThe library **cactuz** is based on the research paper *[CactusTree: A Tree Drawing Approach for Hierarchical Edge Bundling](https://ieeexplore.ieee.org/document/8031596)* by Tommy Dang and Angus Forbes. It provides a framework-agnostic `CactusTree` class for rendering interactive tree visualizations on an HTML canvas, as well as a low-level `CactusLayout` class for computing the layout independently.\n\nSee [cactuz.spren9er.de](https://cactuz.spren9er.de) for a live demo and interactive playground.\n\n## Features\n\n- **Framework-Agnostic** - Works with any JavaScript framework or plain HTML\n- **Fractal-based Tree Layout** - Recursively stacks child nodes on parent nodes\n- **Hierarchical Edge Bundling** - Groups related connections for cleaner visualization\n- **Highly Customizable** - Extensive styling and behavior options\n- **Interactive** - Pan, zoom, hover effects, and edge filtering\n- **Depth-based Styling** - Configure appearance for different tree levels\n\n## Installation\n\n```bash\nnpm install cactuz\n```\n\nThe package provides two entry points:\n\n- **`cactuz`** — Exports `CactusTree`, `CactusLayout`, and the `Cactus` Svelte component.\n- **`cactuz/core`** — Exports `CactusTree` and `CactusLayout` without the Svelte component. Use this when you don't need the Svelte wrapper or want to avoid a Svelte peer dependency.\n\n## Quick Start\n\n```javascript\nimport { CactusTree } from 'cactuz';\n\nconst canvas = document.getElementById('my-canvas');\n\nconst nodes = [\n  { id: 'root', name: 'Root', parent: null },\n  { id: 'child1', name: 'Child 1', parent: 'root' },\n  { id: 'child2', name: 'Child 2', parent: 'root' },\n  { id: 'leaf1', name: 'Leaf 1', parent: 'child1' },\n  { id: 'leaf2', name: 'Leaf 2', parent: 'child1' },\n];\n\nconst edges = [{ source: 'leaf1', target: 'leaf2' }];\n\nconst tree = new CactusTree(canvas, {\n  width: 800,\n  height: 600,\n  nodes,\n  edges,\n});\n```\n\n## API Reference\n\n### CactusTree\n\nThe `CactusTree` class manages canvas setup, layout computation, rendering, and mouse/touch interactions.\n\n#### Constructor\n\n```javascript\nnew CactusTree(canvas, config)\n```\n\n| Parameter | Type | Description |\n| --------- | ---- | ----------- |\n| `canvas` | `HTMLCanvasElement` | The canvas element to render into |\n| `config` | `object` | Configuration (see below) |\n\n#### Config\n\n| Property      | Type      | Required | Default | Description                        |\n| ------------- | --------- |:--------:|:-------:| ---------------------------------- |\n| `width`       | `number`  | yes      | -       | Canvas width in pixels             |\n| `height`      | `number`  | yes      | -       | Canvas height in pixels            |\n| `nodes`       | `Node[]`  | yes      | -       | Array of hierarchical nodes        |\n| `edges`       | `Edge[]`  | no       | `[]`    | Array of connections between nodes |\n| `options`     | `Options` | no       | `{}`    | Layout and behavior configuration  |\n| `styles`      | `Styles`  | no       | `{}`    | Visual styling configuration       |\n| `pannable`    | `boolean` | no       | `true`  | Enable pan interaction             |\n| `zoomable`    | `boolean` | no       | `true`  | Enable zoom interaction            |\n| `collapsible` | `boolean` | no       | `true`  | Enable collapse/expand on click    |\n\n#### Methods\n\n##### `update(config)`\n\nUpdate any subset of the config properties. Triggers a full re-render.\n\n```javascript\ntree.update({ nodes: newNodes, edges: newEdges });\ntree.update({ options: { zoom: 1.5 } });\ntree.update({ styles: myStyles });\n```\n\n##### `render()`\n\nForce a full render (layout recalculation + draw).\n\n##### `draw()`\n\nForce a lightweight redraw without layout recalculation.\n\n##### `destroy()`\n\nRemove event listeners and cancel pending animation frames. Call this when removing the canvas from the DOM.\n\n```javascript\ntree.destroy();\n```\n\n#### Node Structure\n\n```typescript\ninterface Node {\n  id: string | number;        // Unique identifier\n  name: string;               // Display name\n  parent: string | number | null; // Parent node ID\n  weight?: number;            // Optional explicit weight\n}\n```\n\n#### Edge Structure\n\n```typescript\ninterface Edge {\n  source: string;             // Source node ID\n  target: string;             // Target node ID\n}\n```\n\n#### Options\n\n```typescript\ninterface Options {\n  overlap?: number;           // Node overlap factor (-inf to 1, default: 0.5)\n  arcSpan?: number;           // Arc span in radians (default: 5π/4)\n  sizeGrowthRate?: number;    // Size growth rate (default: 0.75)\n  orientation?: number;       // Root orientation in radians (default: π/2)\n  zoom?: number;              // Layout zoom factor (default: 1.0)\n  numLabels?: number;         // Number of labels (default: 20)\n  collapseDuration?: number;  // Collapse/expand animation duration in ms (default: 300)\n  edges?: EdgeOptions;        // Edge-specific options\n}\n\ninterface EdgeOptions {\n  bundlingStrength?: number;  // Edge bundling strength (0..1, default: 0.97)\n  filterMode?: 'hide' | 'mute'; // Hover behavior when over a leaf:\n                              // 'hide' hides unrelated edges\n                              // 'mute' shows them at reduced opacity\n                              // (default: 'mute')\n  muteOpacity?: number;       // When filterMode is 'mute', multiplier applied \n                              // to unrelated edges (0..1, default: 0.1)\n}\n```\n\n**Notes** \n\n1. Negative values for `overlap` create gaps and nodes are connected with links.\n2. The `edges` option controls hierarchical edge bundling behavior. `bundlingStrength` determines how tightly edges are bundled along shared hierarchical paths — a value of `0` draws straight lines between nodes, while `1` routes edges fully along the hierarchy. When hovering over leaf nodes, edges connected to that node are highlighted, while all other edges are hidden or muted (depending on `filterMode`). This allows for better readability in dense visualizations. `'hide'` removes unrelated edges entirely, while `'mute'` renders them at reduced opacity controlled by `muteOpacity`.\n\n#### Styles\n\nThe `styles` config is a nested object with optional groups and an optional `depths` array containing per-depth overrides. Per-depth overrides take precedence over global group values.\n\n```typescript\ninterface Styles {\n  node?: {\n    fillColor?: string;\n    fillOpacity?: number;\n    strokeColor?: string;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  edge?: {\n    strokeColor?: string;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  edgeNode?: {\n    fillColor?: string;\n    fillOpacity?: number;\n    strokeColor?: string;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  label?: {\n    inner: {\n      textColor?: string;\n      textOpacity?: number;\n      fontFamily?: string;\n      fontWeight?: string;\n      minFontSize?: number;\n      maxFontSize?: number;\n    },\n    outer: {\n      textColor?: string;\n      textOpacity?: number;\n      fontFamily?: string;\n      fontWeight?: string;\n      fontSize?: number;\n      padding?: number;\n      link?: {\n        strokeColor?: string;\n        strokeOpacity?: number;\n        strokeWidth?: number;\n        padding?: number;\n        length?: number;\n      };\n    };\n  };\n  link?: {\n    strokeColor?: string;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  highlight?: {\n    node?: {\n      fillColor?: string;\n      fillOpacity?: number;\n      strokeColor?: string;\n      strokeOpacity?: number;\n      strokeWidth?: number;\n    };\n    edge?: {\n      strokeColor?: string;\n      strokeOpacity?: number;\n      strokeWidth?: number;\n    };\n    edgeNode?: {\n      fillColor?: string;\n      fillOpacity?: number;\n      strokeColor?: string;\n      strokeOpacity?: number;\n      strokeWidth?: number;\n    };\n    label?: {\n      inner: {\n        textColor?: string;\n        textOpacity?: number;\n        fontWeight?: string;\n      };\n      outer: {\n        textColor?: string;\n        textOpacity?: number;\n        fontWeight?: string;\n        link?: {\n          strokeColor?: string;\n          strokeOpacity?: number;\n          strokeWidth?: number;\n        };\n      };\n    };\n  };\n  depths?: DepthStyle[]; // Per-depth overrides\n}\n```\n\n#### Depth-Specific Styling\n\nDepth-specific styles allow you to customize the appearance of nodes, labels, links, etc. based on their depth in the tree hierarchy. This is configured through the `styles.depths` array.\n\n```typescript\ninterface ColorScale {\n  scale: string;              // d3-scale-chromatic sequential scale name\n                              // (e.g. 'magma', 'viridis')\n  reverse?: boolean;          // Reverse the scale direction (default: false)\n}\n\ninterface DepthStyle {\n  depth: number | '*';        // Depth level, or '*' for wildcard (all depths)\n  node?: {\n    fillColor?: string | ColorScale;\n    fillOpacity?: number;\n    strokeColor?: string | ColorScale;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  label?: {\n    inner: {\n      textColor?: string;\n      textOpacity?: number;\n      fontFamily?: string;\n      fontWeight?: string;\n      minFontSize?: number;\n      maxFontSize?: number;\n    },\n    outer: {\n      textColor?: string;\n      textOpacity?: number;\n      fontFamily?: string;\n      fontWeight?: string;\n      fontSize?: number;\n      padding?: number;\n      link?: {\n        strokeColor?: string;\n        strokeOpacity?: number;\n        strokeWidth?: number;\n        padding?: number;\n        length?: number;\n      };\n    };\n  };\n  link?: {\n    strokeColor?: string;\n    strokeOpacity?: number;\n    strokeWidth?: number;\n  };\n  highlight?: {\n    node?: {\n      fillColor?: string;\n      fillOpacity?: number;\n      strokeColor?: string;\n      strokeOpacity?: number;\n      strokeWidth?: number;\n    };\n    label?: {\n      inner: {\n        textColor?: string;\n        textOpacity?: number;\n        fontWeight?: string;\n      };\n      outer: {\n        textColor?: string;\n        textOpacity?: number;\n        fontWeight?: string;\n        link?: {\n          strokeColor?: string;\n          strokeOpacity?: number;\n          strokeWidth?: number;\n        };\n      };\n    };\n  }  \n}\n```\n\nEach item in `styles.depths` must include a `depth` integer (or a wildcard `'*'`; see below). The node with `depth` 0 is the root of the tree.\n\nPositive integers (1, 2, 3, ...) refer to deeper levels away from the root:\n  - 1 = direct children of the root,\n  - 2 = grandchildren of the root,\n  - and so on.\n\nUse positive-depth overrides to style internal levels progressively (for example, a different node color for level 2).\n\nNegative integers (-1, -2, -3, ...) are supported as a convenience for leaf-oriented overrides:\n  - -1 = set of all leaves (nodes with no children),\n  - -2 = set of parents of leaves (one level up from leaves),\n  - and so on.\n\nThese negative-depth entries are not absolute numeric depths in the tree; instead the implementation maps them to groups of nodes computed from the layout. This is useful when you want to style leaves or near-leaf levels without knowing their positive depth value.\n\nDepth-based styles are applied in the natural order given in the `depths` array. When multiple entries match a node, later entries override earlier ones. This means the order you define entries in determines their precedence.\n\n#### Wildcard Depth Styling\n\nSetting `depth` to `'*'` applies a style to every depth level in the tree. When combined with `ColorScale` objects for `fillColor` and/or `strokeColor`, it automatically samples colors from a [d3-scale-chromatic](https://d3js.org/d3-scale-chromatic) sequential color scale. The number of sampled colors equals the _tree depth + 1_, evenly distributed across the scale from 0 to 1.\n\nAll d3 sequential scales are supported: `magma`, `viridis`, `inferno`, `plasma`, `blues`, `greens`, `reds`, `turbo`, `cividis`, `warm`, `cool`, and more.\n\nWildcard entries are expanded in place within the `depths` array. Entries that appear later in the array override earlier ones, so placing a wildcard before explicit numeric depth entries allows you to define a color gradient across the entire tree and still customize individual levels.\n\n```javascript\nconst tree = new CactusTree(canvas, {\n  width: 800,\n  height: 600,\n  nodes,\n  edges,\n  styles: {\n    depths: [\n      // Apply a Magma color gradient across all depths\n      {\n        depth: '*',\n        node: {\n          fillColor: { scale: 'magma', reverse: true },\n          strokeColor: { scale: 'magma', reverse: true },\n        },\n      },\n      // Override depth 0 (root) with a specific color\n      {\n        depth: 0,\n        node: { fillColor: '#2c3e50', strokeColor: '#ecf0f1' },\n      },\n    ],\n  },\n});\n```\n\n### CactusLayout\n\nFor use cases where you only need the layout computation (e.g. rendering with a different graphics library), the `CactusLayout` class provides the positioning algorithm without any canvas or interaction management.\n\n#### Constructor\n\n```javascript\nimport { CactusLayout } from 'cactuz';\n\nnew CactusLayout(\n  width,           // Target width\n  height,          // Target height\n  zoom,            // Zoom factor (default: 1)\n  overlap,         // Overlap factor (default: 0)\n  arcSpan,         // Arc span in radians (default: π)\n  sizeGrowthRate   // Size growth rate (default: 0.75)\n)\n```\n\n#### Methods\n\n##### `render(nodes, startX, startY, startAngle)`\n\nComputes the layout and returns positioned node data.\n\n**Parameters:**\n\n- `nodes`: Array of node objects (flat array with `id`, `name`, `parent`)\n- `startX`: Starting X coordinate (usually `width / 2`)\n- `startY`: Starting Y coordinate (usually `height / 2`)\n- `startAngle`: Starting angle in radians (default: `Math.PI / 2`)\n\n**Returns:**\n\n```typescript\ninterface NodeData {\n  x: number;       // X coordinate\n  y: number;       // Y coordinate\n  radius: number;  // Node radius\n  node: Node;      // Original node reference\n  isLeaf: boolean; // Whether this is a leaf node\n  depth: number;   // Depth in hierarchy (0 = root)\n  angle: number;   // Angle from parent (radians)\n}\n```\n\n**Example:**\n\n```javascript\nimport { CactusLayout } from 'cactuz';\n\nconst layout = new CactusLayout(800, 600, 1.0, 0.5, Math.PI, 0.75);\nconst nodeData = layout.render(nodes, 400, 300, Math.PI / 2);\n\n// Use nodeData to render with your own graphics library\nfor (const nd of nodeData) {\n  console.log(nd.x, nd.y, nd.radius, nd.node.name);\n}\n```\n\n## Advanced Usage\n\n### Styling Example\n\n```javascript\nconst tree = new CactusTree(canvas, {\n  width: 800,\n  height: 600,\n  nodes,\n  edges,\n  styles: {\n    node: {\n      fillOpacity: 0.97,\n      strokeColor: '#333333',\n      strokeWidth: 0.5,\n    },\n    edge: {\n      strokeColor: '#ffffff',\n    },\n    edgeNode: {\n      fillOpacity: 0.97,\n      strokeOpacity: 1,\n    },\n    link: {\n      strokeColor: '#333333',\n    },\n    label: {\n      inner: {\n        textColor: '#efefef',\n        fontFamily: 'monospace',\n        minFontSize: 9,\n        maxFontSize: 14,\n      },\n      outer: {\n        textColor: '#6B7280',\n        textOpacity: 1,\n        fontFamily: 'monospace',\n        fontSize: 9,\n        link: {\n          strokeColor: '#cccccc',\n        },\n      },\n    },\n    highlight: {\n      node: {\n        fillOpacity: 0.97,\n        strokeOpacity: 1,\n      },\n      edgeNode: {\n        fillColor: '#efefef',\n        strokeColor: '#333333',\n        strokeWidth: 1,\n      },\n    },\n    depths: [\n      {\n        depth: '*',\n        node: {\n          fillColor: { scale: 'magma' },\n        },\n      },\n      {\n        depth: -1,\n        node: {\n          fillOpacity: 0.75,\n          strokeOpacity: 0.75,\n        },\n        label: {\n          inner: {\n            textColor: '#333333',\n          },\n        },\n        highlight: {\n          node: {\n            strokeWidth: 2,\n          },\n          label: {\n            inner: {\n              textColor: '#333333',\n            },\n          },\n        },\n      },\n    ],\n  },\n});\n```\n\n### Negative Overlap\n\n```javascript\nconst tree = new CactusTree(canvas, {\n  width: 800,\n  height: 600,\n  nodes,\n  options: {\n    overlap: -1.1,                  // Gaps between nodes\n    arcSpan: 2 * Math.PI,           // Full circle layout (radians)\n    orientation: (7 / 9) * Math.PI, // Root orientation (radians)\n    zoom: 0.7,\n  },\n});\n```\n\nFor a negative overlap parameter, nodes are connected by links (see top-level `link` for styling).\n\n## Svelte Component\n\nFor Svelte applications, **cactuz** also exports a ready-to-use `Cactus` Svelte component that wraps the core class with reactive prop handling.\n\n```svelte\n\u003cscript\u003e\n  import { Cactus } from 'cactuz';\n\n  const nodes = [\n    { id: 'root', name: 'Root', parent: null },\n    { id: 'child1', name: 'Child 1', parent: 'root' },\n    { id: 'child2', name: 'Child 2', parent: 'root' },\n    { id: 'leaf1', name: 'Leaf 1', parent: 'child1' },\n    { id: 'leaf2', name: 'Leaf 2', parent: 'child1' },\n  ];\n\n  const edges = [{ source: 'leaf1', target: 'leaf2' }];\n\u003c/script\u003e\n\n\u003cCactus width={800} height={600} {nodes} {edges} /\u003e\n```\n\nThe component accepts the same props as the `CactusTree` config: `width`, `height`, `nodes`, `edges`, `options`, `styles`, `pannable`, `zoomable`, and `collapsible`. It automatically re-renders when any prop changes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspren9er%2Fcactuz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspren9er%2Fcactuz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspren9er%2Fcactuz/lists"}