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| CoreCore 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| UserPlatform-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 propagationVue 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 propagationVue 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 dataReact 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| ReactContextEvent 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 --> CoreVue 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 --> CoreVue 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| CoreReact 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| SkipThe 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:
- Pure TypeScript Core: No framework dependencies in the core engine
- Adapter Pattern: Thin wrappers translate between framework conventions and core APIs
- Reactivity Bridging:
- Vue 2:
setReactiveData()withVue.observable() - Vue 3:
setReactiveDataVue3()withreactive()andmarkRaw() - React:
setUpdateViewHook()with manual force update
- Vue 2:
- Event Hook System:
setEventEmitHook()bridges core events to framework event systems - Dependency Injection: Each platform uses its native pattern (provide/inject, Context API)
- Minimal Platform Detection: Single
isReactflag 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.