DEV Community

ashkan hoseinpoor
ashkan hoseinpoor

Posted on

Detect Industrial Heater Failures with JavaScript: A Rule-Based Anomaly Detection System

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:

  1. Heating rate
  2. Current flow
  3. Temperature overshoot
  4. Sensor activity
  5. Unexpected heating
  6. 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 Heater Anomaly Detector



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}`
  );
Enter fullscreen mode Exit fullscreen 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);
}
Enter fullscreen mode Exit fullscreen mode

}

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>
Enter fullscreen mode Exit fullscreen mode

`;

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
);
Enter fullscreen mode Exit fullscreen mode

}
}

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);
}
Enter fullscreen mode Exit fullscreen mode

});

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");
Enter fullscreen mode Exit fullscreen mode

});
});

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
  );
}
Enter fullscreen mode Exit fullscreen mode

}
);

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:

https://technodesignn.ir


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)