equilibria-engine
v1.4.1
Published
A framework-agnostic TypeScript library for creating dynamic economic charts with Redux state management and real-time interactivity.
Readme
Equilibria Engine
Version 1.3.0
A framework-agnostic JavaScript library for creating interactive economic visualizations, built on Redux, D3.js, and Math.js. Features a modular architecture with advanced performance optimizations for smooth, responsive charts.
Features
- Interactive Charts: Drag curves, adjust parameters, see live updates
- YAML-Driven: Define complex economic models in simple, declarative YAML
- State Sharing: Generate shareable URLs that preserve chart state
- Educational Focus: Built for teaching economics with scenarios, tutorials, and quizzes
- Accessible: WCAG 2.1 AA compliant with full keyboard navigation and screen reader support
- High Performance:
- 2-5x faster rendering with memoization and selective updates
- Modular architecture with 14 focused modules
- LRU caching for expression evaluation
- Per-chart rendering for multi-chart dashboards
- Rich Visualizations: 11 element types including lines, areas, points, circles, rectangles, segments, angles, brackets, sliders, and reference lines
- Math.js Integration: Powerful expression evaluation with custom functions
New in v1.3.0
- Extended Shapes: Circles, rectangles, segments, and angle markers
- Parametric Curves: Define curves using parametric equations (supports circles, ellipses, and spirals)
- Economics Helpers: Pre-built functions for budget lines, indifference curves, supply/demand, and more
- Enhanced Drag System: Multi-directional dragging with curve constraints
- Range Brackets: Visualize changes (ΔQ, ΔP) and tax wedges on axes
- On-Graph Sliders: Direct parameter manipulation within charts
For Developers: See the AI Context Documentation for detailed architecture and implementation guides.
Installation
Using pnpm (recommended in monorepo)
pnpm add equilibria-engineUsing npm
npm install equilibria-engineUsing yarn
yarn add equilibria-engineQuick Start
1. Basic Setup
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Supply & Demand</title>
<link rel="stylesheet" href="node_modules/equilibria-engine/src/styles/engine.css">
</head>
<body>
<div id="app">
<div id="chart-container"></div>
<div id="controls"></div>
</div>
<script type="module">
import { initEngine, parseYAML, initializeFromYAML } from 'equilibria-engine';
// Initialize the engine
const { store, destroy } = initEngine({
chartContainerId: 'chart-container',
controlsHost: document.getElementById('controls')
});
// Load a model from YAML
fetch('models/supply-demand.yaml')
.then(res => res.text())
.then(yaml => {
const parsed = parseYAML(yaml);
store.dispatch(initializeFromYAML(parsed));
console.log('Model loaded!');
});
</script>
</body>
</html>2. Simple YAML Model
# supply-demand.yaml
metadata:
title: "Market for Coffee"
specVersion: "1.0"
parameters:
demandIntercept:
value: 100
min: 50
max: 150
label: "Demand Intercept"
supplyIntercept:
value: 20
min: 0
max: 50
label: "Supply Intercept"
slope:
value: 2
min: 0.5
max: 5
step: 0.5
charts:
- id: mainChart
title: "Coffee Market"
xAxis:
label: "Quantity"
min: 0
max: 60
yAxis:
label: "Price ($)"
min: 0
max: 120
elements:
- id: demand
type: line
label: "Demand"
equation: "demandIntercept - slope * x"
color: "#2563eb"
draggable: true
- id: supply
type: line
label: "Supply"
equation: "supplyIntercept + slope * x"
color: "#dc2626"Core Concepts
Redux State Management
Equilibria uses Redux for predictable state management:
import { createStore, getState, dispatch } from 'equilibria-engine';
// Create store with initial state
const store = createStore(initialState);
// Get current state
const state = store.getState();
// Dispatch actions
import { setParameter, activateScenario } from 'equilibria-engine';
store.dispatch(setParameter('demandIntercept', 120));
store.dispatch(activateScenario('imposeTax'));State Sharing via URL
Generate shareable links that preserve chart state:
import { serializeState, deserializeState } from 'equilibria-engine';
// Save state to URL
const compressed = serializeState(store.getState());
const url = `${window.location.origin}?state=${compressed}`;
// Restore state from URL
const params = new URLSearchParams(window.location.search);
const compressed = params.get('state');
if (compressed) {
const state = deserializeState(compressed);
store = createStore(state);
}Architecture
Modular Rendering System
The engine features a modular architecture with 14 focused modules for maintainability and performance:
src/engine/
├── rendering/
│ ├── core.ts # Main orchestration
│ ├── ChartRenderer.ts # Per-chart rendering class
│ ├── scales.ts # D3 scale calculation
│ ├── axes.ts # Axes rendering
│ ├── gridlines.ts # Gridlines rendering
│ ├── initialization.ts # Chart setup
│ ├── elements/
│ │ ├── lines.ts # Line rendering
│ │ ├── areas.ts # Area rendering
│ │ ├── points.ts # Point rendering
│ │ └── annotations.ts # Text annotations
│ ├── interactions/
│ │ └── drag.ts # Drag behaviors
│ ├── math/
│ │ ├── evaluator.ts # Expression evaluation (with memoization)
│ │ └── intersections.ts # Curve intersections
│ └── types.ts # TypeScript definitions
├── cache/
│ └── memoization.ts # LRU cache infrastructure
├── chartSubscriptions.ts # Selective rendering
├── reducer.ts # Redux reducer
└── store.ts # Redux storePerformance Optimizations
1. Expression Memoization
- Two-level LRU caching (compiled expressions + results)
- 5.5x speedup for repeated evaluations
- Smart cache key generation
2. Selective Rendering
- Per-chart dirty checking
- Only affected charts re-render
- 2-3x faster multi-chart updates
3. Modular Design
- Tree-shakeable imports
- Independent module testing
- Smaller bundle sizes
Performance Benchmarks
| Operation | Before | After | Improvement | |-----------|--------|-------|-------------| | Parameter update | ~16ms | ~8ms | 2x faster | | Chart re-render | ~60ms | ~30ms | 2x faster | | 4-chart dashboard | ~120ms | ~40ms | 3x faster | | 1000 evaluations | ~45ms | ~8ms | 5.5x faster |
API Reference
Core Functions
initializeEngine(options)
Initialize the Equilibria engine with configuration options.
Parameters:
options.chartContainer(string): CSS selector for chart containeroptions.controlsContainer(string): CSS selector for controls containeroptions.initialState(object, optional): Initial Redux state
Returns: Engine instance with methods: loadYAML(), getState(), dispatch()
serializeState(state)
Compress and encode Redux state for URL sharing.
Parameters:
state(object): Redux state object
Returns: Base64-encoded compressed string
deserializeState(compressed)
Decompress and decode state from URL.
Parameters:
compressed(string): Base64-encoded compressed state
Returns: Redux state object
Redux Actions
import {
setParameter,
setMultipleParameters,
shiftCurve,
toggleElementVisibility,
activateScenario,
deactivateScenario
} from 'equilibria-engine';
// Set parameter value
dispatch(setParameter('price', 100));
// Batch update parameters
dispatch(setMultipleParameters({
tax: 10,
subsidy: 0
}));
// Shift a curve element
dispatch(shiftCurve('demand', { yShift: 10 }));
// Toggle element visibility
dispatch(toggleElementVisibility('supply'));
// Activate scenario
dispatch(activateScenario('imposeTax'));
// Deactivate scenario
dispatch(deactivateScenario('imposeTax'));Integration Guide
Integrating with Your Application
The Equilibria Engine is designed to be framework-agnostic and easy to integrate into any JavaScript application.
Basic Integration Pattern
import { initEngine } from 'equilibria-engine';
// 1. Initialize the engine
const { store, destroy } = initEngine({
chartContainerId: 'my-chart-container',
controlsHost: document.getElementById('controls'),
initialState: null, // or load from YAML
renderConfig: {
width: 800,
height: 600,
margin: { top: 20, right: 20, bottom: 40, left: 50 }
},
useSelectiveRendering: true // Enable for multi-chart performance
});
// 2. Load a model
fetch('/models/my-model.yaml')
.then(res => res.text())
.then(yaml => {
const parsed = parseYAML(yaml);
store.dispatch(initializeFromYAML(parsed));
});
// 3. Clean up when done
window.addEventListener('beforeunload', () => {
destroy();
});React Integration
import { useEffect, useRef } from 'react';
import { initEngine, parseYAML, initializeFromYAML } from 'equilibria-engine';
function EconomicChart({ modelYaml }) {
const chartRef = useRef(null);
const controlsRef = useRef(null);
const engineRef = useRef(null);
useEffect(() => {
// Initialize engine
const { store, destroy } = initEngine({
chartContainerId: chartRef.current,
controlsHost: controlsRef.current,
useSelectiveRendering: true
});
engineRef.current = { store, destroy };
// Load model
if (modelYaml) {
const parsed = parseYAML(modelYaml);
store.dispatch(initializeFromYAML(parsed));
}
// Cleanup
return () => destroy();
}, [modelYaml]);
return (
<div>
<div ref={chartRef} className="chart-container" />
<div ref={controlsRef} className="controls-container" />
</div>
);
}Vue Integration
<template>
<div>
<div ref="chartContainer" class="chart-container"></div>
<div ref="controlsContainer" class="controls-container"></div>
</div>
</template>
<script>
import { initEngine, parseYAML, initializeFromYAML } from 'equilibria-engine';
export default {
props: ['modelYaml'],
mounted() {
const { store, destroy } = initEngine({
chartContainerId: this.$refs.chartContainer,
controlsHost: this.$refs.controlsContainer,
useSelectiveRendering: true
});
this.engine = { store, destroy };
if (this.modelYaml) {
const parsed = parseYAML(this.modelYaml);
store.dispatch(initializeFromYAML(parsed));
}
},
beforeUnmount() {
this.engine?.destroy();
}
};
</script>Svelte Integration
<script>
import { onMount, onDestroy } from 'svelte';
import { initEngine, parseYAML, initializeFromYAML } from 'equilibria-engine';
export let modelYaml;
let chartContainer;
let controlsContainer;
let engine;
onMount(() => {
const { store, destroy } = initEngine({
chartContainerId: chartContainer,
controlsHost: controlsContainer,
useSelectiveRendering: true
});
engine = { store, destroy };
if (modelYaml) {
const parsed = parseYAML(modelYaml);
store.dispatch(initializeFromYAML(parsed));
}
});
onDestroy(() => {
engine?.destroy();
});
</script>
<div bind:this={chartContainer} class="chart-container"></div>
<div bind:this={controlsContainer} class="controls-container"></div>Performance Best Practices
1. Use Selective Rendering for Multi-Chart Dashboards
const { store, destroy } = initEngine({
chartContainerId: 'container',
controlsHost: controls,
useSelectiveRendering: true // Only re-render affected charts
});2. Batch Parameter Updates
import { setMultipleParameters } from 'equilibria-engine';
// ❌ Bad - triggers multiple re-renders
dispatch(setParameter('tax', 10));
dispatch(setParameter('subsidy', 5));
dispatch(setParameter('quota', 100));
// ✅ Good - single re-render
dispatch(setMultipleParameters({
tax: 10,
subsidy: 5,
quota: 100
}));3. Monitor Cache Performance
import { getCacheStats } from 'equilibria-engine';
// Check cache hit rates
const stats = getCacheStats();
console.log('Expression cache:', stats.compiledExpressions);
console.log('Evaluation cache:', stats.evaluations);
console.log('Hit rate:', (stats.evaluations.utilization * 100).toFixed(1) + '%');4. Clear Caches on Model Changes
import { clearEvaluationCaches } from 'equilibria-engine';
// Clear caches when loading a completely new model
function loadNewModel(yaml) {
clearEvaluationCaches();
const parsed = parseYAML(yaml);
store.dispatch(initializeFromYAML(parsed));
}State Management Patterns
Subscribe to Specific Changes
let previousParameters = null;
store.subscribe(() => {
const state = store.getState();
// Only react to parameter changes
if (state.parameters !== previousParameters) {
previousParameters = state.parameters;
console.log('Parameters changed:', state.parameters);
// Update external UI, analytics, etc.
}
});Integrate with External State Management
// Redux Toolkit example
import { createSlice } from '@reduxjs/toolkit';
const economicsSlice = createSlice({
name: 'economics',
initialState: { equilibriaState: null },
reducers: {
syncEquilibriaState: (state, action) => {
state.equilibriaState = action.payload;
}
}
});
// Sync Equilibria state to your app's Redux store
equilibriaStore.subscribe(() => {
const eqState = equilibriaStore.getState();
appStore.dispatch(syncEquilibriaState(eqState));
});Advanced Usage
Custom Math Functions
Extend Math.js with domain-specific functions:
import { math } from 'equilibria-engine';
math.import({
elasticity: function(price, quantity, dP, dQ) {
return (dQ / quantity) / (dP / price);
}
});
// Use in YAML equations
// equation: "elasticity(price, quantity, 1, -2)"Event Handling
Listen to state changes:
store.subscribe(() => {
const state = store.getState();
console.log('State updated:', state);
// Custom logic based on state
if (state.parameters.price.value > 100) {
console.log('Price is high!');
}
});Programmatic Chart Updates
import { render, ChartRenderer } from 'equilibria-engine';
// Render all charts
render('chart-container', store.getState(), config, store);
// Use ChartRenderer for per-chart control
const renderer = new ChartRenderer('chart-container', config, store);
renderer.renderChart('mainChart', chartDef, store.getState());Example Models
Supply & Demand with Tax
parameters:
tax:
value: 0
min: 0
max: 30
step: 5
label: "Tax ($/unit)"
charts:
- id: mainChart
elements:
- id: supply
type: line
equation: "20 + 2 * x + tax" # Tax shifts supply up
color: "#dc2626"
- id: consumerSurplus
type: area
topBoundary: "demand"
bottomBoundary: "equilibriumPrice"
leftBoundary: "0"
rightBoundary: "equilibriumQuantity"
color: "#10b981"
opacity: 0.3Production Possibilities Frontier
parameters:
alpha:
value: 0.5
min: 0
max: 1
step: 0.1
label: "Technology Parameter"
charts:
- id: ppf
title: "Production Possibilities Frontier"
elements:
- id: frontier
type: line
equation: "100 * (1 - (x / 100) ** alpha)"
color: "#2563eb"Range Brackets
Brackets visualize ranges and changes on axes (ΔQ, ΔP, tax wedges).
parameters:
q1:
value: 30
label: "Initial Quantity"
q2:
value: 50
label: "Final Quantity"
charts:
- id: mainChart
elements:
- id: quantityChange
type: bracket
axis: 'x'
from: "q1"
to: "q2"
label: 'ΔQ'
offset: -20
color: "#6366f1"
- id: priceChange
type: bracket
axis: 'y'
from: 40
to: 60
label: 'ΔP'
offset: 20
color: "#ec4899"Properties:
type: 'bracket'- Element typeaxis: 'x' | 'y'- Which axis to place bracket onfrom: number | string- Start value (parameter reference allowed)to: number | string- End value (parameter reference allowed)label?: string- Text label for the bracketoffset?: number- Distance from axis (negative = below/left, positive = above/right)color?: string- Bracket color
Use Cases:
- Show quantity/price changes (ΔQ, ΔP)
- Indicate tax wedges between curves
- Highlight equilibrium shifts
- Mark consumer/producer surplus regions
On-Graph Sliders
Sliders provide direct on-chart parameter manipulation.
parameters:
tax:
value: 0
min: 0
max: 30
charts:
- id: interactive
elements:
- id: taxSlider
type: slider
param: "tax"
axis: 'y'
min: 0
max: 30
label: "Tax"
format: "$,.0f"
color: "#8b5cf6"
position: 10 # Position on perpendicular axisProperties:
type: 'slider'- Element typeparam: string- Parameter name to controlaxis: 'x' | 'y'- Which axis to place slider handle onmin: number | string- Minimum valuemax: number | string- Maximum valuelabel?: string- Display labelformat?: string- D3 format string for value display (e.g., "$,.0f", ".2f")color?: string- Handle and track colorposition?: number | string- Position on perpendicular axis
Use Cases:
- Tax/subsidy controls on price axis
- Quota controls on quantity axis
- Technology parameter sliders
- Interactive comparative statics
JSON Schema & IDE Integration
Equilibria includes JSON Schema export for enhanced IDE support and YAML validation.
Generate Schema
Export JSON Schema v7 from Zod schemas:
# One-time export
pnpm schema:export
# Output: schema/equilibria-document.schema.json (~41KB)VSCode Integration
1. Install Extension
Install YAML by Red Hat from the VSCode marketplace.
2. Schema Auto-Configuration
The .vscode/settings.json file is pre-configured to map the schema to your YAML files:
{
"yaml.schemas": {
"./schema/equilibria-document.schema.json": [
"*.equilibria.yaml",
"examples/**/*.yaml",
"tests/fixtures/**/*.yaml"
]
}
}3. IDE Features
Once configured, you get:
- Autocomplete: Press
Ctrl+Spacefor field suggestions - Inline Validation: Real-time error detection while typing
- Hover Documentation: See descriptions for all fields
- Type Checking: Automatic validation of data types
- Required Fields: Warnings for missing required properties
- Enum Values: Dropdown suggestions for
specVersion, element types, etc.
Example YAML with IDE Support:
# Type "met" and press Ctrl+Space to autocomplete "metadata:"
metadata:
title: "My Economic Model"
specVersion: "1.0" # ← Autocomplete suggests valid versions
# Type "param" and press Ctrl+Space
parameters:
price:
value: 100
min: 0 # ← Hover to see "Minimum allowed value (number)"
max: 200
label: "Market Price"
# Type "chart" and see the array structure suggested
charts:
- id: "mainChart"
elements:
- type: "line" # ← Autocomplete suggests: line, area, point, verticalLine, etc.
equation: "100 - 2 * x"
color: "#2563eb"Schema Details
- Format: JSON Schema Draft 7
- Size: ~41KB (fully inlined, no
$refs) - Validation: Includes Zod error messages for better feedback
- Metadata: Custom
x-equilibria-versionandx-generated-fromfields - Source: Generated from Zod schemas in
src/schemas/
Regenerate After Schema Changes
If you modify the Zod schemas, regenerate the JSON Schema:
pnpm schema:exportTesting
The engine includes comprehensive test coverage:
# Run all tests
pnpm test
# Watch mode
pnpm test:watch
# Coverage report
pnpm test:coverage
# E2E tests
pnpm test:e2eTest Stats:
- 430 tests passing
- 85%+ code coverage
- Unit, integration, and E2E tests
Accessibility
Equilibria is designed with accessibility as a core requirement:
- ✅ WCAG 2.1 AA compliant
- ✅ Full keyboard navigation (Tab, Arrow keys, Enter, Space)
- ✅ Screen reader support (NVDA, JAWS, VoiceOver tested)
- ✅ ARIA labels on all interactive elements
- ✅ High contrast mode support
- ✅ Focus indicators on all controls
- ✅ Descriptive button text and labels
Keyboard Shortcuts
Tab- Navigate between controlsArrow Up/Down- Adjust slider valuesEnter- Activate buttonsSpace- Toggle checkboxesEscape- Close dialogs
Browser Support
- Chrome 100+
- Firefox 100+
- Safari 15+
- Edge 100+
Bundle Size
- Full bundle: ~157KB gzipped
- Redux: 5KB
- Math.js: 60KB (selective imports: ~40KB)
- D3.js: 70KB (selective imports: ~45KB)
- js-yaml: 10KB
- pako: 12KB
- Engine code: 15KB
Optimization Tips:
- Import only needed D3 modules
- Use selective Math.js imports
- Enable tree-shaking in build
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Quick Start for Contributors:
# Clone repo
git clone https://github.com/your-org/equilibria.git
# Install dependencies (from monorepo root)
pnpm install
# Run tests
cd packages/equilibria-engine
pnpm test
# Make changes and test
pnpm test:watchChangelog
See CHANGELOG.md for version history and breaking changes.
License
MIT © 2025 Kinetonomics
Links
- Documentation: Full Documentation
- YAML Specification
- Migration Guide - Migrate to v2.0 with Zod validation
- Troubleshooting Guide
- Testing Guide
- API Reference
- Examples: HTML Demos
- GitHub: Repository
- Issues: Bug Reports
CDN Usage (quick snippets)
You can load the built bundles directly from popular CDNs (jsDelivr / unpkg). These files are produced in dist/ during the package build.
UMD (script tag) — jsDelivr / unpkg (pinned to v1.0.2)
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/equilibria-engine.umd.min.js"></script>
<!-- or unpkg -->
<script src="https://unpkg.com/[email protected]/dist/equilibria-engine.umd.min.js"></script>The UMD build exposes a global. Common names to try are EquilibriaEngine or equilibriaEngine (the exact global depends on the build name in Rollup). Example usage:
<script>
// The UMD bundle attaches to window
const { initializeEngine } = window.EquilibriaEngine || window.equilibriaEngine || {};
const engine = initializeEngine({ chartContainer: '#chart', controlsContainer: null });
// engine.loadYAML(...)
</script>ESM (native module import) — esm.sh (pinned to v1.0.2)
<script type="module">
// For reliable named ESM exports in browser demos, use the esm.sh pre-bundled entry.
import { initializeEngine } from 'https://esm.sh/[email protected]/dist/equilibria-engine.esm.js?bundle';
const engine = initializeEngine({ chartContainer: '#chart' });
// engine.loadYAML(...)
</script>Credits
Built with:
- Redux - State management
- D3.js - Data visualization
- Math.js - Expression evaluation
- js-yaml - YAML parsing
- pako - Compression
- [zod] (https://zod.dev/) - YAML Schemas
Support
- Email: [email protected]
- Discord: Join our community
- Twitter: @Equilibria
