Tutorial: Build a custom visualization

Build a bar chart custom visualization from scratch, configure its editor options, package it as a Splunk app, and test it in Dashboard Studio.

This tutorial walks through building a custom bar chart visualization from scratch. It covers the following:

  1. Setting up the project

  2. Implementing the visualization

  3. Exposing configurable options

  4. Packaging the visualization for deployment

This example does not require external libraries.

The bar chart renders rows from a search result as horizontal bars. It expects 2 fields: a label and numeric value. The bar color and maximum bar width are configurable from the Dashboard Studio editor panel.

This tutorial uses the JavaScript template. The tutorial includes a React variant after the main procedure. See React variant.

  • Node.js 22 or later

  • npm or yarn

  • Splunk instance for testing the packaged app

  1. Create a new empty directory and navigate into it:
    CODE
    mkdir bar_chart_app 
    cd bar_chart_app
  2. Scaffold the project with the dashboard extension template.
    CODE
    yarn create @splunk/create@latest --mode=dashboard-studio-extension

    When prompted, select the dashboard-studio-extension mode, then select the JavaScript template. Name the project bar_chart_app.

  3. Review the generated project structure.
    CODE
    bar_chart_app/
     ├── package.json
     ├── package/
     │   └── app/
     │       └── app.conf                   # Splunk app identity
     ├── visualizations/
     │   └── bar_chart/
     │       ├── src/
     │       │   ├── visualization.js       # Visualization source (edit this)
     │       │   └── visualization.css
     │       └── config.json                # Metadata and editor configuration
     ├── dist/                              # Built output (generated)
     └── build.mjs                          # esbuild build script
  4. Configure the Splunk app in package/app/app.conf.
    CODE
    [package]
    id = bar_chart_app
    version = 1.0.0
    
    [launcher]
    version = 1.0.0
    author = Your Name
    description = A simple bar chart custom visualization
    
    [ui]
    label = Bar Chart App
  5. Define the visualization metadata and editor configuration in config.json.

    config.json controls the visualization identity, required data sources, and the editor panel displayed in the Dashboard Studio sidebar.

    JSON
    {
        "showTitleAndDescription": true,
        "includeInToolbar": true,
        "includeInVizSwitcher": true,
        "showDrilldown": false,
        "config": {
            "name": "Bar Chart",
            "description": "Renders search results as horizontal bars",
            "category": "Custom",
            "dataContract": {
                "requiredDataSources": ["primary"]
            },
            "size": {
                "initialWidth": 500,
                "initialHeight": 300
            },
            "optionsSchema": {
                "barColor": {
                    "type": "string",
                    "default": "#4e9cf5"
                },
                "maxBarWidth": {
                    "type": "number",
                    "default": 400
                }
            },
            "editorConfig": [
                {
                    "label": "Appearance",
                    "layout": [
                        [
                            {
                                "editor": "editor.color",
                                "label": "Bar color",
                                "option": "barColor"
                            }
                        ],
                        [
                            {
                                "editor": "editor.text",
                                "label": "Max bar width (px)",
                                "option": "maxBarWidth"
                            }
                        ]
                    ]
                }
            ]
        }
    }
  6. Write the visualization code.
    1. Install dependencies from the project root.
      CODE
      npm install
    2. Replace visualizations/bar_chart/src/visualization.js with the following code.
      JAVASCRIPT
      import { VisualizationAPI } from '@splunk/dashboard-extension';
      import './visualization.css';
      
      const container = document.createElement('div');
      container.className = 'bar-chart';
      document.getElementById('root').appendChild(container); 
      
      const state = {
          data: null,
          loading: false,
          options: {},
      };
      
      function render() {
          container.innerHTML = '';
      
          if (state.loading) {
              container.textContent = 'Loading...';
              return;
          }
      
          // data shape: { fields: [{name}, {name}], columns: [[...labels], [...values]] }
          // columns[fieldIndex][rowIndex] -- all values are strings
          const { fields, columns } = state.data ?? {};
          if (!columns || columns.length < 2 || columns[0].length === 0) {
              container.textContent = 'No data';
              return;
          }
      
          const labels = columns[0];
          const rawValues = columns[1].map(v => parseFloat(v));
      
          if (rawValues.some(isNaN)) {
              VisualizationAPI.setError(
                  `Expected "${fields[1].name}" to contain numeric values.`
              );
              return;
          }
          VisualizationAPI.clearError();
      
          const barColor = state.options.barColor ?? '#4e9cf5';
          const maxBarWidth = parseFloat(state.options.maxBarWidth) || 400;
          const domainMax = Math.max(...rawValues);
      
          labels.forEach((label, i) => {
              const value = rawValues[i];
              const barWidth = domainMax > 0 ? (value / domainMax) * maxBarWidth : 0;
      
              const row = document.createElement('div');
              row.className = 'bar-row';
      
              const labelEl = document.createElement('span');
              labelEl.className = 'bar-label';
              labelEl.textContent = label;
      
              const track = document.createElement('div');
              track.className = 'bar-track';
      
              const bar = document.createElement('div');
              bar.className = 'bar-fill';
              bar.style.width = `${barWidth}px`;
              bar.style.backgroundColor = barColor;
      
              const valueEl = document.createElement('span');
              valueEl.className = 'bar-value';
              valueEl.textContent = value;
      
              track.appendChild(bar);
              row.appendChild(labelEl);
              row.appendChild(track);
              row.appendChild(valueEl);
              container.appendChild(row);
          });
      }
      
      VisualizationAPI.addDataSourcesListener(
          ({ dataSources, loading }) => {
              state.loading = loading;
              state.data = dataSources?.primary?.data ?? null;
              render();
          },
          { invokeImmediately: true }
      );
      
      VisualizationAPI.addOptionsListener(({ options }) => {
          state.options = options;
          render();
      });

      addDataSourcesListener and addOptionsListener are called once at startup and fire on every subsequent state change. invokeImmediately: true on the data listener triggers an initial call as soon as the first data payload arrives from Dashboard Studio.

      The visualization keeps a local state object and calls render() from both listeners so that either a data change or an option change produces an updated render. This pattern is standard for JavaScript visualizations with multiple state sources.

    3. Replace visualizations/bar_chart/src/visualization.css with the following stylesheet.
      JSON
      .bar-chart {
          display: flex;
          flex-direction: column;
          gap: 8px;
          padding: 16px;
          box-sizing: border-box;
          width: 100%;
          height: 100%;
          overflow-y: auto;
      }
      
      .bar-row {
          display: flex;
          align-items: center;
          gap: 8px;
      }
      
      .bar-label {
          width: 120px;
          flex-shrink: 0;
          font-size: 13px;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
      }
      
      .bar-track {
          flex: 1;
      }
      
      .bar-fill {
          height: 20px;
          border-radius: 2px;
          transition: width 0.2s ease;
      }
      
      .bar-value {
          width: 48px;
          flex-shrink: 0;
          font-size: 13px;
          text-align: right;
      }
  7. Build the visualization.
    CODE
    yarn build
    The build output is dist/bar_chart/visualization.js. Rebuild after any source change before packaging or testing.
  8. Package and deploy the app.
    CODE
    yarn package

    This command generates dist/bar_chart_app-1.0.0-<hash>.spl and auto-generates default/visualizations.conf.

    CODE
    [bar_chart]
    label = Bar Chart
    description = Renders search results as horizontal bars
    framework_type = studio_visualization
    1. In Splunk, go to Apps > Manage Apps > Install app from file.
    2. Upload the .spl file.
    3. Restart Splunk if prompted.
  9. Test the visualization in Dashboard Studio.
    1. Open Dashboard Studio and create a new dashboard.
    2. Run a search that returns 2 fields, a label and a count, such as:
      CODE
      index=_internal | stats count by sourcetype
    3. Select Bar Chart from the visualization picker.
    4. Open the editor panel to adjust the bar color and maximum bar width.

React variant

You can implement the same visualization with the React template.

JAVASCRIPT
import { useDataSources, useOptions } from '@splunk/dashboard-extension/react';
import { createRoot } from 'react-dom/client';

function BarChart() {
    const { dataSources, loading } = useDataSources();
    const { options } = useOptions();

    if (loading) return <div>Loading...</div>;

    const { fields, columns } = dataSources?.primary?.data ?? {};
    if (!columns || columns.length < 2 || columns[0].length === 0) {
        return <div>No data</div>;
    }

    const labels = columns[0];
    const values = columns[1].map(v => parseFloat(v));
    const domainMax = Math.max(...values);
    const barColor = options.barColor ?? '#4e9cf5';
    const maxBarWidth = parseFloat(options.maxBarWidth) || 400;

    return (
        <div className="bar-chart">
            {labels.map((label, i) => {
                const barWidth = domainMax > 0 ? (values[i] / domainMax) * maxBarWidth : 0;
                return (
                    <div key={i} className="bar-row">
                        <span className="bar-label">{label}</span>
                        <div className="bar-track">
                            <div
                                className="bar-fill"
                                style={{ width: barWidth, backgroundColor: barColor }}
                            />
                        </div>
                        <span className="bar-value">{values[i]}</span>
                    </div>
                );
            })}
        </div>
    );
}

createRoot(document.getElementById('root')).render(<BarChart />);

The hooks are thin wrappers over the same VisualizationAPI listeners. Packaging and deployment are identical to the JavaScript path.