// Hard rules for Splunk custom visualization development — every rule learned the hard way from shipping icon_library, infographic_shapes, and 30+ viz apps. Organized by severity: FATAL (viz won't load), BROKEN (renders wrong), REJECTED (fails AppInspect), COSMETIC (works but looks bad). MUST be loaded before writing any visualization_source.js, formatter.html, webpack.config.js, or app.conf. Skipping these rules produces vizs that silently fail in Splunk.
[HINT] Download the complete skill directory including SKILL.md and all related files
name
vp-ref-gotchas
description
Hard rules for Splunk custom visualization development — every rule learned the hard way from shipping icon_library, infographic_shapes, and 30+ viz apps. Organized by severity: FATAL (viz won't load), BROKEN (renders wrong), REJECTED (fails AppInspect), COSMETIC (works but looks bad). MUST be loaded before writing any visualization_source.js, formatter.html, webpack.config.js, or app.conf. Skipping these rules produces vizs that silently fail in Splunk.
vp-ref-gotchas — hard rules for custom viz development
Every rule below was learned from real bugs, real AppInspect failures,
and real hours of debugging. They are organized by severity — what
happens if you violate the rule.
MUST load this skill before writing ANY viz code. No exceptions.
FATAL — viz won't load at all
F1. Webpack must target ES5
Splunk's AMD loader requires ES5. Webpack 5+ defaults to ES2015+.
If the bundle contains arrow functions, the viz silently fails to load.
head -c 200 visualization.js
# MUST start with: define(["api/SplunkVisualizationBase"], function(# MUST NOT contain: => or const or let
If you see ( instead of function( in the AMD wrapper, the build is
broken.
F2. Fonts must be base64 data URIs
Custom vizs render inside an <iframe> with src="about:srcdoc".
The iframe origin is null. ALL external font requests are blocked
by CORS — including URLs to the same Splunk server.
url('./fonts/font.woff2') — relative paths rewritten, still CORS
JavaScript FontFace API with fetch — same CORS origin issue
F3. Source must be pure ES5
No const, let, arrow functions, template literals, destructuring,
for...of, async/await, classes, or spread syntax anywhere in
visualization_source.js. Use var, function, string concatenation,
for loops.
// WRONGconst color = config[`${ns}color`] || '#fff';
items.forEach(item => { ... });
// CORRECTvar color = config[ns + 'color'] || '#fff';
for (var i = 0; i < items.length; i++) { ... }
F4. getInitialDataParams must use ROW_MAJOR_OUTPUT_MODE
getInitialDataParams MUST return outputMode: SplunkVisualizationBase.ROW_MAJOR_OUTPUT_MODE.
Using the string 'json' silently delivers data in a different structure — the viz receives
an object without fields/rows, every fieldIndex() returns -1, and every viz renders
its "No data" fallback with no error in the console.
// WRONG — data arrives in wrong format, viz shows "No data"getInitialDataParams: function() {
return { outputMode: 'json', count: 10000 };
}
// CORRECT — data arrives as { fields: [{name:...}], rows: [[...]] }getInitialDataParams: function() {
return {
outputMode: SplunkVisualizationBase.ROW_MAJOR_OUTPUT_MODE,
count: 10000
};
}
SplunkVisualizationBase is available as the AMD module parameter — use
the constant, not a string. Valid modes:
SplunkVisualizationBase.RAW_OUTPUT_MODE — raw JSON
NEVER use 'json', 'xml', or any arbitrary string.
NEVER set outputMode as a property on the extend object literal:
// WRONG — property evaluated at extend() time, may not resolveSplunkVisualizationBase.extend({
outputMode: SplunkVisualizationBase.ROW_MAJOR_OUTPUT_MODE, // BAD// ...
});
// CORRECT — always inside getInitialDataParams methodSplunkVisualizationBase.extend({
getInitialDataParams: function() {
return {
outputMode: SplunkVisualizationBase.ROW_MAJOR_OUTPUT_MODE,
count: 10000
};
},
// ...
});
getInitialDataParams is REQUIRED, not optional. Without it, Splunk
doesn't know how to deliver data to the viz — you get "Unknown output
mode: undefined".
F5. Only externalize what you import
// If you only use SplunkVisualizationBase:externals: ['api/SplunkVisualizationBase']
// Only add SplunkVisualizationUtils if you actually import it:externals: ['api/SplunkVisualizationBase', 'api/SplunkVisualizationUtils']
Externalizing unused modules wastes an AMD slot and webpack emits
warnings.
F6. Source MUST use require(), NEVER define()
Webpack's libraryTarget: 'amd' wraps the output in define().
If the source ALSO uses define(), you get a double AMD wrapper
that breaks RequireJS — every viz shows REQUIREJS_ERROR_MESSAGE Script error.
// WRONG — double AMD wrapper, viz won't loaddefine([
'api/SplunkVisualizationBase'
], function(SplunkVisualizationBase) {
var theme = require('shared/theme');
returnSplunkVisualizationBase.extend({ ... });
});
// CORRECT — webpack adds the AMD wrappervarSplunkVisualizationBase = require('api/SplunkVisualizationBase');
var theme = require('shared/theme');
module.exports = SplunkVisualizationBase.extend({ ... });
F7. MUST use extend({...}) object literal, NEVER prototypal constructors
SplunkVisualizationBase.extend() expects a plain object with method
properties. Passing a constructor function does NOT register the
prototype methods — the viz loads but shows no data (blank panel).
F8. Images must be bundled in the app, never external URLs
Dashboard hero images, logos, and brand assets MUST be downloaded
and placed in appserver/static/images/. External URLs fail on
Splunk instances with domain allowlists, air-gapped environments,
and Splunk Cloud — where outbound requests are blocked.
Download the image during build, save to appserver/static/images/,
and reference via the Splunk static path. This also eliminates
load-time latency from external CDNs.
F9. Vizs MUST be in appserver/static/visualizations/, NOT default/visualizations/
Splunk loads custom viz bundles from appserver/static/visualizations/.
Putting them in default/visualizations/ causes REQUIREJS_ERROR_MESSAGE
on every viz with no useful error in the console.
WRONG — REQUIREJS Script error, viz won't load:
{app}/default/visualizations/{viz_name}/visualization.js
RIGHT — Splunk finds and loads the viz:
{app}/appserver/static/visualizations/{viz_name}/visualization.js
This is the #1 cause of "all vizs show Script error" after install.
default/ is for conf files only. Viz JS/HTML/CSS go under appserver/.
F10. No jQuery — use standard DOM APIs only
Dashboard Studio v2 custom vizs render inside a sandboxed iframe where
jQuery is NOT available. this.$el is undefined. Any jQuery call
crashes the viz at initialization.
// WRONG — TypeError: Cannot read properties of undefinedthis.$el.addClass('my-viz');
this.$el.find('.tooltip').remove();
$('.container').on('click', handler);
// CORRECT — standard DOM APIsthis.el.className = (this.el.className || '') + ' my-viz';
var tooltip = this.el.querySelector('.tooltip');
if (tooltip) tooltip.parentNode.removeChild(tooltip);
this.el.addEventListener('click', handler);
Rule: NEVER use this.$el, $.fn, jQuery, or any jQuery syntax.
Use document.createElement, querySelector, addEventListener,
className, style.cssText, etc.
F11. Webpack 5 IIFE may fail in Dashboard Studio v2 sandbox — flat AMD alternative
Webpack 5 wraps modules in an IIFE inside the AMD factory. Splunk's
RequireJS + iframe sandbox + cross-origin restrictions can cause this
nested structure to fail silently as REQUIREJS_ERROR_MESSAGE Script error.
If webpack bundles cause Script error despite correct config (F1),
use flat AMD builds instead:
Build with build_flat.js (see vp-create for the script):
Reads shared/theme.js
Strips require() and module.exports lines from viz source
Converts module.exports = X; to return X;
Wraps in define(["api/SplunkVisualizationBase"], function(...) { ... });
Inlines theme.js as an IIFE
When to use flat vs webpack:
Try webpack first (F1 config) — it works for most packs
Switch to flat AMD if you get persistent Script errors in the
sandboxed iframe that F1-F9 don't explain
F12. Formatter HTML must use Splunk components, NEVER raw HTML
Splunk's visualization framework only reads its own custom elements.
Raw HTML (<div>, <input>, <select>, <label>, <h3>) is
silently ignored — the Format panel shows NO settings.
<!-- WRONG — Splunk ignores all of this --><divclass="section"><h3>Data Fields</h3><divclass="form-row"><label>Label field</label><inputtype="text"name="myapp.myviz.labelField" /></div></div><!-- ALSO WRONG — no wrapper tags allowed --><html><body><formclass="splunk-formatter-section"...></form></body></html><!-- RIGHT — bare forms with Splunk components --><formclass="splunk-formatter-section"section-label="Data configurations"><splunk-control-grouplabel="Label field"help="SPL field for row labels"><splunk-text-inputname="myapp.myviz.labelField"default="label"></splunk-text-input></splunk-control-group></form>
Allowed components only:
<splunk-text-input> — text fields
<splunk-radio-input> with <option> children — radio/dropdown/boolean
<splunk-color-picker> with <splunk-color> children — color swatches
<splunk-range-input> — slider (rare)
Forbidden elements:<div>, <input>, <select>, <textarea>,
<label>, <h1>-<h6>, <span>, <style>, <script>. No CSS, no
JavaScript. No <html> or <body> wrappers.
Every <splunk-control-group> MUST have help="..." attribute.
BROKEN — renders but wrong
B1. Canvas font rendering requires explicit wait
CSS @font-face registers the font but Canvas 2D does NOT auto-swap
when the font loads. If you draw text before the font is ready, you
get tofu glyphs that NEVER update.
var _fontReady = false;
var _fontPending = false;
functionloadFont(fontFamily, onReady) {
if (_fontReady) { onReady(); return; }
if (typeofdocument === 'undefined' || !document.fonts ||
!document.fonts.load) {
setTimeout(onReady, 200);
return;
}
if (!_fontPending) {
_fontPending = true;
document.fonts.load('400 48px "' + fontFamily + '"').then(function() {
_fontReady = true;
});
}
var attempts = 0;
var poll = function() {
attempts++;
if (_fontReady || attempts > 30) {
_fontReady = true;
onReady();
return;
}
setTimeout(poll, 100);
};
poll();
}
Do NOT use document.fonts.ready — it resolves when ALL currently
loading fonts finish, not when YOUR specific font is ready. Use
document.fonts.load() targeting your exact font.
B2. HiDPI canvas scaling is mandatory
Without this, canvas renders at 1x and looks blurry on Retina/4K.
var dpr = window.devicePixelRatio || 1;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// ALL drawing uses w, h (CSS pixels), NOT canvas.width/height
B3. getOption helper is mandatory
Dashboard Studio may pass formatter-changed values as short keys
(without namespace) while initial JSON values use full namespace.
Direct config[ns + 'key'] misses the short-key path.
functiongetOption(config, ns, key, defaultValue) {
var v = config[ns + key];
if (v !== undefined && v !== null) return v;
v = config[key];
if (v !== undefined && v !== null) return v;
return defaultValue;
}
functiongetNS(viz) {
try {
var info = viz.getPropertyNamespaceInfo();
if (info && info.propertyNamespace) return info.propertyNamespace;
} catch (e) {}
return'';
}
Use getOption for EVERY config read. No exceptions.
B4. Never read config in formatData
formatData is called by Splunk's caching pipeline. Reading config
here causes stale/timing bugs. All config reading belongs in
updateView.
formatData: function(data, config) {
// ONLY data processing here — NO config readsif (!data || !data.rows || data.rows.length === 0) {
if (this._lastGoodData) returnthis._lastGoodData;
thrownewSplunkVisualizationBase.VisualizationError(
'Awaiting data'
);
}
var fields = data.fields;
var colIdx = {};
for (var i = 0; i < fields.length; i++) {
colIdx[fields[i].name] = i;
}
var result = { colIdx: colIdx, rows: data.rows };
this._lastGoodData = result;
return result;
}
B5. Formatter section labels must be exact
Dashboard Studio merges formatter sections by matching section-labelcase-sensitively. Wrong casing creates duplicate groups.
<!-- CORRECT (exact match) --><formclass="splunk-formatter-section"section-label="Data configurations"><formclass="splunk-formatter-section"section-label="Data display"><formclass="splunk-formatter-section"section-label="Color and style">
Wrong
Right
"Data Configuration" (singular, capital C)
"Data configurations"
"Data Display" (capital D)
"Data display"
"Color and Style" (capital S)
"Color and style"
Structure rules:
No wrapper <div> around forms — bare <form> elements only
No nested <form> inside <form>
Every <splunk-control-group> MUST have help="..." attribute
B6. Canvas shadow state leaks
Shadow settings persist across draw calls. Always reset after use:
B7. JS defaults must match formatter HTML defaults
Splunk does NOT send formatter defaults on first load. If your JS
default for accentColor is #06B6D4 but the formatter says
value="#FF0000", the viz renders with cyan until the user touches
the color picker — then it jumps to red. Always keep them in sync.
B8. Auto-scale by default, explicit override at non-zero
NEVER hardcode pixel constants in _render(). Vizs must adapt to any
panel size — from 200×150 (compact tile) to 800×600 (full panel).
// WRONG — breaks at every size except the one you testedvarROW_H = 24;
varLABEL_W = 100;
varPAD = 12;
varFONT_SIZE = 14;
// RIGHT — scale from container dimensionsvar pad = Math.round(w * 0.03);
var rowH = Math.max(16, Math.floor((h - headerH - footerH) / rowCount) - gap);
var labelW = Math.round(w * 0.25);
var fontSize = Math.max(8, Math.round(rowH * 0.45));
Scaling formulas per viz type:
Viz type
Element
Formula
Floor
KPI tile
Value font
h * 0.35 (normal), h * 0.45 (hero)
18px / 28px
KPI tile
Label font
h * 0.08
7px
KPI tile
Padding
w * 0.06
8px
Gauge
Radius
Math.min(w, h) * 0.38
30px
Gauge
Tick font
radius * 0.06
8px
Bar/timeline
Row height
(availH - legendH) / rowCount - gap
16px
Bar/timeline
Label width
Measure longest label + 16px
40px
Table
Column width
panelWidth / colCount (proportional)
50px
Table
Row height
Math.max(20, (availH - headerH) / visibleRows)
20px
Any
Font size
Scale from container, Math.max(floor, calculated)
8px
The pattern: every size = Math.max(floor, containerDimension * ratio).
The floor prevents unreadable text. The ratio adapts to the panel.
For user-overridable sizes:0 = auto-scale (default), positive value
= explicit override:
var userSize = parseInt(getOption(config, ns, 'iconSize', '0'), 10);
var iconSize;
if (userSize > 0) {
iconSize = userSize;
} else {
iconSize = Math.max(16, Math.min(200, Math.min(w, h) * 0.6));
}
B9. Dashboard Studio type format
"type":"app_name.viz_name"
Format: {app_id}.{viz_name}. Nothing else.
Wrong
Why
viz.custom.app.viz
Not a valid prefix
custom.visualizations.app.viz
Internal namespace
splunk.custom.app.viz
splunk.* is for built-ins
splunk.viz_name
Only for built-in vizs
B10. Option namespace — three formats for three contexts
Common mistake — using savedsearches.conf format in formatter.html:
<!-- WRONG — long prefix, Splunk never reads this setting --><splunk-text-inputname="display.visualizations.custom.myapp.myviz.field"default="value"><!-- RIGHT — short format matches Dashboard Studio JSON --><splunk-text-inputname="myapp.myviz.field"default="value">
Dashboard Studio JSON and formatter.html both use the SHORT format
({app}.{viz}.key). Only savedsearches.conf uses the long prefix.
B11. parseFloat truncates string values
parseFloat("1:21.584") returns 1. parseFloat("+4.271s") returns
4. Any KPI-type viz that displays a single value MUST detect
non-numeric strings and display them as-is.
var rawStr = String(row[colIdx[field]]);
var rawValue = parseFloat(rawStr);
var isNumeric = !isNaN(rawValue) && String(rawValue) === rawStr.replace(/^[+\s]+/, '');
var displayValue;
if (!rawStr) {
displayValue = '—';
} elseif (!isNumeric) {
displayValue = rawStr; // lap times, gaps, formatted strings
} elseif (decimals >= 0) {
displayValue = rawValue.toFixed(decimals);
} else {
displayValue = fmtNum(rawValue, { compact: true });
}
Common values that break: 1:21.584 (lap time), +4.271s (gap),
P1 (position), DNS (did not start), 3.8% (percentage with unit
baked in).
B12. Gauge colors must match brand, not default green
The default gauge palette (green→yellow→red) is generic and
off-brand. Gauge segment colors MUST derive from the brand palette:
Red Bull: blue→lightblue→gold→red (navy #1E3A6E → sky #4A8FE7)
Disney+: blue→purple gradient (brand blue #0063E5)
Netflix: dark grey→red (cinema feel)
Use lerpColor(brandLow, brandHigh, segmentPct) for smooth branded
transitions. Reserve red for the actual red zone only.
B13. Canvas background must use clearRect, never fillRect
Vizs that fill the canvas with ctx.fillStyle = t.bg; ctx.fillRect(0, 0, w, h) paint an opaque background that overrides Dashboard Studio's
"backgroundColor": "transparent" option. The user loses control
over the panel background.
Let Dashboard Studio's backgroundColor option control the panel
background. Vizs should only draw their content on a clear canvas.
B14. Variables in _draw() are not accessible from sub-methods
If your viz splits rendering into _draw() → _drawCenter(),
_drawTicks(), etc., local variables in _draw() are NOT in scope
inside the sub-methods. This causes ReferenceError: x is not defined
at runtime.
// WRONG — gi is local to _draw, invisible to _drawCenter_draw: function(parsed, config) {
var gi = 0.8;
this._drawCenter(ctx, parsed); // gi is not defined!
},
_drawCenter: function(ctx, parsed) {
ctx.shadowBlur = 12 * gi; // ReferenceError
}
// CORRECT — store on this_draw: function(parsed, config) {
this._gi = 0.8;
this._drawCenter(ctx, parsed);
},
_drawCenter: function(ctx, parsed) {
ctx.shadowBlur = 12 * (this._gi || 1);
}
B15. Always include formatData in the extend object
Splunk calls formatData during its data pipeline. If missing, the
viz may silently fail or receive data in an unexpected format. Always
include it, even as a passthrough:
formatData: function(data) {
if (!data || !data.rows || data.rows.length === 0) {
if (this._lastGoodData) returnthis._lastGoodData;
return data;
}
var fields = data.fields;
var colIdx = {};
for (var i = 0; i < fields.length; i++) {
colIdx[fields[i].name] = i;
}
var result = { colIdx: colIdx, rows: data.rows };
this._lastGoodData = result;
return result;
},
B16. Every visual property should be configurable via formatter
If the viz code uses a color, size, toggle, or position — there SHOULD
be a corresponding setting for every visual property that the user might
want to customize. Not every internal rendering detail needs a formatter
control — focus on colors, sizes, labels, and behavioral toggles.
Mandatory formatter settings for every viz:
Category
Settings
Type
Theme
theme (dark/light)
radio
Colors
accentColor, colors (CSV hex)
color-picker / text
Typography
label, labelPlacement (top/bottom/left/none)
text / dropdown
Values
field, unit, unitPosition (before/after), decimals
text / radio
Trends
showDelta, deltaField
radio / text
Effects
showGlow (recommended); accentIntensity (0-100) RECOMMENDED for vizs with glow/shadow — lets users dial effects up or down; not required for vizs without accent effects
radio / text
Layout
alignment (left/center/right)
radio
Domain-specific settings (add when applicable):
Setting
When
Type
maxValue, redZoneStart
Gauges with threshold bands
text
segments
Segmented arcs
text
showPosition
Tables with ranking
radio
badgeField
Tables with category badges
text
showReadout, showLegend
Donuts/composition vizs
radio
maxRows
Tables
text
colors
Multi-series charts, donuts
text (CSV hex)
Test: for every var x = getOption(config, ns, 'something', default)
in the source, there MUST be a matching <splunk-control-group> in
formatter.html with the same key and a matching default value (B7).
Anti-pattern: hardcoding ctx.fillStyle = '#DC0000' anywhere
except as a fallback default. Colors come from theme tokens OR
formatter settings — never inline hex in render logic.
B17. setupCanvas must receive the container element, not the canvas
theme.setupCanvas(el) internally calls el.querySelector('canvas').
Passing this._canvas (the canvas itself) causes it to search inside
the canvas element, find nothing, create a NEW canvas appended to the
canvas, and get 0×0 dimensions.
Corollary: either let setupCanvas create the canvas in updateView,
OR create it manually in initialize — not both. If you create it
manually, don't call setupCanvas at all.
[]access = read : [ * ], write : [ admin, sc_admin ]
export = system
[visualizations/viz_name]export = system
Missing global [] stanza → blocked by
check_meta_default_write_access.
Missing sc_admin → blocked by check_kos_are_accessible (Cloud
has no admin role).
R3. No macOS artifacts in tarball
macOS tar silently adds ._ resource fork entries to the archive.
Splunk sees these as extra top-level directories and rejects the
upload: "archive contains more than one immediate subdirectory:
and {app_name}".
visualizations.conf is a Splunk-defined conf file. Adding
[triggers] reload.visualizations = simple causes
check_for_trigger_stanza failure on Cloud.
INTERACTIVE — must have for production
I1. Hover tooltip is mandatory on data-displaying vizs
Every viz that displays DATA must show a tooltip on hover. Decorative
vizs (background shapes, texture overlays) don't need tooltips. Canvas
has no built-in tooltip — use a DOM element positioned at the cursor.
initialize: function() {
// ... after canvas creation ...this._tooltip = document.createElement('div');
this._tooltip.style.cssText =
'position:absolute;display:none;padding:6px 10px;' +
'border-radius:4px;pointer-events:none;white-space:nowrap;' +
'z-index:100;';
// NO hardcoded font/color — set in _render() from theme tokens:// this._tooltip.style.background = t.panelHi;// this._tooltip.style.color = t.text;// this._tooltip.style.fontFamily = theme.FONTS.data;// this._tooltip.style.fontSize = '11px';this.el.style.position = 'relative';
this.el.appendChild(this._tooltip);
var self = this;
this.canvas.addEventListener('mousemove', function(e) {
self._onMouseMove(e);
});
this.canvas.addEventListener('mouseleave', function() {
self._tooltip.style.display = 'none';
});
},
_onMouseMove: function(e) {
var rect = this.canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
// Hit-test against drawn elements (viz-specific logic)var hit = this._hitTest(mx, my);
if (hit) {
this._tooltip.textContent = hit.label + ': ' + hit.value;
this._tooltip.style.display = 'block';
this._tooltip.style.left = (mx + 12) + 'px';
this._tooltip.style.top = (my - 8) + 'px';
this.canvas.style.cursor = 'pointer';
} else {
this._tooltip.style.display = 'none';
this.canvas.style.cursor = 'default';
}
},
Hit-test patterns per viz type:
KPI tile: single hit zone = entire panel. Show field name + value.
Ring gauge: arc region. Show percentage + raw value.
Donut: angle-based. Compute angle from center, match to segment.
Area chart: x-position to data index. Show all series values at
that time point.
Table: row index from y-position. Highlight row, show full row
data.
Always clean up in destroy:
destroy: function() {
if (this._tooltip && this._tooltip.parentNode) {
this._tooltip.parentNode.removeChild(this._tooltip);
}
// ... other cleanup ...
}
I2. Hover highlight on charts and tables
Beyond the tooltip, hovering should visually highlight the element:
Donut: increase segment opacity or add stroke
Area chart: draw vertical crosshair line + data point dots
Table: brighten row background
Gauge: show exact value label near the arc
Store hit regions during _render and re-use in _hitTest.
COSMETIC — works but looks wrong
COSMETIC rules prevent visual bugs. For design quality guidelines
(color choices, typography, spacing), see vp-couture and
vp-ref-patterns.
C1. Panel backgroundColor must be transparent
You CANNOT control the Dashboard Studio panel background from inside
the viz. It must be set in the dashboard JSON:
backgroundColor is a built-in Studio option (no namespace prefix).
Document this in every README and demo dashboard.
C2. MutationObserver hides Splunk placeholders
Vizs that render without data (static icons, decorative elements) get
overlaid with Splunk's "no results" placeholder. Hide it:
initialize: function() {
var self = this;
this._observer = newMutationObserver(function() {
var nodes = self.el.querySelectorAll(
'.viz-placeholder, .shared-viz-no-results, ' +
'[data-test="viz-no-results"], .viz-controller-no-results'
);
for (var i = 0; i < nodes.length; i++) {
nodes[i].style.display = 'none';
}
});
this._observer.observe(this.el, { childList: true, subtree: true });
},
destroy: function() {
if (this._observer) this._observer.disconnect();
}
C3. Cursor pointer on drilldown
if (drilldownEnabled) {
this.el.style.cursor = 'pointer';
this.canvas.style.cursor = 'pointer';
}
Without this, users don't know the viz is clickable.
C4. Drilldown must be wrapped in try/catch
try {
self.drilldown({
action: SplunkVisualizationBase.FIELD_VALUE_DRILLDOWN,
data: payload
}, event);
} catch (e) { /* test harness has no drilldown infra */ }
C5. Animation timers must be cleaned up in destroy
destroy: function() {
if (this._animTimer) {
clearInterval(this._animTimer);
this._animTimer = null;
}
if (this._observer) this._observer.disconnect();
SplunkVisualizationBase.prototype.destroy.apply(this, arguments);
}
C6. Reflow should use direct re-render
reflow: function() {
if (this._lastConfig) {
this._render(this._lastData, this._lastConfig);
}
}
Faster and flicker-free. Cache _lastData and _lastConfig in
updateView.
IMPORTANT: verify the actual render method name before writing
reflow. The method could be _render, _draw, _update, or
inline in updateView. Calling this._render() when the method
is named _draw() causes TypeError: this._render is not a function
on every resize.
// If rendering happens in updateView directly:reflow: function() {
if (this._lastData && this._lastConfig) {
this.updateView(this._lastData, this._lastConfig);
}
}
C7. Viz app name must match the brand/project
The app ID and viz stanza names create the Dashboard Studio type
prefix: {app_id}.{viz_name}. Always name the app after the
brand/project so the dashboard JSON reads naturally:
disney_plus_viz.kpi_tile ← clear: this is a Disney+ KPI
f1_viz_pack.ers_gauge ← clear: this is an F1 ERS gauge
soc_viz_pack.threat_radar ← clear: this is a SOC threat radar
custom_viz.kpi_tile ← bad: what brand? generic
my_viz.gauge ← bad: meaningless
The app ID appears in every "type": reference in every dashboard
JSON. Make it count.
C8. Increment build in app.conf for every release
Splunk caches static assets keyed by a hash derived from build in
app.conf. Same build number = cached old JS/CSS served despite
new install. Different build = fresh load.
Always increment before packaging. Also hard-refresh browser
(Cmd+Shift+R / Ctrl+Shift+R) after installing.
C9. rx on splunk.rectangle must be a number, not a string
Dashboard Studio schema validation rejects "rx": "8" (string).
Use "rx": 8 (number). Same applies to ry, strokeWidth, and
other numeric options in dashboard JSON.
Error diagnosis flowchart
When a custom viz shows a placeholder icon or blank panel, follow this
tree to find the root cause:
Viz shows placeholder icon (bar chart in grey box)
├── Console: "Script error for .../visualization.js"
│ ├── File not found? → F9 (wrong directory)
│ ├── Webpack IIFE? → F11 (use flat AMD)
│ ├── jQuery used? → F10 (no $el in DS v2)
│ └── Double AMD wrapper? → F6 (source uses define())
├── Console: "Unknown output mode: undefined"
│ ├── getInitialDataParams missing? → F4 (required method)
│ └── outputMode as property? → F4 (must be in method)
├── Console: "X is not a function"
│ ├── _render? → C6 (wrong method name in reflow)
│ ├── addClass? → F10 (jQuery)
│ └── constructor? → F7 (must use extend object literal)
├── No console errors but blank
│ ├── setupCanvas wrong element? → B17
│ ├── formatData returns null? → B15
│ └── Canvas dimensions 0×0? → check el.getBoundingClientRect()
└── Changes not taking effect → C8 (build number + hard refresh)
Console noise to IGNORE (Splunk framework, not your bugs):
SecurityError: Failed to read 'cookie' — sandboxed iframe