Close Editor Run Reset Auto Update CJS const lcjs = require('@lightningchart/lcjs')
const {
lightningChart,
Themes,
IndividualPointFill,
SolidLine,
DataSet,
ColorRGBA,
ColorHEX,
AxisTickStrategies,
AxisScrollStrategies,
SolidFill,
emptyLine,
synchronizeAxisIntervals,
emptyFill,
PointStyle3D,
TickStyle,
} = lcjs
const SCENARIO_START_MS = new Date().setHours(10, 45, 0, 0)
const PLAY_SVG = `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="vertical-align: middle;"><path d="M8 5v14l11-7z"/></svg>`
const PAUSE_SVG = `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" style="vertical-align: middle;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`
const NORMAL_STATE = `SYSTEM STATE: NORMAL`
let currentState = NORMAL_STATE
const eventLogs = []
const panelStates = []
const lc = lightningChart()
// Layout
const exampleContainer = document.getElementById('chart') || document.body
if (exampleContainer === document.body) {
exampleContainer.style.width = '100vw'
exampleContainer.style.height = '100vh'
exampleContainer.style.margin = '0px'
}
exampleContainer.style.overflow = 'hidden'
const gridContainer = document.createElement('div')
exampleContainer.append(gridContainer)
gridContainer.style.width = '100%'
gridContainer.style.height = '100%'
gridContainer.style.display = 'grid'
gridContainer.style.gridTemplateColumns = '1fr 1.2fr 1.2fr 1fr'
gridContainer.style.gridTemplateRows = 'auto 1fr 1fr 1fr'
gridContainer.style.gap = '6px'
gridContainer.style.padding = '6px'
gridContainer.style.justifyContent = 'stretch'
gridContainer.style.boxSizing = 'border-box'
const panelContainer = document.createElement('div')
gridContainer.append(panelContainer)
panelContainer.style.gridColumnStart = '1'
panelContainer.style.gridColumnEnd = '5'
panelContainer.style.gridRow = '1'
panelContainer.style.display = 'grid'
panelContainer.style.gridTemplateColumns = '1fr 1fr 1fr 1fr 1.5fr'
panelContainer.style.justifyItems = 'center'
panelContainer.style.alignItems = 'center'
panelContainer.style.fontSize = 'clamp(1rem, 1.5vw, 3rem)'
panelContainer.style.padding = '6px'
const playbackDiv = document.createElement('div')
panelContainer.appendChild(playbackDiv)
playbackDiv.style.gridColumn = '1'
playbackDiv.style.width = '100%'
playbackDiv.style.display = 'flex'
playbackDiv.style.alignItems = 'center'
playbackDiv.style.justifyContent = 'center'
playbackDiv.style.gap = '12px'
const timeMarker = document.createElement('span')
timeMarker.id = 'marker'
timeMarker.style.display = 'flex'
timeMarker.style.alignItems = 'center'
timeMarker.style.cursor = 'pointer'
timeMarker.style.fontSize = 'clamp(1rem, 2vw, 4rem)'
timeMarker.innerHTML = PAUSE_SVG
playbackDiv.appendChild(timeMarker)
const timeSlider = document.createElement('input')
timeSlider.type = 'range'
timeSlider.id = 'slider'
timeSlider.min = 0
timeSlider.max = 300000
timeSlider.step = 1000
timeSlider.value = timeSlider.min
timeSlider.style.width = '50%'
playbackDiv.appendChild(timeSlider)
const speedSelect = document.createElement('select')
speedSelect.id = 'select'
speedSelect.innerHTML = `
<option value="1">1x</option>
<option value="2">2x</option>
<option value="5">5x</option>
<option value="10">10x</option>
<option value="30">30x</option>
<option value="60">60x</option>
`
speedSelect.style.border = 'none'
speedSelect.style.padding = '4px 8px'
speedSelect.style.borderRadius = '4px'
speedSelect.style.cursor = 'pointer'
playbackDiv.appendChild(speedSelect)
const timeDiv = document.createElement('div')
panelContainer.appendChild(timeDiv)
timeDiv.innerHTML = `TIME: `
timeDiv.style.gridColumn = '2'
const depthDiv = document.createElement('div')
panelContainer.appendChild(depthDiv)
depthDiv.style.gridColumn = '3'
depthDiv.innerHTML = `DEPTH: `
const ropDiv = document.createElement('div')
panelContainer.appendChild(ropDiv)
ropDiv.style.gridColumn = '4'
ropDiv.innerHTML = `ROP: `
const stateDiv = document.createElement('div')
panelContainer.appendChild(stateDiv)
stateDiv.style.gridColumn = '5'
stateDiv.innerHTML = NORMAL_STATE
// LATERAL VIBRATION
const whirlContainer = document.createElement('div')
gridContainer.append(whirlContainer)
const whirlChart = lc
.Polar({
container: whirlContainer,
legend: { visible: false },
// theme: Themes.darkGold
})
.setTitle('LATERAL VIBRATION')
.setAnimationsEnabled(false)
.setPadding({ left: 0, right: 0, top: 0, bottom: 8 })
.setCursorFormatting((_, hit) => {
return [
[{ text: 'Direction', font: { weight: 'bold' } }, `${hit.angle.toFixed(0)} °`],
[{ text: 'Vibration', font: { weight: 'bold' } }, `${hit.amplitude.toFixed(2)} g`],
]
})
whirlContainer.style.gridColumn = '2'
whirlContainer.style.gridRow = '4'
whirlChart.getRadialAxis().setTitle('').setNorth(0).setClockwise(true).setMarginAfterTicks(0)
whirlChart.getAmplitudeAxis().setTitle('').setDefaultInterval({ start: 0, end: 15, stopAxisAfter: false })
const theme = whirlChart.getTheme()
// Dashboard colors
const colors = {
border: theme.isDark ? '#2a2d3e' : '#bad6cb',
background: theme.isDark ? '#090A0F' : '#e7ecf1',
divBg: theme.isDark ? '#090A0F' : theme.chartXYBackgroundFillStyle.stops[1].color.toRGBAString(),
text: theme.isDark ? '#00F0FF' : '#8400ff',
warnText: theme.isDark ? '#ffd500' : '#ffcc00',
criticalText: '#FF003C',
cyan: theme.isDark ? ColorHEX('#00F0FF') : ColorHEX('#008080'),
magenta: ColorHEX('#f000ff'),
purple: ColorRGBA(132, 0, 255),
purpleTransparent: ColorRGBA(132, 0, 255, 90),
grey: theme.isDark ? ColorRGBA(42, 45, 62) : ColorRGBA(42, 45, 62, 80),
sand: ColorRGBA(215, 160, 3, 15),
shale: ColorRGBA(100, 26, 5, 15),
transparent: ColorRGBA(0, 0, 0, 0),
warn: ColorHEX('#ffd500'),
critical: ColorHEX('#ff003c'),
}
const applyContainerStyle = (container) => {
container.style.width = '100%'
container.style.height = '100%'
container.style.minWidth = '0'
container.style.minHeight = '0'
container.style.boxSizing = 'border-box'
container.style.border = `2px solid ${colors.border}`
container.style.borderRadius = '4px'
container.style.overflow = 'hidden'
}
applyContainerStyle(panelContainer)
applyContainerStyle(whirlContainer)
exampleContainer.style.fontFamily = theme.chartXYTitleFont.family
gridContainer.style.background = colors.background
panelContainer.style.color = colors.text
panelContainer.style.background = colors.divBg
panelContainer.style.textShadow = `0px 0px 2px ${colors.text}, 0px 0px 6px ${colors.text}`
timeMarker.style.filter = `drop-shadow(0px 0px 4px ${colors.text})`
timeSlider.style.accentColor = colors.text
speedSelect.style.background = colors.divBg
speedSelect.style.color = colors.text
speedSelect.style.border = `2px solid ${colors.border}`
speedSelect.style.outline = 'none'
whirlChart.getRadialAxis().setTickStyle(
new TickStyle({
labelFillStyle: emptyFill,
gridStrokeStyle: theme.polarSectorStrokeStyle,
}),
)
const createChartXY = (chartContainer, title, position) => {
const chart = lc
.ChartXY({
container: chartContainer,
legend: { visible: false },
// theme: Themes.darkGold
})
.setCursorMode('show-nearest')
.setTitle(title)
applyContainerStyle(chartContainer)
chartContainer.style.gridColumnStart = `${position[0]}`
chartContainer.style.gridColumnEnd = `${position[1]}`
chartContainer.style.gridRowStart = `${position[2]}`
chartContainer.style.gridRowEnd = `${position[3]}`
return chart
}
const formatRealTime = (elapsedSeconds) => {
const currentTimeMs = SCENARIO_START_MS + elapsedSeconds * 1000
const currentDate = new Date(currentTimeMs)
const hours = currentDate.getHours().toString().padStart(2, '0')
const minutes = currentDate.getMinutes().toString().padStart(2, '0')
const seconds = currentDate.getSeconds().toString().padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}
// LITHOLOGY
const lithologyContainer = document.createElement('div')
gridContainer.append(lithologyContainer)
const lithologyChart = createChartXY(lithologyContainer, 'LITHOLOGY', [1, 1, 2, 5]).setAnimationsEnabled(false)
lithologyChart.axisX.setTitle('Gamma (API)').setTitlePosition('center').setInterval({ start: 0, end: 120 })
const lithologyAxisY = lithologyChart.axisY
.setTitle('Measured Depth (m)')
.setTitleRotation(270)
.setDefaultInterval({ start: 3451.8, end: 3450 })
.setMarginAfterTicks(12)
.setTickStrategy(AxisTickStrategies.Numeric, (tickStrategy) => tickStrategy.setTickStyle((ticks) => ticks.setLabelRotation(270)))
// DRILLING EFFICIENCY
const efficiencyContainer = document.createElement('div')
gridContainer.append(efficiencyContainer)
const efficiencyChart = createChartXY(efficiencyContainer, 'DRILLING EFFICIENCY', [2, 2, 2, 2])
efficiencyChart.setCursorMode('show-all').setAnimationsEnabled(false)
efficiencyChart.axisX.dispose()
const efficiencyAxisX = efficiencyChart
.addAxisX({})
.setTitle('Time (s)')
.setTickStrategy(AxisTickStrategies.Time, (strategy) => strategy.setFormattingFunction((x, range) => formatRealTime(x)))
.setKeepTickLabelsInAxisBounds(false)
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setDefaultInterval({ start: 0, end: 2, stopAxisAfter: false })
efficiencyChart.axisY.dispose()
const ropAxisY = efficiencyChart
.addAxisY()
.setTitle('ROP (m/h)')
.setInterval({ start: 0, end: 50 })
.setStrokeStyle(new SolidLine({ fillStyle: new SolidFill({ color: colors.cyan }) }))
const wobAxisY = efficiencyChart
.addAxisY({ opposite: true })
.setTitle('WOB (t)')
.setTickStrategy(AxisTickStrategies.Numeric, (tickStrategy) =>
tickStrategy.setTickStyle((ticks) => ticks.setGridStrokeStyle(emptyLine)),
)
.setInterval({ start: 0, end: 30 })
.setStrokeStyle(new SolidLine({ fillStyle: new SolidFill({ color: colors.magenta }) }))
// MECHANICAL DYNAMICS
const dynamicsContainer = document.createElement('div')
gridContainer.append(dynamicsContainer)
const dynamicsChart = createChartXY(dynamicsContainer, 'MECHANICAL DYNAMICS', [2, 2, 3, 3])
dynamicsChart.setCursorMode('show-all').setAnimationsEnabled(false)
dynamicsChart.axisX.dispose()
const dynamicsAxisX = dynamicsChart
.addAxisX({})
.setTitle('Time (s)')
.setTickStrategy(AxisTickStrategies.Time, (strategy) => strategy.setFormattingFunction((x, range) => formatRealTime(x)))
.setKeepTickLabelsInAxisBounds(false)
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setDefaultInterval({ start: 0, end: 2, stopAxisAfter: false })
dynamicsChart.axisY.dispose()
const torqueAxisY = dynamicsChart
.addAxisY()
.setTitle('Torque (kNm)')
.setInterval({ start: 0, end: 45 })
.setStrokeStyle(new SolidLine({ fillStyle: new SolidFill({ color: colors.cyan }) }))
const speedAxisY = dynamicsChart
.addAxisY({ opposite: true })
.setTitle('Speed (rpm)')
.setTickStrategy(AxisTickStrategies.Numeric, (tickStrategy) =>
tickStrategy.setTickStyle((ticks) => ticks.setGridStrokeStyle(emptyLine)),
)
.setInterval({ start: 0, end: 180 })
.setStrokeStyle(new SolidLine({ fillStyle: new SolidFill({ color: colors.magenta }) }))
synchronizeAxisIntervals(efficiencyAxisX, dynamicsAxisX)
// DIRECTIONAL TRAJECTORY
const trajectoryContainer = document.createElement('div')
gridContainer.append(trajectoryContainer)
const trajectoryChart = lc
.Chart3D({
container: trajectoryContainer,
legend: { visible: false },
// theme: Themes.darkGold
})
.setTitle('DIRECTIONAL TRAJECTORY')
.setBoundingBox({ x: 1, y: 3.5, z: 1 })
.setCameraLocation({ x: 1.11, y: 0.91, z: 0.92 })
trajectoryChart.axisX.setTitle('East (m)').setDefaultInterval({ start: -0.01, end: 1.3 })
trajectoryChart.axisZ.setTitle('North (m)').setDefaultInterval({ start: -0.01, end: 1.3 })
trajectoryChart.axisY
.setTitle('TVD (m)')
.setDefaultInterval({ start: -3450.13, end: -3450.01 })
.setTickStrategy(AxisTickStrategies.Numeric, (strategy) =>
strategy.setFormattingFunction((tickValue) => Math.abs(tickValue).toFixed(2)),
)
// FLOW DIFFERENTIAL
applyContainerStyle(trajectoryContainer)
trajectoryContainer.style.gridColumn = '3'
trajectoryContainer.style.gridRowStart = '2'
trajectoryContainer.style.gridRowEnd = '5'
const flowContainer = document.createElement('div')
gridContainer.append(flowContainer)
const flowChart = createChartXY(flowContainer, 'FLOW DIFFERENTIAL', [4, 4, 2, 2]).setAnimationsEnabled(false)
flowChart.axisX.dispose()
const flowAxisX = flowChart
.addAxisX({})
.setTitle('Time (s)')
.setTickStrategy(AxisTickStrategies.Time, (strategy) => strategy.setFormattingFunction((x, range) => formatRealTime(x)))
.setKeepTickLabelsInAxisBounds(false)
.setScrollStrategy(AxisScrollStrategies.scrolling)
.setDefaultInterval({ start: 0, end: 2, stopAxisAfter: false })
flowChart.axisY.setTitle('Flow Rate (L/min)').setInterval({ start: 1800, end: 2600 })
// STANDPIPE PRESSURE
const pressureContainer = document.createElement('div')
gridContainer.append(pressureContainer)
const pressureChart = lc
.Gauge({
container: pressureContainer,
legend: { visible: false },
// theme: Themes.darkGold
})
.setTitle('STANDPIPE PRESSURE')
.setTitleMargin({ bottom: 0 })
applyContainerStyle(pressureContainer)
pressureContainer.style.gridColumn = '4'
pressureContainer.style.gridRow = '3'
pressureChart
.setPadding(0)
.setInterval(0, 5000)
.setRoundedEdges(false)
.setBarThickness(8)
.setNeedleLength(20)
.setNeedleThickness(2)
.setValueIndicators([
{ start: 0, end: 3300, color: colors.grey },
{ start: 3300, end: 4000, color: colors.purple },
{ start: 4000, end: 5000, color: colors.critical },
])
.setValueIndicatorThickness(3)
.setGapBetweenBarAndValueIndicators(1)
.setTickFormatter((tick) => tick.toFixed(0))
.setValueLabelFont((font) => font.setSize(18))
.setTickFont((font) => font.setSize(14))
// Event Log
const logContainer = document.createElement('div')
gridContainer.append(logContainer)
applyContainerStyle(logContainer)
logContainer.style.gridColumn = '4'
logContainer.style.gridRow = '4'
logContainer.style.padding = '4px'
logContainer.style.overflowY = 'auto'
logContainer.style.color = colors.text
logContainer.style.background = colors.divBg
logContainer.style.fontSize = 'clamp(0.75rem, 1vw, 2rem)'
const logTitle = document.createElement('div')
logTitle.innerHTML = `EVENT LOG`
logTitle.style.color = colors.text
logTitle.style.textShadow = `0px 0px 2px ${colors.text}, 0px 0px 6px ${colors.text}`
logContainer.appendChild(logTitle)
// Helper functions
const fetchData = (path) => {
return fetch(new URL(document.head.baseURI).origin + new URL(document.head.baseURI).pathname + path).then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch ${path}`)
}
return response.json()
})
}
const createLogs = (mechanicsData, slowData) => {
const logs = []
const states = [[]]
let activeAlarms = []
for (let i = 1; i <= slowData.length; i++) {
const slowPoint = slowData[i - 1]
const targetMechanicsIndex = i * 1000
const mechanicsEndIndex = Math.min(targetMechanicsIndex, mechanicsData.length)
const mechanicsStartIndex = Math.max(0, mechanicsEndIndex - 1000)
const secondChunk = mechanicsData.slice(mechanicsStartIndex, mechanicsEndIndex)
let ropNow = 0
let wobNow = 0
if (secondChunk.length > 0) {
const latestMechanics = secondChunk[secondChunk.length - 1]
ropNow = latestMechanics['ROP']
wobNow = latestMechanics['WOB']
}
const sssNow = secondChunk.length > 0 ? checkStickSlip(secondChunk) : 0
const drillingState = {
rop: ropNow,
wob: wobNow,
spp: slowPoint['SPP'],
flowIn: slowPoint['Flow_In'],
flowOut: slowPoint['Flow_Out'],
kickAlert: slowPoint['Kick_Alert'],
sss: sssNow,
}
const currentConditions = checkAlarms(drillingState)
const timestamp = formatRealTime(i)
states.push(currentConditions)
currentConditions.forEach((alarm) => {
if (!activeAlarms.includes(alarm)) logs.push({ time: i, timestamp, message: alarm, isWarning: true })
})
activeAlarms.forEach((oldAlarm) => {
if (!currentConditions.includes(oldAlarm)) {
const cleanName = oldAlarm.replace('[CRITICAL] ', '').replace('[WARN] ', '')
logs.push({ time: i, timestamp, message: `[RESOLVED] ${cleanName}`, isWarning: false })
}
})
if (currentConditions.length === 0 && activeAlarms.length > 0) {
logs.push({ time: i, timestamp, message: '[INFO] SYSTEM RETURNED TO NORMAL', isWarning: false })
}
activeAlarms = currentConditions
}
return { logs, states }
}
const showLogs = (timestamp, message, isWarning) => {
const eventDiv = document.createElement('div')
eventDiv.innerHTML = `${timestamp} ${message}`
const alarmColor = message.includes('[WARN]') ? colors.warnText : colors.criticalText
eventDiv.style.color = isWarning ? alarmColor : colors.text
logContainer.appendChild(eventDiv)
logContainer.scrollTo(0, logContainer.scrollHeight)
}
const toRad = (degrees) => degrees * (Math.PI / 180)
const addTrajectory = (data) => {
let currentTVD = data[0].Depth
let currentNorth = 0
let currentEast = 0
data[0].East = currentEast
data[0].Tvd = -currentTVD
data[0].North = currentNorth
for (let i = 1; i < data.length; i++) {
const p1 = data[i - 1]
const p2 = data[i]
const dMD = p2.Depth - p1.Depth
const I1 = toRad(p1.Inc)
const I2 = toRad(p2.Inc)
const A1 = toRad(p1.Azi)
const A2 = toRad(p2.Azi)
let cosBeta = Math.cos(I2 - I1) - Math.sin(I1) * Math.sin(I2) * (1 - Math.cos(A2 - A1))
cosBeta = Math.max(-1.0, Math.min(1.0, cosBeta))
const beta = Math.acos(cosBeta)
let RF = 1.0
if (beta > 0.00001) RF = (2 / beta) * Math.tan(beta / 2)
const dTVD = (dMD / 2) * (Math.cos(I1) + Math.cos(I2)) * RF
const dNorth = (dMD / 2) * (Math.sin(I1) * Math.cos(A1) + Math.sin(I2) * Math.cos(A2)) * RF
const dEast = (dMD / 2) * (Math.sin(I1) * Math.sin(A1) + Math.sin(I2) * Math.sin(A2)) * RF
currentTVD += dTVD
currentNorth += dNorth
currentEast += dEast
data[i].East = currentEast
data[i].Tvd = -currentTVD
data[i].North = currentNorth
}
}
const calculateBitProjection = (currentPoint, targetY) => {
const incRad = toRad(currentPoint.Inc)
const aziRad = toRad(currentPoint.Azi)
let distance
if (Math.abs(Math.cos(incRad)) < 0.0001) {
distance = 2
} else {
distance = (currentPoint.Tvd - targetY) / Math.cos(incRad)
}
if (distance < 0) distance = 2
return {
x: currentPoint.East + distance * Math.sin(incRad) * Math.sin(aziRad),
y: currentPoint.Tvd - distance * Math.cos(incRad),
z: currentPoint.North + distance * Math.sin(incRad) * Math.cos(aziRad),
}
}
const getWhirlColor = (magnitude, alpha) => {
if (magnitude >= 15.0) {
return ColorRGBA(255, 0, 60, alpha)
} else if (magnitude >= 5.0) {
return ColorRGBA(240, 0, 255, alpha)
} else {
return ColorRGBA(132, 0, 255, alpha)
}
}
const checkStickSlip = (mechanicsChunk) => {
const rpmValues = mechanicsChunk.map((point) => point.RPM)
const minRpm = rpmValues.reduce((min, val) => Math.min(min, val), Infinity)
const maxRpm = rpmValues.reduce((max, val) => Math.max(max, val), -Infinity)
const nominalRpm = 120.0
const sss = ((maxRpm - minRpm) / (2 * nominalRpm)) * 100
return sss
}
const checkAlarms = (data) => {
const { rop, wob, spp, flowIn, flowOut, kickAlert, sss } = data
const conditions = []
if (flowOut - flowIn > 300 || kickAlert === 1) conditions.push('[CRITICAL] KICK DETECTED')
if (spp < 3300) conditions.push('[WARN] SPP DROP / WASHOUT')
if (spp > 4000) conditions.push('[WARN] OVERPRESSURE')
if (rop > 40.0 && wob < 10.0) conditions.push('[WARN] DRILLING BREAK')
if (sss > 100) conditions.push('[WARN] SEVERE STICK-SLIP')
if (rop < 10.0 && wob > 12.0) conditions.push('[WARN] BIT FLOUNDER')
return conditions
}
const updateDashboardPanel = (timeIndex, currentTimeSec, currentRop, slowData) => {
timeDiv.innerHTML = `TIME: ${formatRealTime(currentTimeSec)}`
if (timeIndex > 0) {
const latestSlow = slowData[timeIndex - 1]
if (latestSlow) {
depthDiv.innerHTML = `DEPTH: ${latestSlow['Depth'].toFixed(2)} m`
pressureChart.setValue(latestSlow['SPP'])
}
}
ropDiv.innerHTML = `ROP: ${currentRop.toFixed(1)} m/h`
const currentConditions = panelStates[timeIndex] || []
if (currentConditions.length === 0) {
if (currentState !== NORMAL_STATE) {
currentState = NORMAL_STATE
stateDiv.innerHTML = currentState
stateDiv.style.color = colors.text
stateDiv.style.textShadow = `0px 0px 2px ${colors.text}, 0px 0px 6px ${colors.text}`
panelContainer.style.border = `2px solid ${colors.border}`
panelContainer.style.boxShadow = 'none'
}
} else {
if (currentState !== currentConditions[0]) {
currentState = currentConditions[0]
stateDiv.innerHTML = currentState
const alarmColor = currentState.includes('[WARN]') ? colors.warnText : colors.criticalText
stateDiv.style.color = alarmColor
stateDiv.style.textShadow = `0px 0px 2px ${alarmColor}, 0px 0px 6px ${alarmColor}`
panelContainer.style.border = `2px solid ${alarmColor}`
panelContainer.style.boxShadow = `0px 0px 8px ${alarmColor}`
}
}
}
const addSeries = ({ chart, type, axisX, axisY, dataset, dataKeys, style }) => {
switch (type) {
case 'line':
return chart
.addLineSeries({ axisX, axisY })
.setDataSet(dataset, dataKeys)
.setStrokeStyle(
new SolidLine({
thickness: style.thickness,
fillStyle: new SolidFill({ color: style.color }),
}),
)
case 'area':
return chart
.addAreaRangeSeries({ axisX, axisY, orientation: style.orientation })
.setDataSet(dataset, dataKeys)
.setStrokeStyle(
new SolidLine({
thickness: style.thickness,
fillStyle: new SolidFill({ color: style.color1 }),
}),
{ range1: true, range2: false },
)
.setStrokeStyle(
new SolidLine({
thickness: style.thickness,
fillStyle: new SolidFill({ color: style.color2 }),
}),
{ range1: false, range2: true },
)
.setPointFillStyle(emptyFill, { range1: true, range2: true })
.setAreaFillStyle(new SolidFill({ color: style.color3 }))
}
}
// Shared data sets
const dataSetMechanics = new DataSet({
schema: {
Time_s: { pattern: 'progressive' },
ROP: { pattern: null },
RPM: { pattern: null },
Torque: { pattern: null },
WOB: { pattern: null },
Whirl_Angle: { pattern: null },
Whirl_Mag: { pattern: null },
},
}).setMaxSampleCount(500_000)
const dataSetSlow = new DataSet({
schema: {
Time_s: { pattern: 'progressive' },
Depth: { pattern: 'progressive' },
Flow_In: { pattern: null },
Flow_Out: { pattern: null },
SPP: { pattern: null },
Gamma: { pattern: null },
Kick_Alert: { pattern: null },
Inc: { pattern: null },
Azi: { pattern: null },
East: { pattern: null },
Tvd: { pattern: null },
North: { pattern: null },
},
}).setMaxSampleCount(500_000)
Promise.all([fetchData('examples/assets/1717/mechanics_data.json'), fetchData('examples/assets/1717/slow_data.json')])
.then((results) => {
const mechanicsData = results[0]
const slowData = results[1]
addTrajectory(slowData)
const finalTargetPos = slowData[slowData.length - 1]
const targetPos3D = { x: finalTargetPos.East, y: finalTargetPos.Tvd, z: finalTargetPos.North }
dataSetMechanics.appendJSON(mechanicsData)
dataSetSlow.appendJSON(slowData)
const eventLogData = createLogs(mechanicsData, slowData)
eventLogs.push(...eventLogData.logs)
panelStates.push(...eventLogData.states)
// Series
const lithologySeries = addSeries({
chart: lithologyChart,
type: 'line',
axisX: lithologyChart.axisX,
axisY: lithologyChart.axisY,
dataset: dataSetSlow,
dataKeys: { x: 'Gamma', y: 'Depth' },
style: { thickness: 3, color: colors.cyan },
})
const sandBand = lithologyChart.axisX
.addBand()
.setValueStart(0)
.setValueEnd(45)
.setFillStyle(new SolidFill({ color: colors.sand }))
.setStrokeStyle(emptyLine)
.setPointerEvents(false)
const shaleBand = lithologyChart.axisX
.addBand()
.setValueStart(75)
.setValueEnd(120)
.setFillStyle(new SolidFill({ color: colors.shale }))
.setStrokeStyle(emptyLine)
.setPointerEvents(false)
const ropSeries = addSeries({
chart: efficiencyChart,
type: 'line',
axisX: efficiencyAxisX,
axisY: ropAxisY,
dataset: dataSetMechanics,
dataKeys: { x: 'Time_s', y: 'ROP' },
style: { thickness: 1, color: colors.cyan },
})
const wobSeries = addSeries({
chart: efficiencyChart,
type: 'line',
axisX: efficiencyAxisX,
axisY: wobAxisY,
dataset: dataSetMechanics,
dataKeys: { x: 'Time_s', y: 'WOB' },
style: { thickness: 1, color: colors.magenta },
})
const torqueSeries = addSeries({
chart: dynamicsChart,
type: 'line',
axisX: dynamicsAxisX,
axisY: torqueAxisY,
dataset: dataSetMechanics,
dataKeys: { x: 'Time_s', y: 'Torque' },
style: { thickness: 1, color: colors.cyan },
})
const speedSeries = addSeries({
chart: dynamicsChart,
type: 'line',
axisX: dynamicsAxisX,
axisY: speedAxisY,
dataset: dataSetMechanics,
dataKeys: { x: 'Time_s', y: 'RPM' },
style: { thickness: 1, color: colors.magenta },
})
const flowRange = addSeries({
chart: flowChart,
type: 'area',
axisX: flowAxisX,
axisY: flowChart.axisY,
dataset: dataSetSlow,
dataKeys: { position: 'Time_s', range1: 'Flow_In', range2: 'Flow_Out' },
style: {
thickness: 1,
color1: colors.cyan,
color2: colors.magenta,
color3: colors.purpleTransparent,
orientation: 'horizontal',
},
}).setName('Flow In/Out')
const whirlSeries = whirlChart
.addPointSeries()
.setPointStrokeStyle(emptyLine)
.setPointSize(6)
.setPointFillStyle(new IndividualPointFill())
const wellboreSeries = addSeries({
chart: trajectoryChart,
type: 'line',
axisX: trajectoryChart.axisX,
axisY: trajectoryChart.axisY,
dataset: dataSetSlow,
dataKeys: { x: 'East', y: 'Tvd', z: 'North' },
style: { thickness: 10, color: colors.cyan },
}).setName('Drilled Trajectory')
const beamSeries = trajectoryChart
.addLineSeries()
.setName('Projected Path')
.setStrokeStyle(new SolidLine({ thickness: 10, fillStyle: new SolidFill({ color: colors.purpleTransparent }) }))
const bitSeries = trajectoryChart
.addPointSeries()
.setName('Bit Position')
.setPointStyle(
new PointStyle3D.Triangulated({
size: 20,
shape: 'sphere',
fillStyle: new SolidFill({ color: colors.magenta }),
}),
)
const finalPoint = slowData[slowData.length - 1]
const staticTarget3D = { x: finalPoint.East, y: finalPoint.Tvd, z: finalPoint.North }
const targetSeries = trajectoryChart
.addPointSeries()
.setName('Target')
.setPointStyle(
new PointStyle3D.Triangulated({
size: 20,
shape: 'sphere',
fillStyle: new SolidFill({ color: colors.warn }),
}),
)
targetSeries.appendJSON([staticTarget3D])
let isSimulationRunning = false
let wasRunningWhenClicked = false
let simulationStartTime = 0
let skippedSeconds = simulationStartTime / 1000
let playbackSpeed = 5
speedSelect.value = '5'
let currentMechanicsIndex = 0
let currentSlowIndex = 0
let ropNow = 0
const WHIRL_BUFFER_SIZE = 1000
const whirlDataBuffer = Array.from({ length: WHIRL_BUFFER_SIZE }, () => ({
angle: 0,
amplitude: 0,
color: colors.transparent,
}))
let whirlBufferIndex = 0
let activeWhirlCount = 0
timeSlider.onpointerdown = () => {
wasRunningWhenClicked = isSimulationRunning
}
timeSlider.oninput = () => {
isSimulationRunning = false
pressureChart.setAnimationsEnabled(false)
const currentTime = Number(timeSlider.value)
const targetSlowIndex = Math.min(Math.floor(currentTime / 1000) + 1, slowData.length)
timeMarker.innerHTML = PLAY_SVG
timeSlider.style.background = colors.text
updateDashboardPanel(targetSlowIndex, currentTime / 1000, ropNow, slowData)
}
timeSlider.onchange = async () => {
const currentTime = Number(timeSlider.value)
activeWhirlCount = 0
whirlBufferIndex = 0
lithologyAxisY.fit()
efficiencyAxisX.fit()
dynamicsAxisX.fit()
flowAxisX.fit()
trajectoryChart.setCameraAutomaticFittingEnabled(true)
trajectoryChart.setCameraLocation({ x: 1.11, y: 0.91, z: 0.92 })
pressureChart.setAnimationsEnabled(true)
await startDashboardSimulation(currentTime)
isSimulationRunning = wasRunningWhenClicked
timeMarker.innerHTML = isSimulationRunning ? PAUSE_SVG : PLAY_SVG
}
timeMarker.onclick = async () => {
if (isSimulationRunning) {
isSimulationRunning = false
pressureChart.setAnimationsEnabled(false)
timeMarker.innerHTML = PLAY_SVG
} else {
const currentTime = timeSlider.value
activeWhirlCount = 0
whirlBufferIndex = 0
lithologyAxisY.fit()
efficiencyAxisX.fit()
dynamicsAxisX.fit()
flowAxisX.fit()
trajectoryChart.setCameraAutomaticFittingEnabled(true)
trajectoryChart.setCameraLocation({ x: 1.11, y: 0.91, z: 0.92 })
pressureChart.setAnimationsEnabled(true)
await startDashboardSimulation(currentTime)
timeMarker.innerHTML = PAUSE_SVG
}
}
speedSelect.onchange = (e) => {
if (isSimulationRunning) {
const currentPreciseTime = ((performance.now() - simulationStartTime) / 1000) * playbackSpeed + skippedSeconds
playbackSpeed = Number(e.target.value)
skippedSeconds = currentPreciseTime
simulationStartTime = performance.now()
} else {
playbackSpeed = Number(e.target.value)
}
}
const startDashboardSimulation = async (currentTime) => {
isSimulationRunning = true
skippedSeconds = currentTime / 1000
currentMechanicsIndex = Math.min(Math.floor(skippedSeconds * 1000), mechanicsData.length)
currentSlowIndex = Math.min(Math.floor(skippedSeconds) + 1, slowData.length)
if (currentMechanicsIndex > 0) {
const historyChunk = mechanicsData.slice(0, currentMechanicsIndex)
dataSetMechanics.clear().appendJSON(historyChunk)
const latestMechanics = historyChunk[historyChunk.length - 1]
if (latestMechanics) ropNow = latestMechanics['ROP']
const whirlChunk = historyChunk.slice(-WHIRL_BUFFER_SIZE)
activeWhirlCount = whirlChunk.length
whirlBufferIndex = activeWhirlCount % WHIRL_BUFFER_SIZE
for (let i = 0; i < WHIRL_BUFFER_SIZE; i++) {
if (i < activeWhirlCount) {
const point = whirlChunk[i]
whirlDataBuffer[i].angle = point.Whirl_Angle
whirlDataBuffer[i].amplitude = point.Whirl_Mag
const normalizedIndex = i / (activeWhirlCount - 1 || 1)
const alpha = Math.round(Math.pow(normalizedIndex, 2) * 255)
whirlDataBuffer[i].color = getWhirlColor(point.Whirl_Mag, alpha)
} else {
whirlDataBuffer[i].color = colors.transparent
}
}
whirlSeries.setData(whirlDataBuffer)
} else {
dataSetMechanics.clear()
ropNow = 0
activeWhirlCount = 0
whirlBufferIndex = 0
for (let i = 0; i < WHIRL_BUFFER_SIZE; i++) whirlDataBuffer[i].color = colors.transparent
whirlSeries.setData(whirlDataBuffer)
}
if (currentSlowIndex > 0) {
const historyChunk = slowData.slice(0, currentSlowIndex)
dataSetSlow.clear().appendJSON(historyChunk)
const currentPos = historyChunk[historyChunk.length - 1]
if (currentPos) {
const currentPos3D = { x: currentPos.East, y: currentPos.Tvd, z: currentPos.North }
const projectedPos3D = calculateBitProjection(currentPos, targetPos3D.y)
beamSeries.clear().appendJSON([currentPos3D, projectedPos3D])
bitSeries.clear().appendJSON([currentPos3D])
}
} else {
dataSetSlow.clear()
beamSeries.clear()
bitSeries.clear()
}
const titleNode = logContainer.firstElementChild
logContainer.replaceChildren(titleNode)
eventLogs.filter((log) => log.time <= currentSlowIndex).forEach((log) => showLogs(log.timestamp, log.message, log.isWarning))
updateDashboardPanel(currentSlowIndex, skippedSeconds, ropNow, slowData)
await new Promise((resolve) => requestAnimationFrame(resolve))
simulationStartTime = performance.now()
isSimulationRunning = true
requestAnimationFrame(renderLoop)
}
const renderLoop = () => {
if (!isSimulationRunning) return
const elapsedTimeSec = ((performance.now() - simulationStartTime) / 1000) * playbackSpeed + skippedSeconds
const targetMechanicsIndex = Math.min(Math.floor(elapsedTimeSec * 1000), mechanicsData.length)
const targetSlowIndex = Math.min(Math.floor(elapsedTimeSec * 1) + 1, slowData.length)
// 1kHz Data
if (targetMechanicsIndex > currentMechanicsIndex) {
const newMechanics = mechanicsData.slice(currentMechanicsIndex, targetMechanicsIndex)
dataSetMechanics.appendJSON(newMechanics)
if (newMechanics.length > 0) {
ropNow = newMechanics[newMechanics.length - 1]['ROP']
for (let i = 0; i < newMechanics.length; i++) {
const point = newMechanics[i]
const bufferItem = whirlDataBuffer[whirlBufferIndex]
bufferItem.angle = point.Whirl_Angle
bufferItem.amplitude = point.Whirl_Mag
whirlBufferIndex = (whirlBufferIndex + 1) % WHIRL_BUFFER_SIZE
if (activeWhirlCount < WHIRL_BUFFER_SIZE) activeWhirlCount++
}
for (let i = 0; i < activeWhirlCount; i++) {
const indexFromOldest = (whirlBufferIndex - activeWhirlCount + i + WHIRL_BUFFER_SIZE) % WHIRL_BUFFER_SIZE
const bufferItem = whirlDataBuffer[indexFromOldest]
const normalizedIndex = i / (activeWhirlCount - 1 || 1)
const alpha = Math.round(Math.pow(normalizedIndex, 2) * 255)
bufferItem.color = getWhirlColor(bufferItem.amplitude, alpha)
}
whirlSeries.setData(whirlDataBuffer)
}
currentMechanicsIndex = targetMechanicsIndex
}
// 1Hz Data
if (targetSlowIndex > currentSlowIndex) {
timeSlider.value = currentMechanicsIndex
const newSlowChunk = slowData.slice(currentSlowIndex, targetSlowIndex)
dataSetSlow.appendJSON(newSlowChunk)
const currentPos = newSlowChunk[newSlowChunk.length - 1]
const currentPos3D = { x: currentPos.East, y: currentPos.Tvd, z: currentPos.North }
const projectedPos3D = calculateBitProjection(currentPos, targetPos3D.y)
beamSeries.clear().appendJSON([currentPos3D, projectedPos3D])
bitSeries.clear().appendJSON([currentPos3D])
const newLogs = eventLogs.filter((log) => log.time > currentSlowIndex && log.time <= targetSlowIndex)
newLogs.forEach((log) => showLogs(log.timestamp, log.message, log.isWarning))
updateDashboardPanel(targetSlowIndex, elapsedTimeSec, ropNow, slowData)
currentSlowIndex = targetSlowIndex
}
if (currentMechanicsIndex < mechanicsData.length || currentSlowIndex < slowData.length) {
requestAnimationFrame(renderLoop)
} else {
isSimulationRunning = false
timeMarker.innerHTML = PLAY_SVG
pressureChart.setAnimationsEnabled(false)
}
}
startDashboardSimulation(simulationStartTime)
})
.catch((error) => {
console.error('An error occurred during file loading:', error)
})
JavaScript Integrated Drilling Operations Center (iDOC) Dashboard - Editor This dashboard provides a real-time overview of drilling operations, monitoring downhole mechanics, directional trajectory, and overall drilling efficiency.
While this specific demo simulates a live feed using historical data, the application is built from the ground up to handle true real-time data streaming from active rigs. Under the hood, it is highly optimized to share a single memory source across all charts (such as ROP, WOB, Torque, and Speed), ensuring smooth, lag-free visualization even with high-frequency data.
Additionally, the dashboard features an automated diagnostic system that triggers color-coded alerts for critical conditions, alongside a 3D wellbore map and a polar chart for tracking lateral vibration.
if ( targetSlowIndex > currentSlowIndex) {
const newSlowChunk = slowData. slice ( currentSlowIndex, targetSlowIndex)
dataSetSlow. appendJSON ( newSlowChunk)
const currentPos = newSlowChunk[ newSlowChunk. length - 1 ]
const currentPos3D = { x: currentPos. East, y: currentPos. Tvd, z: currentPos. North }
const projectedPos3D = calculateBitProjection ( currentPos, targetPos3D. y)
beamSeries. clear ( ) . appendJSON ( [ currentPos3D, projectedPos3D] )
bitSeries. clear ( ) . appendJSON ( [ currentPos3D] )
const newLogs = eventLogs. filter ( log => log. time > currentSlowIndex && log. time <= targetSlowIndex)
newLogs. forEach ( log => showLogs ( log. timestamp, log. message, log. isWarning) )
updateDashboardPanel ( targetSlowIndex, elapsedTimeSec, ropNow, slowData)
currentSlowIndex = targetSlowIndex
}