Создание простой автономной информационной панели с помощью API (JS, Tailwind, HTML)
На прошлой неделе на работе я оказался в ситуации, когда мне нужно было создать панель мониторинга с данными временных рядов «в реальном времени» из энергосистемы Дании. Данные используются мной и моими коллегами в непрерывном анализе рынка электроэнергии, поэтому наличие свежих данных важно и может обеспечить преимущество при работе на рынках электроэнергии.
Зачем мне API-решение?
Раньше я получал данные с FTP-сервера, а затем имел скрипт Python, объединяющий HTML-файл со стилями и всем остальным. Это было запущено на облачной машине с использованием планировщика заданий (Windows), чтобы скрипт каждые пять минут создавал новый HTML-файл с самыми свежими данными. Однако данные были недостаточно актуальными, и на использование планировщика заданий на облачной машине не всегда можно было положиться.
Поэтому мне пришлось искать альтернативное решение. К счастью для меня, Energinet, датский оператор службы передачи (TSO), недавно выпустила свой API, который находится в свободном доступе для общественности. Различные наборы данных, для которых они предоставляют вызов API, можно найти здесь: https://www.energidataservice.dk/search.
Я никогда раньше не использовал API и лишь немного занимался веб-разработкой. Таким образом, я хотел сделать это как можно проще, не используя какой-либо внутренний сервер и сделав панель мониторинга автономной.
Что необходимо для создания удобной информационной панели с данными в реальном времени?
- API для запроса данных
- HTML-шаблон для отображения данных в
- 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-запрос, приведенный выше, выглядит так же, как приведенный ниже, при запросе в браузере.
Теперь у нас есть запрос, затем нам нужен 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()