Создание простой автономной информационной панели с помощью API (JS, Tailwind, HTML)

На прошлой неделе на работе я оказался в ситуации, когда мне нужно было создать панель мониторинга с данными временных рядов «в реальном времени» из энергосистемы Дании. Данные используются мной и моими коллегами в непрерывном анализе рынка электроэнергии, поэтому наличие свежих данных важно и может обеспечить преимущество при работе на рынках электроэнергии.

Зачем мне API-решение?

Раньше я получал данные с FTP-сервера, а затем имел скрипт Python, объединяющий HTML-файл со стилями и всем остальным. Это было запущено на облачной машине с использованием планировщика заданий (Windows), чтобы скрипт каждые пять минут создавал новый HTML-файл с самыми свежими данными. Однако данные были недостаточно актуальными, и на использование планировщика заданий на облачной машине не всегда можно было положиться.

Поэтому мне пришлось искать альтернативное решение. К счастью для меня, Energinet, датский оператор службы передачи (TSO), недавно выпустила свой API, который находится в свободном доступе для общественности. Различные наборы данных, для которых они предоставляют вызов API, можно найти здесь: https://www.energidataservice.dk/search.

Я никогда раньше не использовал API и лишь немного занимался веб-разработкой. Таким образом, я хотел сделать это как можно проще, не используя какой-либо внутренний сервер и сделав панель мониторинга автономной.

Что необходимо для создания удобной информационной панели с данными в реальном времени?

  1. API для запроса данных
  2. HTML-шаблон для отображения данных в
  3. JS-скрипт для запроса данных и их экспорта в HTML-шаблон

API для запроса данных

Во-первых, вам нужно выяснить, какие данные нужно получить и где эти данные доступны. В моем случае мне нужны данные об энергосистеме Дании, которые можно найти здесь Энергетическая система прямо сейчас.

Данные на изображении выше — это данные временных рядов с периодичностью в одну минуту, которые я хочу получить. Прочитать документацию по API и посмотреть примеры запросов можно здесь Руководства по API. API можно запрашивать двумя способами. Поскольку я знаком с SQL, я буду использовать этот тип запроса.

Пример запроса (первые 5 результатов)
https://api.energidataservice.dk/datastore_search?resource_id=powersystemrightnow&limit=5

Пример запроса (через оператор SQL)
https://api.energidataservice.dk/datastore_search_sql?sql=SELECT * from «powersystemrightnow LIMIT 5»

Мне нужны самые свежие данные за сегодняшний день. Таким образом, мой SQL-запрос выглядит так:

SELECT *
FROM «powersystemrightnow»
ГДЕ «Minutes1DK» › timestamp'2022–02–05 00:00:00'
И «Minutes1DK» ‹ timestamp'2022–02– 05 23:59:59'
ЗАКАЗАТЬ ПО DESC «Minutes1DK»

Согласно документации API, мне нужно поместить "кавычки" вокруг запрашиваемой таблицы и использовать отметку времениобъявление при запросе. Вы можете использовать Почтальон, чтобы упростить тестирование и выяснить, как писать запросы. Кроме того, браузер будет форматировать пробелы и специальные символы. Пробел преобразуется в %20, а кавычка преобразуется в %22. Это означает, что мой SQL-запрос, приведенный выше, выглядит так же, как приведенный ниже, при запросе в браузере.

https://api.energidataservice.dk/datastore_search_sql?sql=SELECT%20*%20from%20%22powersystemrightnow%22%20WHERE%20%22Minutes1DK%22%20%3E%20timestamp%272022-02-05%2000: 00:00%27%20AND%20%22минуты1DK%22%20%3C%20timestamp%272022-02-05%2023:59:59%27%20ORDER%20BY%20%22минуты1DK%22%20DESC

Теперь у нас есть запрос, затем нам нужен HTML-шаблон, который можно использовать для отображения данных.

Если вы раньше не пробовали работать со структурами json , было бы неплохо прочитать кое-что об этом. Это не слишком сложно, и полезно знать об этом при работе с API.

Вот как объект структурирован из API в этом примере. Попробуйте использовать консоль в своем браузере, чтобы посмотреть, как вы получаете доступ к конкретным данным, которые вам нужны, из запроса API.

HTML-шаблон для отображения данных в

Мы получили запрос данных, теперь нам нужно где-то показать эти данные. Для этого я создаю HTML-оболочку, где я могу заставить JavaScript вставлять свежие данные в формате таблицы.

Я не буду использовать CSS для оформления, вместо этого я буду использовать чистый Tailwind. Если вы еще не слышали о Tailwind, то я бы порекомендовал вам его поискать. По сути, это фреймворк, который позволяет легко стилизовать HTML-элементы настраиваемым и быстрым способом. Все стили с Tailwind выполняются в классах элементов HTML, поэтому HTML может выглядеть немного беспорядочно.

Я включил три скрипта: один для Axios, Tailwind и App.js, где находится мой собственный JS-код. Скрипты для Axios и Tailwind берутся из сетей доставки контента (CDN), поэтому вам не нужно загружать скрипты локально.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Energinet</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-green-100/80">
<header class="px-4 py-8 bg-green-900 space-y-2">
<h1 class="text-4xl text-white font-sans">Energinet</h1>
<p class="text-white">Data sourced: 
<a href="https://www.energidataservice.dk/tso-electricity/powersystemrightnow" class="underline text-red-200">Power System Right Now</a></p>
<label for="dateSelect" class="text-white">Chose date:</label>
<input type="date" id="dateSelect" class="border-2 border-slate-200 rounded-lg">
</header>
<div class="flex flex-col my-8">
<table class="table-auto">
<thead class="bg-gray-100 dark:bg-gray-700">
<tr id="tableHeaders"></tr>
</thead>
<tbody id="tableRows">
</tbody>
</table>
</div>
<script src="app.js"></script>
</body>
</html>

На картинке выше показано, как выглядит HTML-шаблон без запущенного моего пользовательского JavaScript. Хотя не видно, что на этой странице есть таблица, по крайней мере, в HTML-разметке. Эта таблица была создана со специально размещенными идентификаторами в двух элементах; а именно tableHeaders и tableRows.

Идентификатор tableHeaders помещается в элемент ‹tr› внутри элемента ‹thead› и используется для добавления к нему элементов th› с именами заголовков. Идентификатор tableRows помещается в элемент ‹tbody› и используется, чтобы сначала добавить элемент ‹tr› в виде строки, а затем добавить элемент ‹td› с отдельными значениями. Все это происходит в JavaScript, однако без какой-нибудь умнойHTML-разметки не обойтись.

Что касается обычной HTML-разметки, то здесь особо нечего сказать. У меня есть H1, ссылка на исходный источник данных и средство выбора даты, которое используется в JavaScript для выбора даты, с которой вы хотите получить данные.

JS-скрипт для запроса данных и их экспорта в HTML-шаблон

Раздел JavaScript этого примера состоит из пяти функций, некоторых второстепенных и некоторых важных. Вот список функций в порядке их появления в файле JavaScript.

smallDate→ Функция для добавления нуля перед месяцем (int), если он меньше 10. Это способ обойти new Date().getMonth( ) в JavaScript, возвращающем целое число, и когда это используется для SQL, нам нужно иметь две цифры для всех месяцев (это означает, что март равен 03 вместо 3). Кроме того, функция фактически возвращает 0 для января, поэтому нужно добавить 1 к выходным данным, чтобы это имело смысл в реальном мире.

const smallDate = (datePart) => {
    if (datePart < 10) {
        return `0${datePart}`;} 
    return `${datePart}`;}

deleteChild → Эта функция используется позже для очистки таблицы всякий раз, когда мы вводим новые данные при изменении даты и поиске новых данных.

// This clears the table before adding new data.
function deleteChild(parent) {
    let child = parent.lastElementChild;
    while (child) {
        parent.removeChild(child);
        child = parent.lastElementChild;
        }
    }

loadEnerginet → Эта функция запрашивает API и выводит данные в виде объекта. Здесь URL-адрес для API запрашивается через асинхронную функцию с использованием Axios. В URL-адресе запроса я сделал так, что запрос будет принимать dateSelect.value в качестве входных данных. Если вы не знакомы с промисами в JavaScript, возможно, вам стоит изучить этот вопрос подробнее. Однако вы также можете просто попробовать этот способ сделать это с помощью API по вашему выбору (он может работать нормально с небольшой настройкой).

const loadEnerginet = async () => {
    try {
        const query = `https://api.energidataservice.dk/datastore_search_sql?sql=SELECT * from "powersystemrightnow" WHERE "Minutes1DK" > timestamp'${dateSelect.value} 00:00:00' AND "Minutes1DK" < timestamp'${dateSelect.value} 23:59:59' ORDER BY "Minutes1DK" DESC`
        const res = await axios.get(query)
        console.log(res)
        return res.data.result;
        } catch (e) {
        console.log(e);
        }
}

loadTable→ Эта функция вызывает функцию loadEnerginet и преобразует данные в таблицу.Может быть проще скопировать и вставить код в VS-код и отформатировать код, так как форматирование немного сложно на Medium.

Код загружает данные и начинает с создания списка заголовков, обходя некоторые столбцы с помощью простого сопоставления строк (.startsWith). Затем создается ‹th› элемент, к которому добавляется текст заголовка. Затем ‹th› добавляется к ‹tr› с идентификатором tableHeaders.

Затем аналогичным образом создаются строки таблицы. Объект данных зацикливается с помощью метода .entries(), который возвращает индекс и значение для каждой строки в объекте данных.
Для каждого Значения(строки) в объекте данных создается ‹tr›, список заголовков перебирается в цикле, ‹td› создается и добавляется со значением данных объект в данной строке с заданным заголовком.
В конце цикла строки ‹tr› добавляется к ‹tbody› с идентификатором tableBody.

Все заголовки и строки оформлены в стиле Tailwind, что можно увидеть в их классах.

const loadTable = async () => {
// Loading the data from the API
const tableData = await loadEnerginet();
console.log(tableData)
// Remove earlier table data
deleteChild(tableHeaders)
// Create the table headers:
// Storing the table headers element
const tableHeaders = document.querySelector('#tableHeaders')
// Creating a headers list
const headerList = []
for (let header of tableData.fields) {
if (!header.id.startsWith('_') & !header.id.endsWith('UTC')) {
// Creating a new element and appending the headers to it
const newHeader = document.createElement('th');
newHeader.innerText = header.id.replace('_', ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
newHeader.classList.add('py-3', 'px-6', 'text-xs', 'font-medium', 'tracking-wider', 'text-left', 'text-gray-700', 'uppercase', 'dark:text-gray-400')
tableHeaders.append(newHeader)
// Saving the header name in the headers list
headerList.push(header.id)
}
}
// Storing the table rows
const tableRows = document.querySelector('#tableRows')
// Remove earlier table data
deleteChild(tableRows)
for (let [keys, values] of tableData.records.entries()) {
// Creating a new table row
const newRow = document.createElement('tr');
newRow.classList.add('border-b', 'odd:bg-white', 'even:bg-gray-50', 'odd:dark:bg-gray-800', 'even:dark:bg-gray-700', 'dark:border-gray-600', 'hover:bg-green-200')
for (let header of headerList) {
const rowValue = document.createElement('td');
if (header.startsWith('Minutes')) {
rowValue.innerText = values[header].slice(11, values[header].length - 3);
} else {
rowValue.innerText = Math.round(values[header])
}
rowValue.classList.add('py-4', 'px-6', 'text-sm', 'font-medium', 'text-gray-900', 'whitespace-nowrap', 'dark:text-white')
newRow.append(rowValue);
}
tableRows.append(newRow)
}
}

Помимо функций, я также создаю константу даты, добавляю прослушиватель событий в средство выбора даты и запускаю функцию loadTable() один раз, чтобы инициировать таблицу.

Весь JS-скрипт можно увидеть ниже. Как упоминалось ранее, его будет легче читать, если вы скопируете и вставите его в VS Code или другую IDE и отформатируете его, так как форматирование в Medium довольно беспорядочно.

Спасибо за прочтение и удачи в ваших собственных проектах!

const dateObj = new Date()
const smallDate = (datePart) => {
if (datePart < 10) {
return `0${datePart}`;
}
return `${datePart}`;
}
let date = `${dateObj.getFullYear()}-${smallDate(dateObj.getMonth() + 1)}-${smallDate(dateObj.getDate())}`
const dateSelect = document.querySelector('#dateSelect')dateSelect.value = date

// This clears the table before adding new data.
function deleteChild(parent) {
let child = parent.lastElementChild;
while (child) {
parent.removeChild(child);
child = parent.lastElementChild;
}
}
const loadEnerginet = async () => {
try {
const query = `https://api.energidataservice.dk/datastore_search_sql?sql=SELECT * from "powersystemrightnow" WHERE "Minutes1DK" > timestamp'${dateSelect.value} 00:00:00' AND "Minutes1DK" < timestamp'${dateSelect.value} 23:59:59' ORDER BY "Minutes1DK" DESC`
const res = await axios.get(query)
console.log(res)
return res.data.result;
} catch (e) {
console.log(e);
}
}
const loadTable = async () => {
// Loading the data from the API
const tableData = await loadEnerginet();
console.log(tableData)
// Create the table headers:
// Storing the table headers element
const tableHeaders = document.querySelector('#tableHeaders')
// Remove earlier table data
deleteChild(tableHeaders)
// Creating a headers list
const headerList = []
for (let header of tableData.fields) {
if (!header.id.startsWith('_') & !header.id.endsWith('UTC')) {
// Creating a new element and appending the headers to it
const newHeader = document.createElement('th');
newHeader.innerText = header.id.replace('_', ' ').replace(/([a-z])([A-Z])/g, '$1 $2');;
newHeader.classList.add('py-3', 'px-6', 'text-xs', 'font-medium', 'tracking-wider', 'text-left', 'text-gray-700', 'uppercase', 'dark:text-gray-400')
tableHeaders.append(newHeader)
// Saving the header name in the headers list
headerList.push(header.id)
}
}
// Storing the table rows
const tableRows = document.querySelector('#tableRows')
// Remove earlier table data
deleteChild(tableRows)
for (let [keys, values] of tableData.records.entries()) {
// Creating a new table row
const newRow = document.createElement('tr');
newRow.classList.add('border-b', 'odd:bg-white', 'even:bg-gray-50', 'odd:dark:bg-gray-800', 'even:dark:bg-gray-700', 'dark:border-gray-600', 'hover:bg-green-200')
for (let header of headerList) {
const rowValue = document.createElement('td');
if (header.startsWith('Minutes')) {
rowValue.innerText = values[header].slice(11, values[header].length - 3);
} else {
rowValue.innerText = Math.round(values[header])
}
rowValue.classList.add('py-4', 'px-6', 'text-sm', 'font-medium', 'text-gray-900', 'whitespace-nowrap', 'dark:text-white')
newRow.append(rowValue);
}
tableRows.append(newRow)
}
}
dateSelect.addEventListener('change', loadTable)
loadTable()