@aarpaardev/stix-visualizer
Version:
STIX Visualizer is a React-based, enhanced version of the OASIS CTI STIX Visualization project. It offers interactive canvas-based rendering of STIX 2.0 bundles with support for custom nodes, links, labels, and complete styling and behavior control.
456 lines (361 loc) • 20.3 kB
Markdown
# 📦 STIX Visualizer
**STIX Visualizer** is a React-based, enhanced version of the [OASIS CTI STIX Visualization project](https://oasis-open.github.io/cti-stix-visualization/). It offers interactive canvas-based rendering of [STIX 2.0](https://oasis-open.github.io/cti-documentation/stix/intro) bundles with support for custom nodes, links, labels, and complete styling and behavior control.
---
## 🚀 Features
- Visualizes STIX 2.x bundles
- Canvas-based performance with directional links and animations
- Fully customizable via props
- Optional legends with configurable positions
- Zoom/hover/click event hooks
- Storybook integration for isolated component previews
---
## 🚧 Try it out / Live Demo
You can interact with the live Storybook demo and explore props such as node interaction and neighbor customization on hover:
**Live preview:**
[Open the demo on Storybook](https://aarpaardev.github.io/stix2-visualizer/?path=/story/props-node-interaction--neighbor-customization-on-hover)
---
<p align="center">
<img width="1024" height="768" alt="STIX Visualizer Preview" src="https://github.com/user-attachments/assets/04409ada-94ab-4239-a3c7-525bfc0d710b" />
</p>
## 📦 Installation
```bash
npm install @aarpaardev/stix-visualizer
# or
yarn add @aarpaardev/stix-visualizer
```
## 🔰 Usage
### 🧩 React (via npm/yarn)
```tsx
import React, { useEffect, useState } from 'react';
import { Stix2Visualizer } from '@aarpaardev/stix-visualizer';
export default function App() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://raw.githubusercontent.com/aarpaardev/stix2-visualizer/main/src/examples/MandiantAPT1Report.json')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <Stix2Visualizer data={data} />;
}
```
### 🌐 HTML (via CDN/UMD)
```html
<!DOCTYPE html>
<html>
<head>
<title>STIX Visualizer Test</title>
<!-- React & ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- UMD Bundle of Stix Visualizer -->
<script src="https://unpkg.com/@aarpaardev/stix-visualizer@latest/dist/index.min.js"></script>
</head>
<body>
<div id="root"></div>
<script>
// Load a STIX2 JSON file
fetch('https://raw.githubusercontent.com/aarpaardev/stix2-visualizer/main/src/examples/MandiantAPT1Report.json')
.then(res => res.json())
.then(json => {
props = { data: json };
const Visualizer = window.AaarPaarDevStixVisualizer.Stix2Visualizer;
ReactDOM.render(React.createElement(Visualizer, props), document.getElementById('root'));
});
</script>
</body>
</html>
```
## 🧩 Props
### `Stix2VisualizerProps`
| Prop | Type | Required | Description |
|--------------------|------------------------|----------|-------------|
| `data` | `StixBundle \| object` | ✅ | A valid [STIX 2.x Bundle JSON object](https://oasis-open.github.io/cti-documentation/stix/intro) that defines the entities and relationships to be visualized. Refer to [this sample STIX bundle](https://oasis-open.github.io/cti-documentation/examples/example_json/apt1.json) for reference. |
| `height` | `number` | ❌ | Height of canvas (default is undefined)|
| `width` | `number` | ❌ | With of canvas (default is undefined)|
| `nodeOptions` | `INodeOptions` | ❌ | Configuration for visual node appearance (e.g., color, radius, interactivity). See table below. |
| `nodeOptions` | `INodeOptions` | ❌ | Configuration for visual node appearance (e.g., color, radius, interactivity). See table below. |
| `linkOptions` | `ILinkOptions` | ❌ | Configuration for link appearance and behavior. See table below. |
| `noiseOptions` | `INoiseOptions` | ❌ | Configuration for noise options (e.g., "object_refs" relations with "Report" object). See table below. |
| `legendOptions` | `ILegendOptions` | ❌ | Options for showing and positioning the legend. See table below. |
| `directionOptions` | `ILinkDirectionOptions`| ❌ | Defines directional indicators on links like arrows and particles. See table below. |
| `linkLabelOptions` | `ILabelOptions` | ❌ | Options for displaying text labels on links. See table below. |
| `nodeLabelOptions` | `ILabelOptions` | ❌ | Options for displaying text labels on nodes. See table below. |
#### `nodeOptions` Props
| Property | Type | Default | Description |
| -------------------- | -------------------------------- | ------------- | ---------------------------------------------------------------------------- |
| `size` | `number` | `12` | Node size in pixels. |
| `disableZoomOnClick` | `boolean` | `false` | Disables zoom behavior on node click. |
| `onHover` | `(node, ctx, neighbors) => void` | *(See below)* | Callback for when a node is hovered. Highlights neighboring nodes. |
| `onClick` | `(node, ref?) => void` | `undefined` | Callback for when a node is clicked. You can access the graph ref if needed. |
###### Default `onHover`
```ts
/**
* @param {NodeObject} node Node Object
* @param {CanvasRenderingContext2D} ctx node canvas context
* @param {Set<NodeObject>} neighbors node canvas context
*/
(node: NodeObject, ctx: CanvasRenderingContext2D, neighbors: Set<NodeObject>) => {
Array.from(neighbors.values()).forEach((neighbor: NodeObject) => {
/**
*
* @param {CanvasRenderingContext2D} neighCtx node canvas context
* @param {number} x starting x-axis position of node
* @param {number} y starting y-axis position of node
*/
neighbor.drawHighlight = (
neighCtx: CanvasRenderingContext2D,
x: number,
y: number
): void => {
neighCtx.beginPath();
neighCtx.arc(x, y, 10, 0, Math.PI * 2); // full circle
neighCtx.fillStyle = 'rgba(182, 181, 181, 0.5)';
neighCtx.fill();
neighCtx.stroke();
};
});
node.links?.forEach((link: LinkObject) => {
link.particleWidth = 4;
});
if (node.img && node.x && node.y) {
ctx.drawImage(node.img, node.x - 20 / 2, node.y - 20 / 2, 20, 20);
}
}
```
#### `linkOptions` Props
| Prop | Type | Default | Description |
| -------------------- | ----------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------- |
| `width` | `number` \| `(link: LinkObject) => number` | `1` | Width of the link line or a function to calculate it dynamically. |
| `curvature` | `number` | `0.25` | Controls how curved the link lines are. |
| `distance` | `number` | `60` | Distance between connected nodes. |
| `color` | `string` | `'rgba(126,126,126, 0.6)'` | Stroke color of the link. |
| `disableZoomOnClick` | `boolean` | `false` | Prevents zoom on link click if set to `true`. |
| `onHover` | `(link: LinkObject, ctx: CanvasRenderingContext2D) => void` | *(See below)* | Callback invoked when hovering over a link. |
| `onClick` | `(link: LinkObject, ref?: ReactForceRef) => void` | `undefined` | Callback invoked when clicking a link. |
###### Default `onHover`
```ts
/**
* @param {LinkObject} link link Object
* @param {CanvasRenderingContext2D} ctx node canvas context
*/
(link: LinkObject, ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = 'rgba(36, 35, 35, 0.6)';
ctx.stroke();
/**
*
* @param {CanvasRenderingContext2D} neighCtx node canvas context
* @param {number} x starting x-axis position of node
* @param {number} y starting y-axis position of node
*/
const drawHighlightFunc = (
neighCtx: CanvasRenderingContext2D,
x: number,
y: number
): void => {
neighCtx.beginPath();
neighCtx.arc(x, y, 10, 0, Math.PI * 2); // full circle
neighCtx.fillStyle = 'rgba(182, 181, 181, 0.5)';
neighCtx.fill();
neighCtx.stroke();
};
if (link.source) {
(link.source as NodeObject).drawHighlight = drawHighlightFunc;
}
if (link.target) {
(link.target as NodeObject).drawHighlight = drawHighlightFunc;
}
}
```
#### `noiseOptions` Props
| Prop | Type | Default | Description |
|-----------------|----------------------------|---------------|---------------------------------------------------------------------------------------------|
| `ignoreReportObjectRefs` | `boolean` | `true` | Whether to show the "object_refs" relations to "Report" object or not. |
#### `legendOptions` Props
| Prop | Type | Default | Description |
|-----------------|----------------------------|---------------|---------------------------------------------------------------------------------------------|
| `display` | `boolean` | `true` | Whether to show the legend. |
| `position` | `'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left'` | `'top-right'` | Position of the legend in the container. |
| `containerStyle`| `React.CSSProperties` | `undefined` | Optional custom style object for the legend container.
| `displayignoreReportObjectRefsCheckBox`| `boolean` | `true` | Optional Checkbox to reduce noise by ignoring certain relations (see noiseOption prop). |
#### `nodeLabelOptions` Props
| Prop | Type | Default | Description |
|--------------------|-----------------|--------------------------------|-------------------------------------------------------------------|
| `font` | `string` | `undefined` | Font family to use for labels (e.g., `'Arial'`, `'Roboto'`). |
| `fontSize` | `number` | `4` | Size of the label font. |
| `backgroundColor` | `string` | `undefined` | Optional background color behind the label. |
| `color` | `string` | `'rgba(39, 37, 37, 0.9)'` | Color of the label text. |
| `display` | `boolean` | `true` | Whether to display the label. |
| `onZoomOutDisplay` | `boolean` | `false` | Whether to continue showing the label when zoomed out. |
#### `linkLabelOptions` Props
| Prop | Type | Default | Description |
|--------------------|-----------------|--------------------------------|-------------------------------------------------------------------|
| `font` | `string` | `undefined` | Font family to use for labels (e.g., `'Arial'`, `'Roboto'`). |
| `fontSize` | `number` | `4` | Size of the label font. |
| `backgroundColor` | `string` | `undefined` | Optional background color behind the label. |
| `color` | `string` | `'rgba(39, 37, 37, 0.9)'` | Color of the label text. |
| `display` | `boolean` | `true` | Whether to display the label. |
| `onZoomOutDisplay` | `boolean` | `false` | Whether to continue showing the label when zoomed out. |
#### `directionOptions` Props
| Prop | Type | Default | Description |
|--------------------------------------------|----------------------------------------------|--------------------------------|-----------------------------------------------------------------------------|
| `directionSize` | `number` \| `(link: LinkObject) => number` | `4` | Size of the arrow indicating direction. |
| `arrowRelativePositions` | `number` \| `(link: LinkObject) => number` | `0.98` | Position of the arrow relative to the link length. |
| `directionalParticles` | `number` \| `(link: LinkObject) => number` | `10` | Number of directional particles to display on a link. |
| `directionalParticleSize` | `number` \| `(link: LinkObject) => number` | `1` | Size of directional particles. |
| `directionalParticleSpeed` | `number` \| `(link: LinkObject) => number` | `0.005` | Speed of directional particles. |
| `directionalParticlesAndArrowColor` | `string` \| `(link: LinkObject) => string` | `'rgba(0, 0, 0, 0, 0)'` | Color of both arrows and directional particles. |
| `onHoverParticlesSize` | `number` | `4` | Size of directional particles on hover. |
| `onHoverArrowSize` | `number` | `undefined` | Optional custom arrow size on hover. |
| `displayDirections` | `boolean` | `true` | Whether to display link directions using arrows. |
| `displayParticles` | `boolean` | `true` | Whether to display directional particles along the links (Greater than `0` will cause the canvas to be **continuously redrawn** to simulate particle motion). |
## 📚 Storybook
The project includes a [Storybook](https://storybook.js.org/) setup for developing, testing, and showcasing components in isolation.
### 🔧 Run Storybook locally
To start the Storybook server:
```bash
npm run storybook
```
This will launch Storybook at:
```
http://localhost:6006
```
You can visually explore all customizable props, states, and interactions of the Stix2Visualizer component from there.
## 🧱 Interfaces & Types
### `ILabelOptions`
```ts
interface ILabelOptions {
font?: string;
fontSize?: number;
backgroundColor?: string;
color?: string;
display?: boolean;
onZoomOutDisplay?: boolean;
}
```
Used to style and control label rendering for nodes and links.
### `INodeOptions`
```ts
interface INodeOptions {
size?: number;
disableZoomOnClick?: boolean;
onHover?: (
node: NodeObject,
ctx: CanvasRenderingContext2D,
highlightedNeighbors: Set<NodeObject>
) => void;
onClick?: (node: NodeObject, ref?: ReactForceRef) => void;
}
```
Controls how nodes behave, appear, and respond to interaction.
### `ILinkOptions`
```ts
interface ILinkOptions {
width?: ((link: LinkObject) => number) | number;
curvature?: number;
distance?: number;
color?: string;
disableZoomOnClick?: boolean;
onHover?: (link: LinkObject, ctx: CanvasRenderingContext2D) => void;
onClick?: (link: LinkObject, ref?: ReactForceRef) => void;
}
```
Defines link styling, interaction behavior, and rendering logic.
### `ILinkDirectionOptions`
```ts
interface ILinkDirectionOptions {
directionSize?: ((link: LinkObject) => number) | number;
arrowRelativePositions?: ((link: LinkObject) => number) | number;
directionalParticles?: ((link: LinkObject) => number) | number;
directionalParticleSpeed?: ((link: LinkObject) => number) | number;
directionalParticleSize?: ((link: LinkObject) => number) | number;
directionalParticlesAndArrowColor?: ((link: LinkObject) => string) | string;
onHoverParticlesSize?: number;
onHoverArrowSize?: number;
displayDirections?: boolean;
displayParticles?: boolean;
}
```
Configures directional arrows and animated particles on links.
> ⚠️ **Performance Note**
> If `directionalParticles` is greater than `0`, the canvas will be **continuously redrawn** to simulate particle motion.
> This may **impact performance** on large graphs. Use this option **with caution**.
### `ILegendOptions`
```ts
type LegendPosition =
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'top-center'
| 'bottom-center';
interface ILegendOptions {
display?: boolean;
position?: LegendPosition;
containerStyle?: React.CSSProperties;
}
```
### `NodeObject`
```ts
type NodeObject<NodeType = object> = NodeType & {
id: string | number;
img?: HTMLImageElement;
size?: number;
name?: string;
val?: number;
x?: number;
y?: number;
z?: number;
vx?: number;
vy?: number;
vz?: number;
fx?: number;
fy?: number;
fz?: number;
draw?: (ctx: CanvasRenderingContext2D, x: number, y: number) => void;
drawHighlight?: (ctx: CanvasRenderingContext2D, x: number, y: number) => void;
neighbors?: Array<NodeObject>;
links?: Array<LinkObject>;
[others: string]: unknown;
};
```
Represents a node in the visualizer. This is a generic structure and can be extended with additional fields as needed.
### `LinkObject`
```ts
type LinkObject<NodeType = object, LinkType = object> = LinkType & {
source?: string | number | NodeObject<NodeType>;
target?: string | number | NodeObject<NodeType>;
drawHighlight?: (ctx: CanvasRenderingContext2D) => void;
particleWidth?: number;
color?: string;
[others: string]: unknown;
};
```
Defines a connection between two nodes. Can be enriched with custom properties.
## 🛠 Development
Follow these steps to set up the project locally for development:
### 🔧 Prerequisites
- [Node.js](https://nodejs.org/) (version 20.10.0 or above recommended)
- npm or yarn
### 📥 Clone the repository and run
```bash
git clone https://github.com/your-org/stix-visualizer.git
cd stix-visualizer
npm run storybook
```
## 🤝 Contributing
Contributions are welcome!
- Fork the repository
- Create your feature branch:
```bash
git checkout -b feature/amazing-feature
```
- Commit your changes:
```bash
git commit -m 'Add amazing feature'
```
- Push to the branch:
```bash
git push origin feature/amazing-feature
```
- Open Pull Request