Getting Started Cameras & Video Detection & Recording Automation & Events Actions Integration & Connectivity Network & Discovery AI & Remote Control MQTT Modbus Pi4J & Raspberry Pi GPIO ZeroMQ System & Administration Comparisons Use Cases Troubleshooting About & Legal
Home / Documentation / D3JS Charts
Thing ยท D3JS Charts 2.0.0

Programmable D3.js charts

D3JSThing renders a custom D3.js 7.9.0 visualization in the portal. Its JavaScript file lives on the agent and can load current data, poll periodically, consume live events, or load stored history once and then extend it with real-time events.

From an agent value to a browser visualization

01

The agent stores the script

Chart script file points to a .js file in the agent file system. If it does not exist when the Thing starts, the module creates it from chart-template.js.

02

The portal evaluates the definition

Opening the component loads the script and D3.js 7.9.0 into the browser. The script must return an object containing at least convert() and render().

03

Raw data arrives

Depending on the selected strategy, the component invokes poll(), receives events forwarded through the environment WebRTC channel, or loads history once before continuing with live events.

04

The chart converts and renders

convert() normalizes the incoming payload. render() uses the supplied D3 instance and chart container to draw or update the visualization.

Create your first D3JS Chart

01

Install the module

Install the D3JS Charts 2.0.0 module on the same agent that exposes the data source you want to visualize.

02

Add the Thing

Create a D3JS Chart component. Give it a descriptive title such as System utilization.

03

Select delivery and storage

For the packaged System Monitor example, choose One time load or Polling. Choose One time load & async events for a historical series that must continue with live updates. Select or create a JavaScript file such as /charts/system-utilization.js.

04

Start and open it

Start the Thing and open its UI. The Chart tab displays the gauge. The Script tab opens the editable source. Changes are autosaved after one second and the active chart is rebuilt after saving.

05

Adapt the template

Replace systemMonitorThingUuid, change poll() to call your data source, then update convert() and render() for its payload. An empty script shows Load template chart to restore the packaged example.

Trusted code: the chart script executes in the signed-in user's browser and can access the environment channel with that user's permissions. Allow only trusted users to edit chart files.
FieldDescriptionDefault
titleName displayed for the component and above the chart.D3JS Chart
dataDeliveryStrategyControls whether the chart uses one-time loading, periodic polling, async events, or one historical load followed by async events.ONE_TIME_LOAD
refreshIntervalSecondsPolling interval, minimum one second. It is shown only for POLLING.30
fileSystemUuidHidden reference to the file-system Thing that owns the chart script.Server local file system
scriptPathRequired path to a JavaScript file. The chooser accepts .js files and can create a new file.None

Choose the strategy that matches how data changes

StrategyRuntime behaviorUse it for
ONE_TIME_LOADCalls poll() once when opened and shows a Refresh button.Reports, inventory, or expensive queries requested by a user.
POLLINGCalls poll() immediately and then every configured interval.Current state exposed through another Thing's Remote API.
ASYNC_EVENTSListens only for events forwarded to the portal. poll() is not required.Low-latency sensor, recognition, or state-change updates.
ONE_TIME_LOAD_AND_ASYNC_EVENTSSubscribes to events, calls poll() once for stored history, and then processes incoming events without periodic polling. Refresh repeats the historical load on demand.Historical charts that must continue in real time.
Events must reach the browser: configure an Event Manager rule with Forward event in Portal WebRTC Integration. A D3JS subscription filters already-forwarded events; it does not forward them by itself.

The object returned by the chart script

MemberRequiredPurpose
configNoYour own constants, colors, UUIDs, thresholds, and other chart settings.
subscriptionsFor filtered eventsArray of {nodeUuid, eventType} filters. Missing or empty means all forwarded events when an async strategy is active.
init(context)NoCreates the persistent SVG or DOM skeleton when the chart starts.
convert(rawData)YesTransforms a poll response or event into the model consumed by render().
onEvent(event, context)NoHandles an event explicitly. Without it, the component invokes render(convert(event), context).
poll(channel, context)For all non-event-only strategiesCalls an agent Thing through the environment channel, or loads another browser-accessible source. With ONE_TIME_LOAD_AND_ASYNC_EVENTS, it returns the initial historical dataset.
render(data, context)YesDraws or updates the chart using D3.js.
destroy(context)NoRemoves listeners and resources when the chart closes or is rebuilt.

The context object supplies d3, container, environmentUuid, thingUuid, channel, component options, and a per-browser stateManager. The state manager exposes loadRawState() and saveRawState(state).

CPU and memory gauge from chart-template.js

The supplied template calls the built-in System Monitor Thing, converts its CPU ratio and memory counters into percentages, and draws two needles over green, amber, and red utilization zones. This abridged excerpt preserves the template's essential data path:

return {
    config: {
        systemMonitorThingUuid: '00000000-0000-0000-0000-000000000010',
        zones: [
            {from: 0, to: 60, color: '#22c55e'},
            {from: 60, to: 80, color: '#f59e0b'},
            {from: 80, to: 100, color: '#ef4444'}
        ]
    },

    init(context) {
        this.svg = context.d3.select(context.container)
            .append('svg')
            .attr('width', '100%')
            .attr('height', '100%');
    },

    async poll(channel) {
        const result = await channel.thingApiCall(
            this.config.systemMonitorThingUuid,
            {method: 'readState'}
        );
        return result.response;
    },

    convert(rawData) {
        const total = Number(rawData.ram.totalMemorySize);
        const free = Number(rawData.ram.freeMemorySize);
        return [
            {label: 'CPU', value: Number(rawData.cpu.cpuLoad) * 100},
            {label: 'Memory', value: total > 0 ? ((total - free) / total) * 100 : 0}
        ];
    },

    render(data, context) {
        const d3 = context.d3;
        const angle = d3.scaleLinear().domain([0, 100]).range([-90, 90]);
        // The complete template draws zones, ticks, two needles and value labels.
        // Keep persistent elements on this object when later updates must reuse them.
    },

    destroy() {
        this.svg?.remove();
        this.svg = null;
    }
};

Adapting it to another Thing

  1. Put the target component UUID in config.
  2. Change the method and parameters passed to channel.thingApiCall().
  3. Inspect result.response and make convert() return a stable, simple chart model.
  4. Bind that model to D3 selections in render(). Handle repeated calls and container resizing rather than assuming a single render.
Responsive rendering: the component calls render() again with the last converted data after the container size changes. Read context.container.getBoundingClientRect() inside render(), as the complete template does.

Subscribe without polling

Select ASYNC_EVENTS and declare precise filters. Replace the example values with the source node UUID and the fully qualified event type delivered by your environment.

return {
    subscriptions: [{
        nodeUuid: 'SOURCE-COMPONENT-UUID',
        eventType: 'com.example.SensorStateEvent'
    }],

    convert(event) {
        return {value: Number(event.value), timestamp: event.timestamp};
    },

    render(data, context) {
        // Update the D3 visualization with the converted event.
    }
};
Avoid unintended wildcard subscriptions: for an asynchronous strategy, an absent or empty subscriptions array matches every forwarded event in the environment. Declare explicit filters on production charts.

Load history once, then append live events

With ONE_TIME_LOAD_AND_ASYNC_EVENTS, the component establishes event subscriptions first and then invokes poll() once. poll() should return the stored historical collection; onEvent() should normalize and append each later event. The component coordinates the two deliveries but does not merge their payloads automatically. The Refresh button can repeat the history request without enabling periodic polling.

return {
    subscriptions: [{
        nodeUuid: 'SOURCE-COMPONENT-UUID',
        eventType: 'com.example.SensorStateEvent'
    }],

    async poll(channel) {
        this.historyLoading = true;
        try {
            const result = await channel.thingApiCall(
                'HISTORY-STORAGE-THING-UUID',
                {method: 'readHistory'}
            );
            return result.response;
        } catch (error) {
            this.historyLoading = false;
            throw error;
        }
    },

    convert(rawData) {
        const items = Array.isArray(rawData) ? rawData : [rawData];
        return items.map(item => ({
            timestamp: new Date(item.timestamp),
            value: Number(item.value)
        }));
    },

    render(history, context) {
        // A new array means an initial or manually refreshed history snapshot.
        // Preserve events that arrived while the initial history request was running.
        if (history !== this.historySnapshot) {
            const pending = this.pendingPoints ?? [];
            this.historySnapshot = history;
            this.points = [...history, ...pending];
            this.pendingPoints = [];
        }
        this.historyLoading = false;
        this.draw(this.points, context);
    },

    onEvent(event, context) {
        const [point] = this.convert(event);
        if (point) {
            if (this.historyLoading || this.historySnapshot === undefined) {
                this.pendingPoints ??= [];
                this.pendingPoints.push(point);
            } else {
                this.points.push(point);
                this.draw(this.points, context);
            }
        }
    },

    draw(points, context) {
        // Apply the normal D3 data join and redraw axes or paths here.
    }
};
Ordering and deduplication belong to the script: storage results and live events can overlap or arrive with different timestamps. Sort, trim, and deduplicate this.points according to the source event identity and retention window.
MethodResult
readOptionsReturns title, refresh interval, delivery strategy, and script path.
readScriptEnsures the script exists and returns its content as {script}.
readTemplateReturns the original packaged chart-template.js as {script}.
updateScriptWrites the supplied script string to the configured file.

The browser editor uses these methods automatically. Read operations require component read permission; updating the script requires update permission.

JS

Errors stay in the chart UI

Syntax errors, rejected API calls, invalid payloads, and rendering exceptions appear in the component error panel and browser console. Validate payload fields before drawing.

UI

Rendering uses browser resources

Large datasets, frequent full redraws, or complex SVG structures can make the portal tab slow. Aggregate data and use D3 update joins for high-frequency charts.

NET

The environment channel must be connected

Polling and forwarded events depend on the portal connection to the agent. The chart cannot update while that environment channel is unavailable.

CORS

Browser network rules still apply

A chart can use fetch(), but direct browser requests remain subject to HTTPS mixed-content rules, CORS, authentication, and target availability. Prefer channel.thingApiCall() for data exposed by the agent.

FILE

The script is shared agent state

The script file is stored on the agent rather than per user. Editing it changes the chart definition for every user of that Thing; browser state saved through stateManager remains user-side.

SYS

Agent health gauge

Use the packaged template to display current CPU and memory utilization from System Monitor.

IOT

Device telemetry

Poll MQTT, Modbus, Pi4J, or another integration Thing and visualize its normalized state.

EVT

Historical timeline with a live tail

Load stored measurements once, then append events forwarded through WebRTC without repeatedly querying the full history.

OPS

Custom operational view

Create a focused visualization when a standard dashboard widget does not fit the data shape or interaction model.