Skip to main content

Displaying Variables on HTML Canvas

This widget allows you to create custom visualizations using HTML, CSS, and JavaScript. It’s a flexible option for advanced users who want to design personalized dashboards or interactive data views.


Step-by-Step Instructions

  1. Open Dashboards

    In the top navigation bar, click on Data, then select Dashboards. This will open the dashboard management area.


  1. Add a New Widget

    Click the Add new Widget button (marked with a + icon).


  1. Choose a Widget Type

    From the list of available widgets, choose the HTML Canvas.


  1. Configure HTML Canvas Settings

In the SETTINGS tab, configure the available options.
This is where you can enable or disable features such as:

  • Preload dashboard data (recommended: ON)
  • Enable React.js (optional, for dynamic rendering)
  • Add third-party libraries (for external JS/CSS resources)

After configuration, click Edit code under Code editor and insert your HTML, CSS, and JavaScript.


  1. Add HTML Code to the Canvas

In the Code Editor, insert your desired layout and logic.


  1. Edit Canvas Appearance

Switch to the APPEARANCE tab to modify the visual style of your widget.
You can set:

  • Widget name
  • Option to hide the header
  • Add a custom style (JSON format)


Example of HTML Canvas (Floor Plan)


Configure the Floor Plan Widget

Before using the HTML code, you must update several parameters inside the CONFIG section of the script.
These define your Ubidots connection, the floor plan image, and the BLE tag positions.


ParameterDescriptionRequired
CONFIG.ubidots.tokenYour Ubidots API token (found under API Credentials in Ubidots).
CONFIG.ubidots.deviceLabelThe device label that contains your BLE sensor variables.
CONFIG.floorplanUrlPublic URL to your PNG/JPG floor plan image. You can host it via Postimages or Imgur.
CONFIG.pollingMsHow often to refresh values from Ubidots, in milliseconds. Default: 15000 (15 seconds).⚙️ Optional
CONFIG.tagsArray containing all BLE sensors with their names, indexes, and positions. Each entry represents one sensor pin on the floor plan.
roomDisplay name (e.g., "Server Room").
idxBLE tag index (0–N) — values are read from: ble_tags.<idx>.temperature.avg and ble_tags.<idx>.humidity.avg.
x, yPosition in percentages (0–100) relative to the image dimensions.
loc.anchorLocator arrow direction — "down" (below), "top" (above), or "diag" (diagonal).⚙️ Optional
loc.dx, loc.dyFine offset adjustment for the locator position in pixels.⚙️ Optional
loc.angleRotation of the locator in degrees (used with "diag").⚙️ Optional
loc.mirrorFlip the locator horizontally (true / false).⚙️ Optional

Example Code – HTML Floor Plan

<div id="floor-wrapper">
<img id="floorplan" alt="Floor plan" />
<div id="pins"></div>
</div>

<style>
#floor-wrapper {
position: relative;
width: 100%;
max-width: 1000px; /* You can change the max width of the canvas */
margin: 0 auto;
}
/* The image scales responsively; coordinates are given in PERCENT (0–100) */
#floorplan { width: 100%; display: block; }

.pin-wrapper {
position: absolute;
transform: translate(-50%, -50%);
}

.pin {
width: 65px;
height: 65px;
border-radius: 50%;
background: #e60000; /* You may change bubble color */
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,.25);
pointer-events: none;
position: relative;
}
.pin .room {
font-size: 8px;
font-weight: 700;
line-height: 1.1;
margin-bottom: 2px;
max-width: 56px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pin .t { font-size: 12px; font-weight: 700; line-height: 1.2; }
.pin .h { font-size: 10px; font-weight: 500; line-height: 1.1; opacity: .95; }

.locator {
position: absolute;
width: 20px;
height: 30px;
pointer-events: none;
}

@media (max-width: 520px) {
.pin { width: 56px; height: 56px; }
.pin .room { font-size: 7px; }
.pin .t { font-size: 10px; }
.pin .h { font-size: 9px; }
.locator { width: 16px; height: 24px; }
}
</style>

<script>
/* ========= CONFIG (fill these) ========= */
const CONFIG = {
ubidots: {
token: "PASTE_UBIDOTS_TOKEN", /* REQUIRED: your Ubidots API token */
deviceLabel: "PASTE_DEVICE_LABEL" /* REQUIRED: your Ubidots device label */
},

floorplanUrl: "PASTE_FLOORPLAN_IMAGE_URL", /* REQUIRED: public URL of your floor plan image (PNG/JPG) */

pollingMs: 15000, /* OPTIONAL: refresh interval in ms */

/* REQUIRED: Define your BLE tags (coordinates in PERCENT 0–100; data from ble_tags.<idx>.*.avg) */
tags: [
{ room: "Tag-1", idx: 0, x: 50, y: 30, loc: { anchor: "down", dx: 0, dy: -5 } },
{ room: "Tag-2", idx: 2, x: 12, y: 20, loc: { anchor: "down", dx: 0, dy: -5 } },
{ room: "Tag-3", idx: 3, x: 90, y: 17, loc: { anchor: "down", dx: 0, dy: -5 } },
{ room: "Tag-4", idx: 4, x: 80, y: 52, loc: { anchor: "down", dx: 0, dy: -5 } }
]
};
/* ======== END CONFIG ======== */

const floor = document.getElementById("floorplan");
const pinsContainer = document.getElementById("pins");
floor.src = CONFIG.floorplanUrl;

function markerSVG(hex) {
if (!hex) hex = "#e60000";
return '' +
'<svg viewBox="0 0 24 24" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
'<path fill="' + hex + '" d="M12 2c-4.42 0-8 3.58-8 8 0 5.25 8 12 8 12s8-6.75 8-12c0-4.42-3.58-8-8-8z"/>' +
'<circle cx="12" cy="10" r="3" fill="#ffffff"/>' +
'</svg>';
}

function apiUrl(variableLabel) {
return 'https://industrial.api.ubidots.com/api/v1.6/devices/' +
encodeURIComponent(CONFIG.ubidots.deviceLabel) + '/' +
encodeURIComponent(variableLabel) + '/values?page_size=1';
}

async function getLastValue(varLabel) {
const res = await fetch(apiUrl(varLabel), { headers: { "X-Auth-Token": CONFIG.ubidots.token } });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
const v = (data && data.results && data.results[0]) ? data.results[0] : null;
return v ? v.value : null;
}

function placeLocator(el, loc) {
const anchor = (loc && loc.anchor) ? loc.anchor : "down";
const dx = Number(loc && (loc.dx != null) ? loc.dx : 0);
const dy = Number(loc && (loc.dy != null) ? loc.dy : 0);
const angle = Number(loc && (loc.angle != null) ? loc.angle : 0);
const mirror = !!(loc && loc.mirror);

el.style.left = ""; el.style.right = ""; el.style.top = ""; el.style.bottom = "";
const sx = mirror ? -1 : 1;

if (anchor === "down") {
el.style.left = "50%";
el.style.bottom = (-18 + dy) + "px";
el.style.transform = "translateX(-50%) rotate(0deg) scaleX(" + sx + ")";
} else if (anchor === "top") {
el.style.left = "50%";
el.style.top = (-18 + dy) + "px";
el.style.transform = "translateX(-50%) rotate(180deg) scaleX(" + sx + ")";
} else if (anchor === "diag") {
el.style.right = (-10 + dx) + "px";
el.style.bottom = (-10 + dy) + "px";
el.style.transform = "rotate(" + angle + "deg) scaleX(" + sx + ")";
}
}

function ensurePins() {
pinsContainer.innerHTML = "";
CONFIG.tags.forEach(function(tag, i) {
const wrap = document.createElement("div");
wrap.className = "pin-wrapper";
wrap.id = "wrap-" + i;

const pin = document.createElement("div");
pin.className = "pin";
pin.id = "pin-" + i;
pin.innerHTML =
'<span class="room">' + (tag.room || ('Tag ' + (i + 1))) + '</span>' +
'<span class="t">--.-°C</span>' +
'<span class="h">--%</span>';

const loc = document.createElement("div");
loc.className = "locator";
loc.innerHTML = markerSVG();

pin.appendChild(loc);
wrap.appendChild(pin);
pinsContainer.appendChild(wrap);

placeLocator(loc, tag.loc || {});
});
positionPins();
}

function positionPins() {
const rect = floor.getBoundingClientRect();
CONFIG.tags.forEach(function(tag, i) {
const wrap = document.getElementById("wrap-" + i);
if (!wrap) return;

/* PERCENT COORDINATES: x,y are 0–100 relative to the current rendered image size */
const px = (tag.x / 100) * rect.width;
const py = (tag.y / 100) * rect.height;

wrap.style.left = px + "px";
wrap.style.top = py + "px";
});
}

async function refreshData() {
for (let i = 0; i < CONFIG.tags.length; i++) {
const tag = CONFIG.tags[i];
const base = "ble_tags." + tag.idx; /* Always reading BLE by index with .avg */
const tLabel = base + ".temperature.avg";
const hLabel = base + ".humidity.avg";

try {
const values = await Promise.all([
getLastValue(tLabel),
getLastValue(hLabel)
]);
const t = values[0], h = values[1];
const el = document.getElementById("pin-" + i);
if (el) {
el.querySelector(".t").textContent = (t == null ? "--.-°C" : (Number(t).toFixed(1) + "°C"));
el.querySelector(".h").textContent = (h == null ? "--%" : (Math.round(h) + "%"));
}
} catch (e) {
console.warn("Fetch error (pin " + i + "):", e);
}
}
}

ensurePins();
floor.addEventListener("load", positionPins);
window.addEventListener("resize", positionPins);
refreshData();
setInterval(refreshData, CONFIG.pollingMs);
</script>

Example of HTML Canvas (Data Table)


Configure the Table Widget

Before using the HTML code, you must update several parameters inside the CONFIG section of the script.


ParameterDescriptionRequired
CONFIG.ubidots.tokenYour Ubidots API token (found under API Credentials).
CONFIG.ubidots.deviceLabelDevice label in Ubidots that contains the BLE variables.
CONFIG.pollingMsRefresh interval in milliseconds. Default: 15000.⚙️ Optional
CONFIG.tagsArray of rows to display in the table. Each row maps to ble_tags.<idx>.*.
nameDisplay name of the tag (table’s first column).
idxBLE tag index (0–N) used to read: rssi, voltage, temperature.avg, humidity.avg.

Example Code – HTML Data Table

<!-- ======== HTML TABLE: BLE TAGS STATUS ======== -->
<table id="tags-table" cellspacing="0" cellpadding="8"
style="width:100%; border-collapse: collapse; text-align:center; font-family: system-ui, sans-serif;">
<thead>
<tr style="background:#e60000; color:#fff;">
<th style="border:1px solid #ddd;">🏷️ Tag</th>
<th style="border:1px solid #ddd;">🛰️ RSSI<br><span style="font-weight:400;font-size:12px;">(dBm)</span></th>
<th style="border:1px solid #ddd;">🔋 Voltage<br><span style="font-weight:400;font-size:12px;">(V)</span></th>
<th style="border:1px solid #ddd;">🌡️ Temperature<br><span style="font-weight:400;font-size:12px;">(°C)</span></th>
<th style="border:1px solid #ddd;">💧 Humidity<br><span style="font-weight:400;font-size:12px;">(%)</span></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>

<script>
/* ========= CONFIG (fill these) ========= */
const CONFIG = {
ubidots: {
token: "PASTE_UBIDOTS_TOKEN", /* REQUIRED: your Ubidots API token */
deviceLabel: "PASTE_DEVICE_LABEL" /* REQUIRED: your Ubidots device label */
},
pollingMs: 15000, /* OPTIONAL: refresh interval in ms */
/* REQUIRED: Define your BLE tags (idx = ble_tags.<idx>.*) */
tags: [
{ name: "Tag-1", idx: 0 },
{ name: "Tag-2", idx: 1 },
{ name: "Tag-3", idx: 2 },
{ name: "Tag-4", idx: 3 },
{ name: "Tag-5", idx: 4 }
]
};
/* ======== END CONFIG ======== */

const tbodyEl = document.getElementById("tbody");

function apiUrl(variableLabel) {
return "https://industrial.api.ubidots.com/api/v1.6/devices/"
+ encodeURIComponent(CONFIG.ubidots.deviceLabel)
+ "/"
+ encodeURIComponent(variableLabel)
+ "/values?page_size=1";
}

async function getLastValue(varLabel) {
try {
const res = await fetch(apiUrl(varLabel), {
headers: { "X-Auth-Token": CONFIG.ubidots.token }
});
if (!res.ok) return null;
const data = await res.json();
return (data && data.results && data.results[0])
? data.results[0].value
: null;
} catch {
return null;
}
}

function renderTableSkeleton() {
tbodyEl.innerHTML = "";
CONFIG.tags.forEach(t => {
const tr = document.createElement("tr");
tr.innerHTML =
'<td style="border:1px solid #eee; text-align:left; padding-left:10px; font-weight:600;">'
+ t.name + "</td>"
+ '<td id="rssi-' + t.idx + '" style="border:1px solid #eee;">--</td>'
+ '<td id="voltage-' + t.idx + '" style="border:1px solid #eee;">--</td>'
+ '<td id="temp-' + t.idx + '" style="border:1px solid #eee;">--</td>'
+ '<td id="hum-' + t.idx + '" style="border:1px solid #eee;">--</td>';
tbodyEl.appendChild(tr);
});
}

function formatVoltage(v) {
if (v == null) return "--";
const num = Number(v);
if (num >= 1) return num.toFixed(2) + " V";
return (num * 1000).toFixed(0) + " mV";
}

async function refreshOnce() {
for (const t of CONFIG.tags) {
// Variable labels:
// RSSI: ble_tags.<idx>.rssi
// Voltage: ble_tags.<idx>.voltage
// Temperature: ble_tags.<idx>.temperature.avg
// Humidity: ble_tags.<idx>.humidity.avg
const rssiLabel = "ble_tags." + t.idx + ".rssi";
const voltLabel = "ble_tags." + t.idx + ".voltage";
const tempLabel = "ble_tags." + t.idx + ".temperature.avg";
const humLabel = "ble_tags." + t.idx + ".humidity.avg";

try {
const [rssi, volt, temp, hum] = await Promise.all([
getLastValue(rssiLabel),
getLastValue(voltLabel),
getLastValue(tempLabel),
getLastValue(humLabel)
]);

setText("rssi-" + t.idx, rssi == null ? "--" : Math.round(rssi) + " dBm");
setText("voltage-" + t.idx, volt == null ? "--" : formatVoltage(volt));
setText("temp-" + t.idx, temp == null ? "--" : Number(temp).toFixed(1) + " °C");
setText("hum-" + t.idx, hum == null ? "--" : Math.round(hum) + " %");
} catch (e) {
console.warn("Tag " + t.idx + " error:", e);
}
}
}

function setText(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}

renderTableSkeleton();
refreshOnce();
setInterval(refreshOnce, CONFIG.pollingMs);
</script>