Detect Industrial Heater Failures with JavaScript: A Rule-Based Anomaly Detection System
Industrial electric heaters rarely fail without warning.
Before a heating element burns out completely, the system may show signs such as:
- A slower-than-normal temperature rise
- High current without sufficient heating
- Temperature increase while the heater is supposed to be off
- A temperature sensor stuck at one value
- Excessive overshoot above the target temperature
- Sudden and unrealistic temperature changes
In industrial systems, these symptoms may indicate problems with the heating element, thermocouple, contactor, solid-state relay, wiring, airflow, or temperature controller.
In this tutorial, we will build a browser-based heater anomaly detector using JavaScript.
The application will analyze simulated temperature and current readings and detect several common heater faults without using machine learning or external libraries.
What We Are Building
Our monitoring system will receive a new data sample every second.
Each sample contains:
{
timestamp: 1720000000000,
temperature: 82.4,
current: 13.6,
heaterCommand: true,
targetTemperature: 120
}
The application will monitor:
- Heating rate
- Current flow
- Temperature overshoot
- Sensor activity
- Unexpected heating
- Sudden sensor changes
It will then classify the system status as:
NORMAL
WARNING
FAULT
Why Use Rule-Based Detection?
Machine learning can be useful when large historical datasets are available.
However, many industrial heating systems do not have enough labeled failure data to train a reliable model.
A rule-based system offers several advantages:
- Easy to understand
- Easy to test
- Fast to execute
- No training dataset required
- Suitable for browsers and embedded dashboards
- Thresholds can be adjusted for each heater
The main limitation is that thresholds must be configured according to the real process.
A 3 kW water heater behaves differently from a 30 kW industrial oven.
Project Structure
Create the following files:
heater-anomaly-detector/
├── index.html
├── style.css
└── app.js
Step 1: Create the HTML Interface
Add the following code to "index.html":
<!DOCTYPE html>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
<meta
name="description"
content="A JavaScript-based industrial heater anomaly detection dashboard."
Industrial Monitoring Demo
<h1>Heater Anomaly Detector</h1>
<p>
Monitor temperature, current, heating rate,
and common heater failure patterns.
</p>
</div>
<div
id="system-status"
class="status status-normal"
>
NORMAL
</div>
</header>
<section class="metrics">
<article class="metric-card">
<span>Temperature</span>
<strong id="temperature-value">-- °C</strong>
</article>
<article class="metric-card">
<span>Target</span>
<strong id="target-value">-- °C</strong>
</article>
<article class="metric-card">
<span>Current</span>
<strong id="current-value">-- A</strong>
</article>
<article class="metric-card">
<span>Heating rate</span>
<strong id="heating-rate-value">
-- °C/min
</strong>
</article>
</section>
<section class="panel">
<div class="panel-heading">
<div>
<h2>Temperature History</h2>
<p>
Recent process temperature and target value
</p>
</div>
<div class="legend">
<span>
<i class="temperature-marker"></i>
Temperature
</span>
<span>
<i class="target-marker"></i>
Target
</span>
</div>
</div>
<canvas
id="temperature-chart"
width="900"
height="360"
></canvas>
</section>
<section class="controls panel">
<div>
<h2>Simulation Mode</h2>
<p>
Select a condition to test the detection rules.
</p>
</div>
<div class="control-buttons">
<button data-mode="normal">
Normal operation
</button>
<button data-mode="weak-heater">
Weak heater
</button>
<button data-mode="open-circuit">
Open circuit
</button>
<button data-mode="stuck-sensor">
Stuck sensor
</button>
<button data-mode="relay-stuck">
Relay stuck on
</button>
<button data-mode="overheating">
Overheating
</button>
</div>
</section>
<section class="panel">
<div class="panel-heading">
<div>
<h2>Diagnostic Events</h2>
<p>
Warnings and detected fault conditions
</p>
</div>
<button
id="clear-events"
class="secondary-button"
>
Clear events
</button>
</div>
<div id="event-list" class="event-list">
<p class="empty-state">
No abnormal condition detected.
</p>
</div>
</section>
Step 2: Style the Dashboard
Add the following code to "style.css":
- { box-sizing: border-box; }
:root {
font-family:
Inter,
Arial,
Helvetica,
sans-serif;
color: #172033;
background: #eef2f7;
}
body {
margin: 0;
padding: 32px 20px;
}
button {
font: inherit;
}
.dashboard {
width: min(1180px, 100%);
margin: 0 auto;
}
.dashboard-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 26px;
}
.eyebrow {
display: block;
margin-bottom: 8px;
color: #d97706;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 10px;
font-size: clamp(32px, 5vw, 52px);
line-height: 1.05;
}
h2 {
margin-bottom: 6px;
font-size: 20px;
}
.dashboard-header p,
.panel-heading p,
.controls p {
margin-bottom: 0;
color: #64748b;
line-height: 1.7;
}
.status {
min-width: 130px;
padding: 13px 18px;
border-radius: 999px;
text-align: center;
font-size: 14px;
font-weight: 800;
letter-spacing: 0.06em;
}
.status-normal {
background: #dcfce7;
color: #166534;
}
.status-warning {
background: #fef3c7;
color: #92400e;
}
.status-fault {
background: #fee2e2;
color: #b91c1c;
}
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 18px;
}
.metric-card,
.panel {
border: 1px solid #e2e8f0;
background: #ffffff;
box-shadow: 0 14px 35px rgba(15, 23, 42, 0.06);
}
.metric-card {
padding: 22px;
border-radius: 16px;
}
.metric-card span {
display: block;
margin-bottom: 10px;
color: #64748b;
font-size: 14px;
}
.metric-card strong {
font-size: 25px;
}
.panel {
margin-bottom: 18px;
padding: 24px;
border-radius: 18px;
}
.panel-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 22px;
}
.legend {
display: flex;
gap: 16px;
color: #64748b;
font-size: 13px;
}
.legend span {
display: flex;
align-items: center;
gap: 7px;
}
.legend i {
width: 24px;
height: 4px;
border-radius: 99px;
}
.temperature-marker {
background: #ea580c;
}
.target-marker {
background: #2563eb;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 12px;
background: #f8fafc;
}
.controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.control-buttons {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 9px;
}
.control-buttons button,
.secondary-button {
min-height: 42px;
padding: 9px 14px;
border: 1px solid #d7dee8;
border-radius: 9px;
background: #f8fafc;
color: #334155;
cursor: pointer;
}
.control-buttons button:hover,
.control-buttons button.active {
border-color: #f59e0b;
background: #fff7ed;
color: #9a3412;
}
.secondary-button:hover {
background: #eef2f7;
}
.event-list {
display: grid;
gap: 10px;
}
.event-item {
padding: 15px 16px;
border-left: 5px solid;
border-radius: 9px;
background: #f8fafc;
}
.event-item strong {
display: block;
margin-bottom: 5px;
}
.event-item p {
margin-bottom: 5px;
color: #475569;
line-height: 1.5;
}
.event-item time {
color: #94a3b8;
font-size: 12px;
}
.event-warning {
border-color: #f59e0b;
background: #fffbeb;
}
.event-fault {
border-color: #dc2626;
background: #fef2f2;
}
.empty-state {
margin: 0;
padding: 20px;
border: 1px dashed #cbd5e1;
border-radius: 10px;
color: #64748b;
text-align: center;
}
@media (max-width: 900px) {
.metrics {
grid-template-columns: repeat(2, 1fr);
}
.controls {
align-items: flex-start;
flex-direction: column;
}
.control-buttons {
justify-content: flex-start;
}
}
@media (max-width: 560px) {
body {
padding: 20px 12px;
}
.dashboard-header {
flex-direction: column;
}
.metrics {
grid-template-columns: 1fr;
}
.panel-heading {
flex-direction: column;
}
.legend {
flex-wrap: wrap;
}
.control-buttons {
display: grid;
width: 100%;
grid-template-columns: 1fr;
}
.control-buttons button {
width: 100%;
}
}
Step 3: Define the Detection Thresholds
We need configurable thresholds before implementing the diagnostic logic.
Add the following code to "app.js":
const config = {
sampleIntervalMs: 1000,
maximumHistorySize: 90,
minimumActiveCurrent: 1,
expectedCurrent: 13.6,
currentTolerance: 0.25,
minimumHeatingRate: 0.35,
maximumHeatingRate: 8,
maximumOvershoot: 12,
maximumUnexpectedHeatingRate: 0.4,
stuckSensorWindow: 10,
stuckSensorTolerance: 0.05,
suddenTemperatureChange: 12,
faultConfirmationSamples: 5
};
These values are only suitable for a demonstration.
In a real installation, thresholds should be determined from:
- Heater power
- Heated material
- Process volume
- Thermal insulation
- Sensor response time
- Ambient conditions
- Normal heating curve
- Control strategy
- Sample interval
Step 4: Create the Application State
Add the following code below the configuration:
const state = {
mode: "normal",
temperature: 24,
targetTemperature: 120,
current: 0,
heaterCommand: true,
history: [],
faultCounters: new Map(),
reportedFaults: new Set()
};
const elements = {
status: document.getElementById("system-status"),
temperature: document.getElementById(
"temperature-value"
),
target: document.getElementById("target-value"),
current: document.getElementById("current-value"),
heatingRate: document.getElementById(
"heating-rate-value"
),
eventList: document.getElementById("event-list"),
chart: document.getElementById(
"temperature-chart"
),
clearEvents: document.getElementById(
"clear-events"
)
};
const chartContext = elements.chart.getContext("2d");
The application stores recent samples in the "history" array.
Fault counters are used to avoid reporting an error after only one abnormal reading.
Step 5: Simulate Heater Data
Add the following simulation function:
function generateSample() {
const noise = randomBetween(-0.08, 0.08);
let temperatureChange = 0;
let current = 0;
let heaterCommand =
state.temperature < state.targetTemperature;
switch (state.mode) {
case "normal":
current = heaterCommand
? randomBetween(13.2, 13.9)
: 0;
temperatureChange = heaterCommand
? randomBetween(0.11, 0.18)
: randomBetween(-0.04, -0.01);
break;
case "weak-heater":
current = heaterCommand
? randomBetween(12.9, 13.8)
: 0;
temperatureChange = heaterCommand
? randomBetween(0.01, 0.035)
: -0.02;
break;
case "open-circuit":
current = 0;
temperatureChange = -0.02;
break;
case "stuck-sensor":
current = heaterCommand ? 13.6 : 0;
temperatureChange = 0;
break;
case "relay-stuck":
heaterCommand = false;
current = randomBetween(13.2, 13.9);
temperatureChange = randomBetween(0.1, 0.16);
break;
case "overheating":
current = randomBetween(13.2, 13.9);
temperatureChange = randomBetween(0.32, 0.45);
heaterCommand = true;
break;
default:
throw new Error(
`Unknown simulation mode: ${state.mode}`
);
}
state.temperature += temperatureChange + noise;
if (
state.mode === "normal" &&
state.temperature >= state.targetTemperature
) {
state.temperature =
state.targetTemperature +
randomBetween(-0.8, 0.8);
}
state.current = Math.max(0, current);
state.heaterCommand = heaterCommand;
return {
timestamp: Date.now(),
temperature: state.temperature,
current: state.current,
heaterCommand: state.heaterCommand,
targetTemperature: state.targetTemperature
};
}
function randomBetween(minimum, maximum) {
return (
Math.random() * (maximum - minimum) +
minimum
);
}
Each simulation mode represents a different operating condition.
Normal operation
The heater current is close to its expected value and temperature rises normally.
Weak heater
Current is present, but the temperature rises too slowly.
Possible causes include:
- Reduced supply voltage
- Incorrect element resistance
- Partial element failure
- Excessive heat loss
- Poor thermal contact
- Insufficient heater capacity
Open circuit
The controller requests heat, but no current flows.
Possible causes include:
- Burned heating element
- Open fuse
- Broken wire
- Failed contactor
- Disconnected terminal
- Protection device activated
Stuck sensor
The sensor value does not change despite heater operation.
Possible causes include:
- Frozen software value
- Failed transmitter
- Disconnected sensor
- Incorrect analog input configuration
- Communication failure
Relay stuck on
The controller sends an off command, but current continues to flow and temperature rises.
This can happen when:
- A contactor is mechanically welded
- A solid-state relay fails in a closed state
- Control wiring is incorrect
- The output signal is bypassed
Overheating
The temperature rises too rapidly or exceeds the acceptable target range.
Step 6: Calculate the Heating Rate
The temperature slope is one of the most useful values for heater monitoring.
Add this function:
function calculateHeatingRate(history) {
if (history.length < 2) {
return 0;
}
const windowSize = Math.min(15, history.length);
const firstSample =
history[history.length - windowSize];
const lastSample =
history[history.length - 1];
const temperatureDifference =
lastSample.temperature -
firstSample.temperature;
const timeDifferenceMinutes =
(
lastSample.timestamp -
firstSample.timestamp
) / 60000;
if (timeDifferenceMinutes <= 0) {
return 0;
}
return (
temperatureDifference /
timeDifferenceMinutes
);
}
The formula is:
Heating rate =
Temperature change ÷ Time change
The result is expressed in degrees Celsius per minute.
Using several samples instead of only two readings reduces the effect of sensor noise.
Step 7: Create the Diagnostic Rules
Add the main analysis function:
function analyzeSample(sample) {
const events = [];
const heatingRate = calculateHeatingRate(
state.history
);
detectOpenCircuit(sample, events);
detectWeakHeating(sample, heatingRate, events);
detectUnexpectedHeating(
sample,
heatingRate,
events
);
detectOverheating(sample, events);
detectStuckSensor(sample, events);
detectSuddenTemperatureChange(events);
updateFaultConfirmations(events);
return {
heatingRate,
events
};
}
Now add each individual rule.
Rule 1: Detect an Open Circuit
function detectOpenCircuit(sample, events) {
const heatingRequested =
sample.heaterCommand === true;
const noCurrent =
sample.current <
config.minimumActiveCurrent;
if (heatingRequested && noCurrent) {
events.push({
code: "OPEN_CIRCUIT",
level: "fault",
title: "No heater current detected",
message:
"The controller is requesting heat, but the measured current is below the active threshold."
});
}
}
The rule compares the controller command with measured current.
A heater command without current is a strong indicator of an electrical fault.
Rule 2: Detect Weak Heating
function detectWeakHeating(
sample,
heatingRate,
events
) {
const heaterIsActive =
sample.heaterCommand &&
sample.current >=
config.minimumActiveCurrent;
const enoughSamples =
state.history.length >= 12;
const rateIsTooLow =
heatingRate <
config.minimumHeatingRate;
if (
heaterIsActive &&
enoughSamples &&
rateIsTooLow
) {
events.push({
code: "LOW_HEATING_RATE",
level: "warning",
title: "Heating rate is too low",
message:
The heater is drawing current, but the measured heating rate is only ${heatingRate.toFixed(2)} °C/min.
});
}
}
This condition can detect a heater that still consumes electrical power but does not transfer enough heat into the process.
Rule 3: Detect Unexpected Heating
function detectUnexpectedHeating(
sample,
heatingRate,
events
) {
const heaterShouldBeOff =
sample.heaterCommand === false;
const currentIsFlowing =
sample.current >=
config.minimumActiveCurrent;
const temperatureIsRising =
heatingRate >
config.maximumUnexpectedHeatingRate;
if (
heaterShouldBeOff &&
currentIsFlowing &&
temperatureIsRising
) {
events.push({
code: "RELAY_STUCK_ON",
level: "fault",
title: "Possible relay stuck on",
message:
"Current is flowing and temperature is increasing while the heater command is off."
});
}
}
This is one of the most important safety rules in the application.
A stuck contactor or solid-state relay may continue energizing the heater even after the controller turns the output off.
Rule 4: Detect Overheating
function detectOverheating(sample, events) {
const maximumAllowedTemperature =
sample.targetTemperature +
config.maximumOvershoot;
if (
sample.temperature >
maximumAllowedTemperature
) {
events.push({
code: "OVER_TEMPERATURE",
level: "fault",
title: "Overtemperature detected",
message:
The process temperature is ${sample.temperature.toFixed(1)} °C, above the maximum allowed value of ${maximumAllowedTemperature.toFixed(1)} °C.
});
}
}
A real industrial system should also have an independent hardware overtemperature protection device.
Software monitoring must not be the only safety layer.
Rule 5: Detect a Stuck Sensor
function detectStuckSensor(sample, events) {
const windowSize =
config.stuckSensorWindow;
if (state.history.length < windowSize) {
return;
}
const recentSamples =
state.history.slice(-windowSize);
const temperatures =
recentSamples.map(
(item) => item.temperature
);
const minimumTemperature =
Math.min(...temperatures);
const maximumTemperature =
Math.max(...temperatures);
const totalVariation =
maximumTemperature -
minimumTemperature;
const heaterHasBeenActive =
recentSamples.every(
(item) =>
item.heaterCommand &&
item.current >=
config.minimumActiveCurrent
);
if (
heaterHasBeenActive &&
totalVariation <=
config.stuckSensorTolerance
) {
events.push({
code: "STUCK_SENSOR",
level: "fault",
title: "Temperature sensor may be stuck",
message:
"The temperature value has not changed while the heater has remained active."
});
}
}
The rule checks whether the sensor value remains nearly constant over several samples.
Rule 6: Detect Sudden Temperature Changes
function detectSuddenTemperatureChange(
events
) {
if (state.history.length < 2) {
return;
}
const currentSample =
state.history[state.history.length - 1];
const previousSample =
state.history[state.history.length - 2];
const difference = Math.abs(
currentSample.temperature -
previousSample.temperature
);
if (
difference >
config.suddenTemperatureChange
) {
events.push({
code: "SENSOR_SPIKE",
level: "warning",
title: "Unrealistic temperature change",
message:
The sensor value changed by ${difference.toFixed(1)} °C between two consecutive samples.
});
}
}
A large one-second temperature jump may indicate electrical noise, a loose thermocouple connection, data corruption, or a scaling problem.
Step 8: Prevent False Alarms
One abnormal sample should not always generate a fault.
Add this confirmation system:
function updateFaultConfirmations(events) {
const activeCodes = new Set(
events.map((event) => event.code)
);
for (const event of events) {
const currentCount =
state.faultCounters.get(event.code) ?? 0;
const newCount = currentCount + 1;
state.faultCounters.set(
event.code,
newCount
);
if (
newCount >=
config.faultConfirmationSamples &&
!state.reportedFaults.has(event.code)
) {
addDiagnosticEvent(event);
state.reportedFaults.add(event.code);
}
}
for (
const code
of state.faultCounters.keys()
) {
if (!activeCodes.has(code)) {
state.faultCounters.set(code, 0);
state.reportedFaults.delete(code);
}
}
}
The anomaly must remain active for several consecutive samples before it is reported.
This approach is called persistence filtering or fault debounce.
It reduces false alarms caused by brief electrical noise or normal process variation.
Step 9: Update the Interface
Add the following functions:
function updateMetrics(
sample,
heatingRate
) {
elements.temperature.textContent =
${sample.temperature.toFixed(1)} °C;
elements.target.textContent =
${sample.targetTemperature.toFixed(1)} °C;
elements.current.textContent =
${sample.current.toFixed(1)} A;
elements.heatingRate.textContent =
${heatingRate.toFixed(2)} °C/min;
}
function updateStatus(events) {
const hasFault = events.some(
(event) => event.level === "fault"
);
const hasWarning = events.some(
(event) => event.level === "warning"
);
elements.status.className = "status";
if (hasFault) {
elements.status.textContent = "FAULT";
elements.status.classList.add(
"status-fault"
);
return;
}
if (hasWarning) {
elements.status.textContent = "WARNING";
elements.status.classList.add(
"status-warning"
);
return;
}
elements.status.textContent = "NORMAL";
elements.status.classList.add(
"status-normal"
);
}
Add the event renderer:
function addDiagnosticEvent(event) {
const emptyState =
elements.eventList.querySelector(
".empty-state"
);
if (emptyState) {
emptyState.remove();
}
const item =
document.createElement("article");
item.className =
event-item event-${event.level};
const timestamp =
new Date().toLocaleTimeString();
item.innerHTML = `
${escapeHtml(event.title)}
<p>${escapeHtml(event.message)}</p>
<time>${timestamp}</time>
`;
elements.eventList.prepend(item);
}
function escapeHtml(value) {
const element =
document.createElement("div");
element.textContent = value;
return element.innerHTML;
}
Even though our messages are generated internally, escaping dynamic values is a good habit when using "innerHTML".
Step 10: Draw the Temperature Chart
We can create a lightweight chart with the Canvas API instead of using an external library.
Add this function:
function drawChart() {
const canvas = elements.chart;
const context = chartContext;
const width = canvas.width;
const height = canvas.height;
const padding = {
top: 28,
right: 30,
bottom: 38,
left: 58
};
context.clearRect(0, 0, width, height);
context.fillStyle = "#f8fafc";
context.fillRect(0, 0, width, height);
if (state.history.length < 2) {
return;
}
const temperatures =
state.history.map(
(sample) => sample.temperature
);
const targets =
state.history.map(
(sample) => sample.targetTemperature
);
const allValues = [
...temperatures,
...targets
];
const minimumValue =
Math.floor(Math.min(...allValues) - 10);
const maximumValue =
Math.ceil(Math.max(...allValues) + 10);
drawGrid(
context,
width,
height,
padding,
minimumValue,
maximumValue
);
drawLine({
context,
values: temperatures,
color: "#ea580c",
width,
height,
padding,
minimumValue,
maximumValue
});
drawLine({
context,
values: targets,
color: "#2563eb",
width,
height,
padding,
minimumValue,
maximumValue,
dashed: true
});
}
Now add the helper functions:
function drawGrid(
context,
width,
height,
padding,
minimumValue,
maximumValue
) {
const chartWidth =
width -
padding.left -
padding.right;
const chartHeight =
height -
padding.top -
padding.bottom;
context.strokeStyle = "#dbe4ee";
context.fillStyle = "#64748b";
context.font = "12px Arial";
context.lineWidth = 1;
const horizontalLines = 5;
for (
let index = 0;
index <= horizontalLines;
index += 1
) {
const ratio =
index / horizontalLines;
const y =
padding.top +
chartHeight * ratio;
const value =
maximumValue -
(
maximumValue -
minimumValue
) * ratio;
context.beginPath();
context.moveTo(padding.left, y);
context.lineTo(
padding.left + chartWidth,
y
);
context.stroke();
context.fillText(
`${value.toFixed(0)} °C`,
8,
y + 4
);
}
}
function drawLine({
context,
values,
color,
width,
height,
padding,
minimumValue,
maximumValue,
dashed = false
}) {
const chartWidth =
width -
padding.left -
padding.right;
const chartHeight =
height -
padding.top -
padding.bottom;
const range =
maximumValue -
minimumValue;
context.beginPath();
context.strokeStyle = color;
context.lineWidth = 3;
context.lineJoin = "round";
context.lineCap = "round";
context.setLineDash(
dashed ? [9, 8] : []
);
values.forEach((value, index) => {
const x =
padding.left +
(
index /
Math.max(values.length - 1, 1)
) *
chartWidth;
const normalizedValue =
(value - minimumValue) / range;
const y =
padding.top +
chartHeight -
normalizedValue * chartHeight;
if (index === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
});
context.stroke();
context.setLineDash([]);
}
Step 11: Run the Monitoring Loop
Add the main application loop:
function monitoringLoop() {
const sample = generateSample();
state.history.push(sample);
if (
state.history.length >
config.maximumHistorySize
) {
state.history.shift();
}
const analysis =
analyzeSample(sample);
updateMetrics(
sample,
analysis.heatingRate
);
updateStatus(analysis.events);
drawChart();
}
Start the loop:
setInterval(
monitoringLoop,
config.sampleIntervalMs
);
monitoringLoop();
Step 12: Add the Simulation Controls
Add the following event listeners:
const modeButtons =
document.querySelectorAll("[data-mode]");
modeButtons.forEach((button) => {
button.addEventListener("click", () => {
state.mode = button.dataset.mode;
state.faultCounters.clear();
state.reportedFaults.clear();
modeButtons.forEach(
(item) =>
item.classList.remove("active")
);
button.classList.add("active");
});
});
elements.clearEvents.addEventListener(
"click",
() => {
elements.eventList.innerHTML = ;
<p class="empty-state">
No abnormal condition detected.
</p>
}
);
document
.querySelector('[data-mode="normal"]')
.classList.add("active");
The user can now switch between normal operation and different failure scenarios.
Understanding the Detection Strategy
The detector does not decide whether the heater is healthy based on only one measurement.
It combines several process signals:
Controller command
- Electrical current
- Temperature
- Temperature rate of change
- Target temperature
- Time
This is more reliable than monitoring temperature alone.
For example, a low temperature does not necessarily indicate a fault. The process may have just started.
However, the combination below is more meaningful:
Heater command = ON
Current = Normal
Heating rate = Very low
This pattern may indicate:
- Insufficient heater capacity
- Heat loss
- Partial heater failure
- Incorrect process conditions
- Mechanical installation problems
Another important pattern is:
Heater command = OFF
Current = Present
Temperature = Rising
This combination strongly suggests a failed switching device or incorrect control wiring.
Improving Heating-Rate Calculation with Linear Regression
The basic heating-rate function compares the first and last samples in a time window.
A more stable method is linear regression.
Add this alternative function:
function calculateHeatingRateWithRegression(
history
) {
const windowSize =
Math.min(20, history.length);
if (windowSize < 2) {
return 0;
}
const samples =
history.slice(-windowSize);
const startTime =
samples[0].timestamp;
const points = samples.map(
(sample) => ({
x:
(
sample.timestamp -
startTime
) / 60000,
y: sample.temperature
})
);
const count = points.length;
const sumX = points.reduce(
(total, point) =>
total + point.x,
0
);
const sumY = points.reduce(
(total, point) =>
total + point.y,
0
);
const sumXY = points.reduce(
(total, point) =>
total + point.x * point.y,
0
);
const sumXX = points.reduce(
(total, point) =>
total + point.x * point.x,
0
);
const denominator =
count * sumXX -
sumX * sumX;
if (denominator === 0) {
return 0;
}
return (
count * sumXY -
sumX * sumY
) / denominator;
}
The returned slope represents the estimated temperature change per minute.
Linear regression is less sensitive to individual noisy readings.
Connecting the Dashboard to Real Data
The simulation can later be replaced with data from a real system.
Possible data sources include:
- WebSocket server
- MQTT broker with a web gateway
- REST API
- Industrial PC
- PLC communication gateway
- Modbus TCP service
- OPC UA gateway
- Microcontroller
- ESP32
- Raspberry Pi
- Cloud IoT platform
A WebSocket connection may look like this:
const socket = new WebSocket(
"wss://example.com/heater-data"
);
socket.addEventListener(
"message",
(message) => {
try {
const sample =
JSON.parse(message.data);
validateIncomingSample(sample);
state.history.push(sample);
const analysis =
analyzeSample(sample);
updateMetrics(
sample,
analysis.heatingRate
);
updateStatus(analysis.events);
drawChart();
} catch (error) {
console.error(
"Invalid heater data:",
error
);
}
}
);
Incoming data must be validated before it is used.
function validateIncomingSample(sample) {
const requiredNumericFields = [
"timestamp",
"temperature",
"current",
"targetTemperature"
];
for (
const field
of requiredNumericFields
) {
if (
!Number.isFinite(sample[field])
) {
throw new TypeError(
Invalid ${field}
);
}
}
if (
typeof sample.heaterCommand !==
"boolean"
) {
throw new TypeError(
"Invalid heaterCommand"
);
}
}
Additional Faults You Can Detect
The system can be expanded to identify more advanced conditions.
Unbalanced three-phase current
For a three-phase heater, monitor all three line currents:
{
currentL1: 27.3,
currentL2: 26.9,
currentL3: 13.5
}
A large difference may indicate:
- One failed heating branch
- Loose connection
- Damaged fuse
- Incorrect star or delta wiring
- Supply voltage imbalance
Excessive current
Higher-than-normal current may indicate:
- Incorrect supply voltage
- Shorted heating element
- Incorrect element resistance
- Wiring mistake
- Wrong star or delta connection
Slow cooling
When the heater is off, abnormal cooling behavior may reveal:
- Unexpected external heat
- Incorrect sensor position
- Process flow problems
- Stuck switching device
Repeated short cycling
Frequent switching may indicate:
- Incorrect PID tuning
- Small control differential
- Oversized heater
- Poor sensor location
- Excessive electrical noise
Increasing heat-up time
The system can compare the current heat-up cycle with previous cycles.
A gradual increase in heating time may indicate:
- Element aging
- Scale formation
- Reduced airflow
- Contamination
- Thermal insulation damage
- Lower supply voltage
Safety Limitations
This project is intended for monitoring and educational use.
It must not directly replace:
- Certified safety relays
- Independent overtemperature controllers
- Thermal fuses
- Pressure protection
- Level switches
- Emergency stop circuits
- Circuit breakers
- Ground fault protection
- Proper electrical design
- Qualified engineering review
A browser application may crash, lose network access, or receive delayed data.
Critical heater shutdown functions should be implemented in reliable industrial hardware, such as a PLC, safety controller, or independent temperature limiter.
Possible Next Improvements
This application can be developed further by adding:
- Real-time MQTT data
- Three-phase current monitoring
- Alarm acknowledgment
- User authentication
- Historical event storage
- CSV data export
- PDF maintenance reports
- Configurable thresholds
- Multiple heater zones
- Equipment-specific profiles
- Notification services
- Predictive maintenance scoring
- Statistical process control
- Offline PWA support
- TypeScript type safety
- Automated unit tests
- Database integration
You could also calculate a simple heater health score:
Heater health =
100
- Current deviation penalty
- Heating-rate penalty
- Overshoot penalty
- Sensor stability penalty
The score could help maintenance teams compare multiple heating zones in one dashboard.
Final Thoughts
JavaScript can be used for more than websites and user interfaces.
With the right process data, it can also support industrial monitoring, diagnostics, maintenance planning, and engineering calculations.
This rule-based anomaly detector combines electrical current, temperature, controller commands, and heating rate to identify common heater problems.
Although it is not a replacement for industrial safety hardware, it can become a useful maintenance and visualization tool when connected to real equipment data.
I work on the design and manufacturing of industrial electric heaters, including flanged heaters, tubular heating elements, cartridge heaters, immersion heaters, ceramic heaters, and electric fan heaters at Techno Design.
More information about industrial heating systems:
Suggested DEV Community Tags
javascript
iot
webdev
engineering
Suggested Article Description
Learn how to build a JavaScript-based industrial heater anomaly detector that monitors temperature, current, heating rate, sensor faults, overheating, and relay failures.
Top comments (0)