Skip to content

Test and troubleshoot

Test your graphic before you push it. npx ligr-graphic dev runs a local harness that needs no API key. The viewer frames your graphic, sends the same message order as the LIGR overlay, and logs every message. It needs no dashboard login. Preview controls change local test state; they do not publish or update a saved graphic.

Terminal
npm run dev

npm run dev runs npx ligr-graphic dev in a scaffolded graphic. Open http://localhost:8080.

The harness sends HELLO on frame load, waits for READY, then sends LOAD_GRAPHIC, and a PING every 10 seconds after that. That is the order and the cadence the overlay uses. The panel next to the frame holds:

ControlWhat it sends
Data → scenario pickerA GRAPHIC_UPDATE with the first step of the scenario
Previous, NextA GRAPHIC_UPDATE with the sport data of that step. Step n is steps 0..n merged
HideGRAPHIC_HIDE, then a GRAPHIC_UPDATE with show: false. The log reports when HIDDEN arrives against exitDurationMs
ShowA GRAPHIC_UPDATE with show: true
Play, PauseAdvance scenario steps once per second, or stop playback
ControlsEdit declared control variables and send their current values
DataInspect the current sport data and scenario step
FilesInspect the available local file names and sizes
ResetRestore the first step, default controls, and visible state, then reload the frame
The logEvery message in both directions, with a warning for a seq gap and for a missing PONG

With LIGR_API_KEY set, the harness loads the scenarios of the sport you pass with --sport and the real JSON Schema of every surface from LIGR. Without a key it uses one bundled football scenario. The scenarios are the ones the LIGR graphics builder uses. See Schemas & scenarios.

With a start script in package.json, the harness runs it on the next free port and frames that server. Any development server can use this path; it must serve runnable browser files on the supplied port. Without a start script, the harness serves dist/ and reloads the frame when a file under dist/ changes.

Terminal
npx ligr-graphic dev --port 8080 --sport football

Save this file as harness.html next to your dist/ folder. Serve the folder from any static file server, then open the harness in a browser. It sends the same first messages as npx ligr-graphic dev, with one sample payload and no scenarios.

Terminal
python3 -m http.server 8080
open http://localhost:8080/harness.html
harness.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 14px monospace; margin: 0; display: flex; height: 100vh; }
#stage { position: relative; width: 960px; height: 540px; background: #223; flex: none; }
#stage iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
#panel { flex: 1; display: flex; flex-direction: column; padding: 8px; }
#log { flex: 1; overflow: auto; white-space: pre-wrap; background: #111; color: #9f9; padding: 8px; }
button { margin: 0 4px 8px 0; }
</style>
</head>
<body>
<div id="stage"><iframe id="frame" src="dist/index.html"></iframe></div>
<div id="panel">
<div>
<button id="btnScore">Update score</button>
<button id="btnHide">Hide</button>
<button id="btnShow">Show</button>
</div>
<div id="log"></div>
</div>
<script>
const PROTOCOL = 'ligr.gfx.v1'
const frame = document.getElementById('frame')
const log = document.getElementById('log')
let sessionId = null, seq = 0, ready = false, pingTimer = null
let homeScore = 0, awayScore = 0
const samplePayload = () => ({
graphicId: 'demo-graphic-1',
show: true,
sport: 'football',
sportData: {
'1': { score: homeScore, abbreviation: 'HOM', logoUrl: '', kit: { primaryColor: '#c00' } },
'2': { score: awayScore, abbreviation: 'AWY', logoUrl: '', kit: { primaryColor: '#00c' } },
clock: '12:00',
lastClock: '12:00',
clockRunning: true,
periodShortName: 'First Half',
},
controlVariables: { hide: false },
controlVariableData: { hide: { value: false } },
themeVariables: {},
externalData: {},
images: { player: [], team: [], competition: [] },
userExpressionValues: {},
})
function writeLog(dir, msg) {
log.textContent += `${dir} ${msg.type} ${JSON.stringify(msg.payload)}\n`
log.scrollTop = log.scrollHeight
}
function send(type, payload) {
const msg = { protocol: PROTOCOL, sessionId, seq: ++seq, type, payload }
frame.contentWindow.postMessage(msg, '*')
writeLog('→', msg)
}
frame.addEventListener('load', () => {
sessionId = `cg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
seq = 0
ready = false
clearInterval(pingTimer)
send('HELLO', { width: 1920, height: 1080 })
})
window.addEventListener('message', event => {
const msg = event.data
if (!msg || msg.protocol !== PROTOCOL || msg.sessionId !== sessionId) return
writeLog('←', msg)
if (msg.type === 'READY' && !ready) {
ready = true
send('LOAD_GRAPHIC', samplePayload())
pingTimer = setInterval(() => send('PING', {}), 10_000)
}
})
document.getElementById('btnScore').addEventListener('click', () => {
homeScore += 1
send('GRAPHIC_UPDATE', { graphicId: 'demo-graphic-1', show: true, sport: 'football', sportData: samplePayload().sportData })
})
document.getElementById('btnHide').addEventListener('click', () => {
send('GRAPHIC_HIDE', { graphicId: 'demo-graphic-1' })
})
document.getElementById('btnShow').addEventListener('click', () => {
send('GRAPHIC_UPDATE', { graphicId: 'demo-graphic-1', show: true, sport: 'football' })
})
</script>
</body>
</html>

Empty schema objects are fine for a local test. The overlay sends full JSON Schema. For real sample data, fetch GET /v2/scenarios/{sport} and paste one scenario into samplePayload.

Open a code graphic from your theme to inspect it in the dashboard viewer. The dashboard viewer requires a session with access to the owning organization. It provides Controls, Data, and Files, plus scenario playback and Show, Hide, and Reset.

A working version uses the current saved draft. A numbered version loads that published snapshot and its pinned files. Preview controls only change the current preview. They do not edit the published version. Use Open working version when you need the draft after inspecting a published version.

After you push and publish, open a control room of a competition that uses the theme. Your graphic appears in the graphic list with its control variables. Show it on the preview overlay of a test match before you use it on air.

The overlay frames your graphic in a sandbox, so the browser cannot inspect the frame from the page. Add ?debugCodeGraphics=1 to an overlay URL to see each data message that your graphic receives. Use one of these overlay URLs from the control room of a test match:

URLWhere to get it
…/monitoring-{key}?debugCodeGraphics=1Copy monitoring link
…/preview-{key}?debugCodeGraphics=1Preview Link in the control room header
  1. Open the overlay URL with ?debugCodeGraphics=1 in a desktop browser.
  2. Open the browser console and show messages at the Verbose level.
  3. Find the [CodeGraphicIframe] payload line with type: 'LOAD_GRAPHIC'. It holds the full payload of the load.
  4. Paste this listener to print each later message as an object:
Browser console
addEventListener('ligr:code-graphic-payload', e => console.log(e.detail))

Each event has graphicId, type, seq, and payload. type is LOAD_GRAPHIC or GRAPHIC_UPDATE. A GRAPHIC_UPDATE payload holds only the changed surfaces. seq is the same value that your graphic receives.

The overlay reads the flag when it loads a graphic. If you add the flag to an open overlay, reload the page. The flag changes no graphic output, and it sends no network request.

Check every item before you push a version for a broadcast.

  • The handshake, HELLO through LOAD_GRAPHIC, completes within 2 seconds.
  • The graphic renders correctly from LOAD_GRAPHIC alone, with no earlier GRAPHIC_UPDATE.
  • The graphic updates correctly on a partial GRAPHIC_UPDATE, for example a score change with no clock change.
  • The graphic sends HIDDEN within exitDurationMs of GRAPHIC_HIDE.
  • The browser console shows no error across a full show and hide cycle.
  • The graphic makes no network call at runtime.
  • The built dist/ folder is under 10 MiB, and no file is over 5 MiB.
  • The <body> background stays transparent in every state.
  • The animation holds 60 frames per second during show and hide.
SymptomLikely causeFix
The frame stays blankThe entry file never sends READY, or a script error stopped itOpen the browser console. Check the harness log for a missing READY
The graphic never receives dataYou echoed the wrong sessionId, or you never sent READYCopy sessionId from HELLO exactly. Send READY before you expect LOAD_GRAPHIC
The graphic never hidesYou never send HIDDEN, or exitDurationMs is shorter than your animationSend HIDDEN when the out-animation ends. Raise exitDurationMs in the manifest
A message fails with a 1 MB errorA payload, usually externalData, is too largeTrim the data source. Load large assets from the bundle instead of the payload
Fonts do not loadA font path is absolute, or the font is missing from the bundleUse relative paths. Ship the font file inside dist/
The wrong team shows on the leftThe home and away fields are swappedsportData['1'] is the home team. sportData['2'] is the away team
CODE_GRAPHIC_INVALID on push or publishThe manifest breaks a rule, or a file was not uploaded in the session you namedRead details[]. Each line is one problem. See The manifest
409 LOCKED on push or publishA person has the graphic open in the dashboard editor, or a stale lock has not expiredRead holder.userName. Wait up to 60 seconds, then retry
404 on a theme you expect to reachThe API key belongs to another organizationUse a key of the organization that owns the theme
The frame is blank on the overlay but fine in the harnessThe bundle did not upload, or the entry file is missingGET …/code-graphics/{graphicId} lists assets. Check the entry file is there

Use Issues and support for code graphics, SDK, and CLI reports. General app problems, including broken pages, login, and billing, go to LIGR support.