Embedded Chart Widget Tutorial
Build a widget that wraps a platform time-series chart with a dropdown to switch which data key is charted, and a large current-value display above the chart.
This tutorial teaches you how to embed ThingsBoard’s built-in chart component inside a custom widget, create dynamic subscriptions at runtime, and use Angular Reactive Forms for user input.
What you’ll build
Section titled “What you’ll build”A time-series widget with:
- A dropdown to select which data key (e.g., “Temperature”, “Humidity”) is charted
- A large current-value display showing the latest value of the selected key
- An embedded
<tb-time-series-chart-widget>that renders the actual chart - A custom title panel with an icon
Step-by-step
Section titled “Step-by-step”-
Open Widget Library → open your bundle → click + → select Time-Series as the type.
-
In the HTML tab, paste:
<div class="flex flex-col w-full h-full" *ngIf="showChart"><!-- Title bar + icon --><div class="flex items-center justify-between"><ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container><div class="icon-wrapper flex items-center justify-center w-10 h-10 shrink-0"><mat-icon class="icon-inner">bolt</mat-icon></div></div><!-- Value (left) + key selector (right) --><div class="flex items-center justify-between mt-3"><div class="flex items-baseline gap-1"><span class="value">{{value}}</span><span class="units">{{units}}</span></div><div class="w-48" *ngIf="dataKeysFormControl"><mat-form-field appearance="outline" color="primary" class="w-full"><mat-label>Keys</mat-label><mat-select [formControl]="dataKeysFormControl"panelClass="custom-select-panel"placeholder="Select keys"><mat-option *ngFor="let key of allDataKeys" [value]="key.name">{{key.label}}</mat-option></mat-select></mat-form-field></div></div><!-- Chart --><div class="flex-1" *ngIf="showChart"><tb-time-series-chart-widget [ctx]="ctx"></tb-time-series-chart-widget></div></div>Key details:
<ng-container *ngTemplateOutlet="widgetTitlePanel">— renders the widget’s title bar (configured in dashboard widget settings). This works becausetypeParameterssetsembedTitlePanel: true.<mat-select [formControl]="dataKeysFormControl">— Angular Material dropdown bound to a Reactive Forms control, aligned to the right of the value.<tb-time-series-chart-widget [ctx]="ctx">— embeds the platform’s built-in time-series chart. It receives the widget context and renders a fully functional chart.*ngIf="showChart"— used to force chart re-initialization when the subscription changes (toggled off then on).
-
In the CSS tab, paste:
/* Icon badge — uses TB primary color tokens */.icon-wrapper {background-color: var(--tb-primary-50);border-radius: 50%;}.icon-inner {font-size: 20px;width: 20px;height: 20px;color: var(--tb-primary-500);}/* Value display */.value {color: #212121;font-size: 48px;line-height: 44px;margin-right: 4px;}.units {color: #212121;font-size: 24px;font-weight: 500;line-height: 32px;}/* ThingsBoard component overrides */.mat-mdc-form-field-subscript-wrapper {display: none !important;}.tb-time-series-chart-panel {gap: 0 !important;} -
In the JavaScript tab, paste:
function createCustomSubscription(dataKeyNames) {const dataKeys = self.ctx.$scope.allDataKeys.filter(key => dataKeyNames.includes(key.name));const datasources =self.ctx.defaultSubscription.datasources.map(ds => ({...ds,dataKeys}));let options = {type: 'timeseries',datasources,callbacks: {onDataUpdated: () => {self.onDataUpdated();}}};if (self.ctx.$scope.customSubscription) {self.ctx.$scope.customSubscription.unsubscribe();}self.ctx.$scope.customSubscription =self.ctx.subscriptionApi.createSubscription(options, true).subscribe(subscription => {subscription.timeWindowConfig =self.ctx.defaultSubscription.timeWindowConfig;self.ctx.defaultSubscription = subscription;self.ctx.data = subscription.data;self.ctx.datasources = subscription.datasources;self.ctx.defaultSubscription.update();// Force chart re-init by toggling visibilityself.ctx.$scope.showChart = false;self.ctx.detectChanges();self.ctx.$scope.units =self.ctx.data[0].dataKey.units;setTimeout(() => {self.ctx.$scope.showChart = true;self.ctx.detectChanges();}, 0);});}self.onInit = function() {if (self.ctx.$scope.timeSeriesChartWidget) {self.ctx.$scope.timeSeriesChartWidget.onInit();self.ctx.$scope.timeSeriesChartWidget.timeSeriesChart.onResize();}self.ctx.$scope.allDataKeys =self.ctx.widget.config.datasources[0].dataKeys;self.ctx.$scope.dataKeysFormControl =self.ctx.$scope.fb.control(self.ctx.$scope.allDataKeys[0].name);createCustomSubscription(self.ctx.$scope.dataKeysFormControl.value);self.ctx.$scope.dataKeysFormControl.valueChanges.subscribe(value => {createCustomSubscription(value);});};self.onDataUpdated = function() {const data = self.ctx.data[0].data;const valueArr = data[data.length - 1];if (valueArr && valueArr.length) {const decimals = self.ctx.data[0].dataKey.decimals;self.ctx.$scope.value = (decimals != null && decimals >= 0)? parseFloat(valueArr[1]).toFixed(decimals): valueArr[1];}if (self.ctx.$scope.timeSeriesChartWidget) {self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();self.ctx.detectChanges();}};self.typeParameters = function() {return {embedTitlePanel: true,hasAdditionalLatestDataKeys: true};};Let’s walk through the key concepts:
Custom subscriptions —
createCustomSubscription()is the core technique. When the user picks a different key from the dropdown, we:- Filter
allDataKeysto include only the selected key - Build new
datasourceswith the filtered keys - Call
self.ctx.subscriptionApi.createSubscription(options, true)to create a new subscription - Replace
self.ctx.defaultSubscriptionwith the new one - Toggle
showChartoff/on to force the embedded chart to reinitialize with new data
Reactive Forms —
self.ctx.$scope.fbis Angular’sFormBuilder, available in all widgets. We create a form control withfb.control(initialValue)and subscribe tovalueChangesto react when the user selects a different key.Embedded chart delegation — in
onDataUpdated(), we callself.ctx.$scope.timeSeriesChartWidget.onDataUpdated()to forward the data update to the embedded chart component. - Filter
-
In the Settings form tab, you have two options:
Option A — leave it empty: the widget has no settings panel. All configuration comes from the datasource keys.
Option B — inherit the embedded chart’s settings: set the Settings schema selector to
tb-time-series-chart-widget-settings. This reuses the built-in time-series chart settings form, so users can configure chart appearance (legend, axis labels, thresholds, etc.) exactly as they would on the native chart widget.Additionally, in the Data key settings form field, set the selector to
tb-time-series-chart-key-settings. This gives each data key the same per-key configuration panel (color, fill, step mode, etc.) that the built-in chart exposes. -
Click Run (or
Ctrl+Enter) to preview the widget layout in the preview panel. -
Click Save (or
Ctrl+S) → name the widget “Card with Chart”. -
Add the widget to a dashboard:
- Select a device as the datasource
- Add multiple data keys (e.g.,
temperature,humidity,pressure) - The dropdown will list all configured keys
- Switching keys replaces the chart and updates the current value display
How the chart re-init trick works
Section titled “How the chart re-init trick works”The embedded <tb-time-series-chart-widget> binds to self.ctx and reads self.ctx.data and self.ctx.defaultSubscription. When we replace the subscription, the chart needs to reinitialize to pick up the new data structure.
Simply replacing the subscription doesn’t trigger a re-render of the chart’s internal state. The solution: toggle showChart = false (which removes the chart from the DOM via *ngIf), call detectChanges(), then immediately set showChart = true in a setTimeout. This forces Angular to destroy and recreate the chart component, which reinitializes it with the new subscription data.
Type parameters for embedded widgets
Section titled “Type parameters for embedded widgets”embedTitlePanel: true tells the platform to inject the widget title bar as a template reference (widgetTitlePanel) that you can place anywhere in your HTML via *ngTemplateOutlet="widgetTitlePanel". Without this flag, the widgetTitlePanel template is not available and the title panel won’t render.
hasAdditionalLatestDataKeys: true allows the widget to accept extra latest-value data keys in addition to the time-series keys — useful if you want to display a current value alongside the chart.
Next steps
Section titled “Next steps”- RPC Control tutorial — build a device control panel with command buttons
- Alarm Widget tutorial — build a custom alarm table
- Latest Values tutorial — sensor card with dynamic colors and relative timestamp
- Widget Patterns — CRUD tables, navigation, entity hierarchy, and more
- Advanced Topics — external libraries, inter-widget communication, debugging
- Widget API Reference — full details on
subscriptionApi,createSubscription, and data structures