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:
-
Setting up the project
-
Implementing the visualization
-
Exposing configurable options
-
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.
- Create a new empty directory and navigate into it:
mkdir bar_chart_app
cd bar_chart_app
- Scaffold the project with the dashboard extension template.
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.
- Review the generated project structure.
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
- Configure the Splunk app in package/app/app.conf.
[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
- 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.
{
"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"
}
]
]
}
]
}
}
- Write the visualization code.
- Install dependencies from the project root.
- Replace
visualizations/bar_chart/src/visualization.js with the following code.
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.
- Replace
visualizations/bar_chart/src/visualization.css with the following stylesheet.
.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;
}
- Build the visualization.
The build output is dist/bar_chart/visualization.js. Rebuild after any source change before packaging or testing.
- Package and deploy the app.
This command generates dist/bar_chart_app-1.0.0-<hash>.spl and auto-generates default/visualizations.conf.
[bar_chart]
label = Bar Chart
description = Renders search results as horizontal bars
framework_type = studio_visualization
- In Splunk, go to Apps > Manage Apps > Install app from file.
- Upload the
.spl file.
- Restart Splunk if prompted.
- Test the visualization in Dashboard Studio.
- Open Dashboard Studio and create a new dashboard.
- Run a search that returns 2 fields, a label and a count, such as:
index=_internal | stats count by sourcetype
- Select Bar Chart from the visualization picker.
- 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.
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.