JavaScript is required

Multi-Platform Architecture

Purpose and Scope

This document explains the multi-platform architecture of relation-graph, specifically how a single framework-agnostic core engine integrates with Vue 2, Vue 3, React, Svelte, and Web Components through adapter components. It covers the shared core pattern, data provider specialization, component export structure, reactivity integration strategies, and event bridging mechanisms that enable consistent functionality across different UI frameworks.

For information about the core class hierarchy, see Class Inheritance Hierarchy. For detailed platform-specific integration, see Vue 2 & Vue 3 Integration, React Integration, Svelte Integration, and Web Components.


Framework-Agnostic Core Design

The relation-graph architecture achieves multi-platform support by separating the graph logic engine from UI framework concerns. The core engine (RelationGraphFinal and its inheritance chain) is written in pure TypeScript with zero dependencies on any UI framework.

Core Independence Strategy

graph TB
    subgraph "Core Package: relation-graph-models"
        Core["RelationGraphFinal
(Framework-Agnostic Core)"] Base["RelationGraphBase
Event System Foundation"] Layouts["Layout Algorithms
RGForceLayout, RGTreeLayout, etc"] Utils["Utility Classes
RGGraphMath, RGNodesAnalytic"] end subgraph "Platform Adapters" Vue2["Vue 2 Adapter
packages/platforms/vue2"] Vue3["Vue 3 Adapter
packages/platforms/vue3"] React["React Adapter
packages/platforms/react"] Svelte["Svelte Adapter
packages/platforms/svelte"] WC["Web Components
packages/platforms/web-components"] end Core --> Base Core --> Layouts Core --> Utils Vue2 -->|"instantiates + setReactiveData"| Core Vue3 -->|"instantiates + setReactiveDataVue3"| Core React -->|"instantiates + setUpdateViewHook"| Core Svelte -->|"instantiates + RGDataProvider4Svelte"| Core WC -->|"built from Svelte"| Svelte Vue2 -->|setEventEmitHook| Core Vue3 -->|setEventEmitHook| Core React -->|Context Provider| Core Svelte -->|RGHooks| Core

Core Independence Characteristics:

Aspect Implementation
No Framework Imports Core models never import Vue/React/Svelte libraries
Hook-Based Integration Core exposes setUpdateViewHook() and setEventEmitHook()
Platform Detection isReact flag enables conditional behavior
Data Separation Core operates on plain objects, not reactive proxies
Shared Layouts All platforms use identical layout algorithms
Shared Utilities All platforms share RGGraphMath, RGNodesAnalytic, etc.

Package Structure and Distribution

The codebase maintains separate packages for each platform while sharing the core engine. Each platform is published as an independent npm package with synchronized versioning.

NPM Package Distribution

Platform NPM Package Source Directory Build Output
Vue 2 @relation-graph/vue2 packages/platforms/vue2 lib/vue2/
Vue 3 @relation-graph/vue packages/platforms/vue3 lib/vue3/
React @relation-graph/react packages/platforms/react lib/react/
Svelte @relation-graph/svelte packages/platforms/svelte lib/svelte/
Web Components @relation-graph/web-components (built from Svelte) lib/web-components/

All packages share the same version number (currently 3.0.2) defined in the root package.json. The build system synchronizes versions across all platform packages during the publish process.


Data Provider Specialization

Each platform requires a specialized data provider to bridge the core’s data structures with the framework’s reactivity system. These data providers extend the base RGDataProvider class.

Data Provider Hierarchy

graph TB
    Base["RGDataProvider
(Base Class)
packages/relation-graph-models"] Vue2Provider["RGDataProvider4Vue2
Uses Vue.set() for reactivity"] Vue3Provider["RGDataProvider4Vue3
Uses reactive() and ref()"] ReactProvider["RGDataProvider4React
Triggers updateViewHook()"] SvelteProvider["RGDataProvider4Svelte
Uses writable() stores"] Base --> Vue2Provider Base --> Vue3Provider Base --> ReactProvider Base --> SvelteProvider Vue2Provider -->|"exported in"| Vue2Index["packages/platforms/vue2/src/index.ts"] Vue3Provider -->|"exported in"| Vue3Index["packages/platforms/vue3/src/index.ts"] ReactProvider -->|"exported in"| ReactIndex["packages/platforms/react/src/index.ts"] SvelteProvider -->|"exported in"| SvelteIndex["packages/platforms/svelte/src/index.ts"]

Platform-Specific Data Provider Features

Platform Data Provider Key Methods Reactivity Mechanism
Vue 2 Framework handles via Vue.observable() Direct mutation Automatic via Vue 2 reactivity
Vue 3 Framework handles via reactive() Direct mutation Automatic via Vue 3 reactivity
React Via hooks in core updateViewHook() Manual force update
Svelte RGDataProvider4Svelte Store mutations Svelte stores (writable())

The data provider specialization ensures that data mutations in the core engine properly trigger UI updates in each framework’s idiomatic way.


The Adapter Pattern

Each platform implements an adapter component that wraps the core engine and translates between the framework’s conventions and the core’s APIs.

Adapter Component Responsibilities

graph LR
    User["User Application"]
    Adapter["Platform Adapter
Vue2/Vue3/React Component"] Core["RelationGraphFinal
Core Engine"] subgraph "Adapter Responsibilities" A1["1. Instantiate Core"] A2["2. Bridge Reactivity"] A3["3. Map Events"] A4["4. Manage Lifecycle"] A5["5. Provide DOM Refs"] end User -->|props/options| Adapter Adapter --> A1 Adapter --> A2 Adapter --> A3 Adapter --> A4 Adapter --> A5 A1 --> Core A2 --> Core A3 --> Core A4 --> Core A5 --> Core Core -->|methods/state| Adapter Adapter -->|reactive updates| User

Platform-Specific Adapter Files

Platform Adapter Component Core Instantiation Build Tool
Vue 2 packages/platforms/vue2/src/core4vue/RelationGraph.vue Line 255 Vite
Vue 3 packages/platforms/vue3/src/relation-graph/src/core4vue3/RelationGraph.vue Line 77 Vite
React packages/platforms/react/src/relation-graph/RelationGraph.tsx Via Context Provider Rollup
Svelte packages/platforms/svelte/src/relation-graph/RelationGraph.svelte Direct instantiation Vite
Web Components (compiled from Svelte) Via Svelte build Vite

Reactivity Integration Per Platform

The core challenge in multi-platform support is bridging each framework’s reactivity system with the core engine’s data structures. Each platform uses different mechanisms to detect and propagate data changes.

Vue 2 Reactivity Integration

Vue 2 uses Vue.observable() to create reactive data objects that the core engine can mutate.

sequenceDiagram
    participant Adapter as "Vue2 Adapter
index.vue" participant Core as "RelationGraphFinal" participant Data as "Reactive Data
Vue.observable()" participant DOM as "Vue Template" Adapter->>Data: Create reactive objects
graphData, graph Adapter->>Core: new RelationGraphFinal(options, listeners) Adapter->>Core: setReactiveData(graphData, graph) Note over Core: Core stores references
to reactive objects Core->>Data: Mutate graphData.nodes[...] Data-->>DOM: Automatic re-render Core->>Adapter: $emit(eventName, args) Adapter->>DOM: Event propagation

Vue 2 Implementation Details:

// Data initialization with Vue 2 reactivity
data() {
    return {
        graphData: {
            rootNode: null,
            nodes: [],
            links: [],
            elementLines: []
        },
        graph: {
            options: createDefaultConfig({}),
            allLineColors: []
        }
    };
}

The core engine receives these reactive objects via setReactiveData() and mutates them directly, triggering Vue 2’s reactivity system automatically.


Vue 3 Reactivity Integration

Vue 3 uses reactive() for objects and wraps the core instance in markRaw() to prevent reactive conversion of the core engine itself.

sequenceDiagram
    participant Adapter as "Vue3 Adapter
index.vue" participant Core as "RelationGraphFinal" participant Data as "Reactive Data
reactive()" participant Instance as "markRaw(instance)" participant DOM as "Vue Template" Adapter->>Data: reactive(graphData) Adapter->>Data: reactive(graph) Adapter->>Core: new RelationGraphFinal(options, listeners) Adapter->>Instance: markRaw(rgInstance) Note over Instance: Prevents Vue from
wrapping core methods Adapter->>Core: setReactiveDataVue3(graphData, graph) Core->>Data: Mutate graphData.nodes[...] Data-->>DOM: Automatic re-render Core->>Adapter: emit(eventName, args) Adapter->>DOM: Event propagation

Vue 3 Implementation Details:

// Reactive data objects
const graphData = reactive<RGGraphData>({
    rootNode: undefined,
    nodes: [],
    links: [],
    elementLines: []
});

const graph = reactive<RGGraphReactiveData>({
    instance: undefined,
    options: createDefaultConfig({}),
    allLineColors: []
});

// Core instance wrapped in markRaw to prevent reactivity
graph.instance = markRaw(rgInstance);

The markRaw() wrapper is critical because Vue 3’s reactivity system would otherwise wrap all core engine methods in reactive proxies, causing severe performance degradation and breaking method references.


React Integration

React uses the Context API to provide the core instance to child components, with a manual update hook to trigger re-renders.

sequenceDiagram
    participant StoreProvider as "RelationGraphStoreProvider"
    participant Context as "RelationGraphStoreContext"
    participant Adapter as "RelationGraph Component"
    participant Core as "RelationGraphFinal"
    participant Hook as "updateViewHook"
    participant React as "React Re-render"

    StoreProvider->>Core: new RelationGraphFinal(options, listeners)
    StoreProvider->>Hook: setUpdateViewHook(forceUpdate)
    Note over Hook: forceUpdate triggers
component re-render StoreProvider->>Context: Provide core instance Adapter->>Context: useContext(RelationGraphStoreContext) Note over Adapter: Component receives
core instance Core->>Core: Data mutation occurs Core->>Hook: updateViewHook() Hook->>React: Force component update React-->>Adapter: Re-render with new data

React Implementation Details:

The React adapter relies on a setUpdateViewHook() method in the core engine:

// In RelationGraphBase.ts
protected updateViewHook: () => void = () => {
    // do nothing
};

setUpdateViewHook(hook: () => void) {
    this.isReact = true;
    this.updateViewHook = hook;
}

When the core engine needs to trigger a UI update, it calls this.updateViewHook(), which invokes React’s force update mechanism:

protected _doSomethingAfterDataUpdated() {
    devLog('_dataUpdated:', this._dataUpdatingNext);
    this.updateShouldRenderGraphData();
    this.updateEasyView();
    this.updateViewHook(); // Triggers React re-render
}

Event System Bridging

The core engine maintains a three-tier event system that bridges to each framework’s native event mechanism.

Event Flow Architecture

graph TB
    UserAction["User Interaction
Click, Drag, etc."] CoreEvent["Core Event Handler
RelationGraphWith7Event"] EventDispatch["emitEvent()
RelationGraphBase"] subgraph "Three-Tier Event Handling" Tier1["1. Default Handlers
listeners.onNodeClick"] Tier2["2. Custom Handlers
eventHandlers[]"] Tier3["3. Framework Hook
_hook()"] end Vue2Emit["Vue2: $emit(eventName)"] Vue3Emit["Vue3: emit(eventName)"] ReactContext["React: Event props"] UserAction --> CoreEvent CoreEvent --> EventDispatch EventDispatch --> Tier1 EventDispatch --> Tier2 EventDispatch --> Tier3 Tier3 -->|Vue 2| Vue2Emit Tier3 -->|Vue 3| Vue3Emit Tier3 -->|React| ReactContext

Event Hook Installation

Each platform installs its event hook during initialization:

Vue 2:

relationGraph.setEventEmitHook((eventName: RGEventNames, ...eventArgs: any[]) => {
    this.$emit(eventName, ...eventArgs);
});

Vue 3:

rgInstance.setEventEmitHook((eventName: RGEventNames, ...eventArgs: any[]) => {
    emit(eventName, ...eventArgs);
});

React: React passes event listeners directly to the core constructor via the listeners parameter, bypassing the hook system.


Component Dependency Injection

Each platform uses its native dependency injection pattern to provide the core instance to child components.

Vue 2: Provide/Inject with Function

graph TB
    Parent["Vue2 Adapter
index.vue"] Provide["provide()
getGraphInstance: this.getInstance"] Child1["RGCanvas.vue"] Child2["RGNode.vue"] Child3["RGLine.vue"] Inject["inject: ['getGraphInstance']"] Core["RelationGraphFinal
Core Instance"] Parent --> Provide Provide --> Child1 Provide --> Child2 Provide --> Child3 Child1 --> Inject Child2 --> Inject Child3 --> Inject Inject --> Core

Vue 2 Implementation:

provide() {
    return {
        getGraphInstance: this.getInstance
    };
}

methods: {
    getInstance() {
        return this.relationGraph;
    }
}

Vue 3: Provide/Inject with Symbol Keys

graph TB
    Parent["Vue3 Adapter
index.vue"] Symbol["getGraphInstanceKey
Symbol key"] Provide["provide(key, () => graph.instance)"] Child1["RGCanvas.vue"] Child2["RGNode.vue"] Child3["RGLine.vue"] Inject["inject(getGraphInstanceKey)"] Core["RelationGraphFinal
Core Instance"] Parent --> Symbol Symbol --> Provide Provide --> Child1 Provide --> Child2 Provide --> Child3 Child1 --> Inject Child2 --> Inject Child3 --> Inject Inject --> Core

Vue 3 Implementation:

// Using Symbol key for type safety
provide(getGraphInstanceKey, () => graph.instance)

Vue 3 uses Symbol-based keys from the constants file for better type safety and to avoid naming collisions.


React: Context API

graph TB
    Provider["RelationGraphStoreProvider"]
    Context["RelationGraphStoreContext"]
    Core["RelationGraphFinal
Core Instance"] Adapter["RelationGraph.tsx"] Child1["RGCanvas"] Child2["RGNode"] Child3["RGLine"] Hook["useContext()"] Provider -->|creates| Core Provider -->|provides via| Context Context --> Adapter Context --> Child1 Context --> Child2 Context --> Child3 Adapter --> Hook Child1 --> Hook Child2 --> Hook Child3 --> Hook Hook -->|returns| Core

React Implementation:

const RelationGraph: React.FC<RelationGraphCompJsxProps> = (props) => {
    const graphInstance = useContext(RelationGraphStoreContext);
    // graphInstance is the core engine
};

React’s Context API wraps the core instance in a provider at the application level, making it available to all child components via useContext().


Lifecycle Management

Each platform handles component lifecycle differently, but all follow the same core initialization sequence.

Unified Initialization Sequence

sequenceDiagram
    participant Adapter as "Platform Adapter"
    participant Core as "RelationGraphFinal"
    participant Dom as "DOM Reference"
    participant Layout as "Layout System"

    Note over Adapter: Component Mount
    Adapter->>Core: new RelationGraphFinal(options, listeners)
    Adapter->>Core: setReactiveData*() / setUpdateViewHook()
    Adapter->>Core: setEventEmitHook()
    Adapter->>Dom: Get DOM reference
    Adapter->>Core: setDom(domElement)
    Adapter->>Core: ready()
    Note over Core: Initialization sequence
    Core->>Core: initDom()
    Core->>Core: resetViewSize()
    Core->>Core: initImageTool()
    Core->>Core: updateViewBoxInfo()
    Core->>Core: addFullscreenListener()

    Note over Adapter: User calls setJsonData()
    Adapter->>Core: setJsonData(data)
    Core->>Layout: doLayout()
    Core->>Adapter: Trigger UI update

    Note over Adapter: Component Unmount
    Adapter->>Core: beforeUnmount()
    Core->>Core: options.instanceDestroyed = true
    Core->>Core: removeFullscreenListener()

Platform-Specific Lifecycle Hooks

Platform Mount Hook Unmount Hook DOM Reference
Vue 2 mounted() beforeDestroy() this.$refs.seeksRelationGraph
Vue 3 onMounted() onBeforeUnmount() seeksRelationGraph$.value
React useEffect(() => {...}, []) useEffect(() => () => {...}, []) seeksRelationGraph$.current

Platform Detection and Conditional Behavior

The core engine includes minimal platform detection to enable conditional behavior where necessary.

React Detection Flag

graph LR
    SetHook["setUpdateViewHook(hook)"]
    Flag["isReact = true"]
    DataUpdate["_dataUpdated()"]
    Condition{"isReact?"}
    UpdateHook["updateViewHook()"]
    Skip["Skip (Vue handles automatically)"]

    SetHook --> Flag
    DataUpdate --> Condition
    Condition -->|true| UpdateHook
    Condition -->|false| Skip

The isReact flag is set when setUpdateViewHook() is called, enabling the core to trigger React’s manual update mechanism:

setUpdateViewHook(hook: () => void) {
    this.isReact = true;
    this.updateViewHook = hook;
}

This is the only platform-specific conditional in the core. Vue platforms don’t need this because their reactivity systems automatically detect mutations.


Summary: Multi-Platform Strategy

The relation-graph multi-platform architecture achieves framework independence through:

  1. Pure TypeScript Core: No framework dependencies in the core engine
  2. Adapter Pattern: Thin wrappers translate between framework conventions and core APIs
  3. Reactivity Bridging:
    • Vue 2: setReactiveData() with Vue.observable()
    • Vue 3: setReactiveDataVue3() with reactive() and markRaw()
    • React: setUpdateViewHook() with manual force update
  4. Event Hook System: setEventEmitHook() bridges core events to framework event systems
  5. Dependency Injection: Each platform uses its native pattern (provide/inject, Context API)
  6. Minimal Platform Detection: Single isReact flag for conditional update triggering

This architecture enables a single, well-tested core engine to power three different UI frameworks while respecting each framework’s idiomatic patterns.