跪拜 Guibai
← Back to the summary

A React Scaffold That Turns Dashboard Assembly into Drag-and-Drop Configuration

When building visualization dashboards, many projects start with just "drawing a few charts," but soon a whole set of engineering problems emerges:

@lius1314/visual-dashboard-scaffold is a React visualization dashboard scaffold built around these problems. It doesn't try to replace ECharts, Three.js, or business components, but provides an editable, configurable, and publishable "dashboard assembly layer."

npm address: https://www.npmjs.com/package/@lius1314/visual-dashboard-scaffold
Current version: v1.2.2
Tech stack: React + TypeScript + Zustand + react-grid-layout + react-router-dom + autofit.js

wechat-dashboard-overview.png

What problem does it solve

Screenshot 2026-06-29 20.40.26.png

The common approach for traditional dashboard projects is: developers first hardcode the layout according to the design draft, then change the code when requirements change. This is fast initially but heavy later:

This scaffold separates the "dashboard framework capabilities" from the "business components":

The end result is: developers can quickly assemble pages, and business colleagues can also adjust layouts and styles in edit mode, then export JSON configurations for production environments.

Core Capabilities

1. Visual Editing

In edit mode, chart blocks can be directly dragged, scaled, copied, deleted, and have their z-index adjusted. Block titles, backgrounds, content areas, card backgrounds, title fonts, margins, animations, etc., can all be configured individually.

image.png

In preview mode, these editing guides, drag handles, and tool panels are automatically hidden, leaving only the final display effect.

2. Multi-Page Dashboards

WeChat Image_20260629205016_369_53.png

The SDK has a built-in HashRouter with the route format:

/#/page/:pageId

Each page independently maintains:

The header and footer are globally shared configurations, suitable for real dashboard systems with "unified title bar + unified bottom navigation."

3. Block Registration Mechanism

WeChat Image_20260629205836_370_53.png The scaffold does not bind to any chart library. You can register ECharts, AntV, Three.js, map components, or even a regular business table.

import {
  App,
  registerBlock,
  type BlockComponentProps,
} from '@lius1314/visual-dashboard-scaffold';
import '@lius1314/visual-dashboard-scaffold/style.css';

function SalesChart({ title, pageParams, emit }: BlockComponentProps) {
  return (
    <div style={{ width: '100%', height: '100%' }}>
      <button onClick={() => emit?.('sales:refresh', { region: pageParams?.region })}>
        {title}
      </button>
    </div>
  );
}

registerBlock('sales-chart', SalesChart);

export default function AppPage() {
  return <App />;
}

When the chartBlock.layout.i in the configuration matches the registered ID, the SDK renders your component in the corresponding block.

4. Themes and Asset Resources

ScreenShot_2026-06-29_210054_272.png

WeChat Image_20260629210828_371_53.png

The project includes multiple types of static assets:

The asset selector displays them by category, making it easy to switch backgrounds, cards, headers, or footer styles directly in the editing panel.

If the project has its own assets, they can also be injected at runtime:

import { configureImageAssets } from '@lius1314/visual-dashboard-scaffold';

configureImageAssets({
  assets: {
    bg: ['factory.png', 'city.png'],
    business: ['alarm-card.png', 'device-panel.png'],
  },
  folders: [
    { key: 'bg', label: 'Background' },
    { key: 'business', label: 'Business Assets' },
  ],
  basePath: '/images',
});

5. Configuration Import/Export

ScreenShot_2026-06-29_210944_170.png

After editing, you can export the JSON configuration from the settings panel. The host project passes the JSON as initialConfig on the next startup:

import { App } from '@lius1314/visual-dashboard-scaffold';
import config from './dashboard-config.json';

export default function Dashboard() {
  return <App initialConfig={config} defaultEditMode={false} />;
}

The design of initialConfig is restrained: it only loads when there is no locally persisted data for the first time. Subsequent user edits are saved locally and won't be overwritten on each refresh. If you need to revert to the default configuration, you can click "Reset" in the settings panel.

6. Local Persistence

State management uses Zustand. Persistence doesn't simply stuff the entire large JSON into localStorage; it uses hybrid storage:

This prevents noticeable lag from constant JSON.stringify of large configurations during dragging, scaling, and frequent block adjustments.

7. Page Navigation and Parameter Passing

Page parameters support three-layer merging:

image.png

URL search params > switchPage params > PageConfig.params

Blocks can access the page context via usePageContext:

import { usePageContext } from '@lius1314/visual-dashboard-scaffold';

function RegionCard() {
  const { switchPage, pageParams } = usePageContext();

  return (
    <button onClick={() => switchPage('detail-page', { region: pageParams.region ?? 'beijing' })}>
      View Details
    </button>
  );
}

Shareable links can also be accessed directly:

/#/page/detail-page?region=beijing&theme=dark

8. Action System

Bottom buttons, header panels, and chart blocks can all be configured with click actions:

ScreenShot_2026-06-30_200905_527.png

type BlockAction =
  | { type: 'switchPage'; pageId: string; params?: Record<string, unknown> }
  | { type: 'emitEvent'; eventName: string; payload?: unknown }
  | { type: 'custom'; handlerKey: string }
  | { type: 'none' };

Common scenarios:

Custom handlers can also be registered in the host project:

import { registerActionHandler } from '@lius1314/visual-dashboard-scaffold';

registerActionHandler('openAlarmDialog', () => {
  console.log('Open alarm dialog');
});

Architecture Breakdown

wechat-dashboard-architecture.png The overall structure can be understood as three layers:

OuterContainer
  HeaderSection   # Global header: title + draggable panels
  MiddleSection   # Page body: chart block orchestration
  FooterSection   # Global footer: navigation dock

The corresponding data structure:

DashboardConfig
  outer
  header
  footer
  activePageId
  pages[]
    middle
    chartBlocks[]
    params

The advantage of this structure is that the boundaries are very clear:

Why Pixel Coordinates for Drag-and-Drop Layout

ScreenShot_2026-06-29_212825_817.png

Many backend systems are better suited for grid layouts, but visualization dashboards usually come from design drafts, with explicit pixel positions as the target. If a regular responsive layout is used, developers have to constantly convert units during development.

This project's ChartBlockLayout uses pixel coordinates:

interface ChartBlockLayout {
  i: string;
  x: number;
  y: number;
  w: number;
  h: number;
  zIndex?: number;
  minW?: number;
  minH?: number;
}

It aligns more closely with design handoff: where a block is placed, how much space it occupies, and its z-index can all be expressed directly. During preview, autofit.js handles the overall proportional scaling.

Adaptive Solution

In preview mode, the SDK uses autofit.js to scale proportionally according to the design dimensions. The default design size is 1920 x 1080, and the settings panel supports:

ScreenShot_2026-06-29_210624_178.png

In edit mode, instead of handing the entire body to autofit for scaling, only the content area is scaled independently. This keeps the toolbar, drawer, and configuration panel still operable, avoiding the problem of editing UI being shrunk together and becoming hard to click.

Event Communication

The scaffold has a built-in EventBus that supports page-level scoping. Senders can write like this:

function MapBlock({ emit }: BlockComponentProps) {
  return (
    <button onClick={() => emit?.('map:regionClicked', { region: 'East China' })}>
      Click Region
    </button>
  );
}

Receivers listen for events within the same page:

import { useEventBus, type BlockComponentProps } from '@lius1314/visual-dashboard-scaffold';

function TrendBlock({ pageId }: BlockComponentProps) {
  useEventBus('map:regionClicked', (payload) => {
    console.log('Refresh trend chart', payload);
  }, { scope: pageId ? `page:${pageId}` : undefined });

  return <div>Trend Chart</div>;
}

When a page is unmounted, the SDK cleans up listeners under the current page's scope, preventing old components from continuing to respond to events after page switches.

Quick Start

wechat-dashboard-workflow.png

Installation:

npm install @lius1314/visual-dashboard-scaffold
npm install react react-dom react-router-dom

Host entry:

import { App } from '@lius1314/visual-dashboard-scaffold';
import '@lius1314/visual-dashboard-scaffold/style.css';

export default function Dashboard() {
  return <App editable defaultEditMode={false} />;
}

Read-only production mode:

<App editable={false} initialConfig={config} />

Custom blocks:

import { registerBlock, type BlockComponentProps } from '@lius1314/visual-dashboard-scaffold';

function DevicePanel({ title, pageParams }: BlockComponentProps) {
  return (
    <div style={{ width: '100%', height: '100%' }}>
      {title}: {String(pageParams?.deviceId ?? '-')}
    </div>
  );
}

registerBlock('device-panel', DevicePanel);

Points to Note When Using

1. Static Assets Need to Be Synced

The npm package carries a public/ directory, but the browser ultimately accesses the host project's static asset path. Therefore, the package's public needs to be synced to the host's public:

node_modules/@lius1314/visual-dashboard-scaffold/public -> public

Otherwise, themes, backgrounds, fonts, and Font Awesome icons may return 404 errors.

If assets get messed up, or you want to restore built-in assets after upgrading the SDK, you can add a reset script:

{
  "scripts": {
    "reset:datavis-assets": "node -e \"const fs=require('fs'); ['themes','images','fonts','css','webfonts'].forEach(d=>fs.rmSync('public/'+d,{recursive:true,force:true})); fs.cpSync('node_modules/@lius1314/visual-dashboard-scaffold/public','public',{recursive:true,force:true});\""
  }
}

Then execute:

npm run reset:datavis-assets

This command only resets the SDK-related themes, images, fonts, css, webfonts directories. If business assets are also placed in these directories, back them up before execution, or put business assets in a separate directory and integrate them using configureImageAssets.

2. initialConfig Is Not a Forced Override

It is a "first-time default configuration," not an "override configuration on every startup." This design protects the user's local modifications in edit mode.

To force a restore to default, use the "Reset" button in the settings panel, or call the store's resetConfig().

3. header/footer Are Now Global

In the new configuration, header and footer are at the top level of DashboardConfig. Old page-level header/footer will still be compatible and migrated, but new projects are recommended to uniformly use the top-level fields.

Suitable Scenarios

This scaffold is suitable for:

If it's just a fixed single-page display with no editing configuration needs, hand-coding a page might be lighter. But as soon as a project starts to have requirements for "configurable, reusable, multi-page, deliverable," this kind of scaffold can significantly reduce repetitive work.

Summary

The positioning of @lius1314/visual-dashboard-scaffold is not a chart library, but an assembly layer for dashboard applications.

It sinks common capabilities like drag-and-drop layout, multi-page routing, theme assets, configuration persistence, import/export, event communication, and action configuration into the framework, allowing business development to focus more on the charts, maps, and data logic themselves.

For projects that need to quickly build visualization dashboards and hope to continuously adjust and reuse configurations later, it can serve as a relatively complete starting point.

Project address: https://www.npmjs.com/package/@lius1314/visual-dashboard-scaffold

If you are interested in visualization and 3D technology, welcome to check out the WeChat public account 柳杉前端.