Skip to content
Stand with Ukraine flag

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.

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
  1. Open Widget Library → open your bundle → click + → select Time-Series as the type.

  2. 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 because typeParameters sets embedTitlePanel: 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).
  3. 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;
    }
  4. 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 visibility
    self.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 subscriptionscreateCustomSubscription() is the core technique. When the user picks a different key from the dropdown, we:

    1. Filter allDataKeys to include only the selected key
    2. Build new datasources with the filtered keys
    3. Call self.ctx.subscriptionApi.createSubscription(options, true) to create a new subscription
    4. Replace self.ctx.defaultSubscription with the new one
    5. Toggle showChart off/on to force the embedded chart to reinitialize with new data

    Reactive Formsself.ctx.$scope.fb is Angular’s FormBuilder, available in all widgets. We create a form control with fb.control(initialValue) and subscribe to valueChanges to react when the user selects a different key.

    Embedded chart delegation — in onDataUpdated(), we call self.ctx.$scope.timeSeriesChartWidget.onDataUpdated() to forward the data update to the embedded chart component.

  5. 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.

  6. Click Run (or Ctrl+Enter) to preview the widget layout in the preview panel.

  7. Click Save (or Ctrl+S) → name the widget “Card with Chart”.

  8. 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

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.

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.