Visualizing the Data
With our backend publishing data, let’s build the frontend to consume it. We’ll use a public client from the Arkiv SDK, which can read data without needing a private key.
Step 1: HTML Structure
Section titled “Step 1: HTML Structure”First, create the skeleton of our dashboard in index.html. We need containers for the price displays and <canvas> elements for Chart.js to draw on.
Directorybackend/
- index.js
- .env
- package.json
Directoryfrontend/
- index.html
- style.css
- script.js
- charts.js
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Arkiv Crypto Dashboard</title> <link rel="stylesheet" href="style.css"></head><body> <h1>Arkiv Real-Time Crypto Dashboard</h1>
<div class="main-container"> <div id="price-container" class="price-container"></div>
<div class="charts-container"> <div class="chart-box"> <h2>Bitcoin Price (USD)</h2> <canvas id="btcPriceChart"></canvas> </div> <div class="chart-box"> <h2>Ethereum Price (USD)</h2> <canvas id="ethPriceChart"></canvas> </div> <div class="chart-box"> <h2>Golem Price (USD)</h2> <canvas id="glmPriceChart"></canvas> </div> </div>
<div class="market-cap-chart"> <h2>Market Cap Comparison (USD) (log scale)</h2> <canvas id="marketCapChart"></canvas> </div> </div>
<script type="module" src="charts.js"></script> <script type="module" src="script.js"></script></body></html>Step 2: CSS Styling
Section titled “Step 2: CSS Styling”Add some styles in style.css to make the dashboard presentable.
Directorybackend/
- index.js
- .env
- package.json
Directoryfrontend/
- index.html
- style.css
- script.js
- charts.js
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; color: #333; padding: 20px; margin: 0; }h1 { text-align: center; color: #1a202c; margin-bottom: 30px; }h2 { text-align: center; font-size: 1.1rem; color: #4a5568; margin-bottom: 15px; }.main-container { max-width: 1400px; margin: auto; }.price-container { display: flex; justify-content: space-around; flex-wrap: wrap; margin-bottom: 30px; gap: 15px; }.price-box { background-color: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); min-width: 250px; text-align: center; transition: transform 0.2s; }.price-box:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }.price-box .token-name { font-size: 1.3rem; font-weight: bold; color: #2d3748; text-transform: uppercase; }.price-box .current-price { font-size: 2.2rem; margin: 12px 0; font-weight: 600; color: #1a202c; }.price-change { font-size: 1rem; font-weight: 500; }.price-change.positive { color: #2f855a; }.price-change.negative { color: #c53030; }.market-cap-chart { background-color: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 25px; height: 400px; }.charts-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 25px; margin-bottom: 25px; }.chart-box { background-color: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }.explorer-link { display: inline-block; margin-top: 12px; padding: 8px 16px; background-color: #4299e1; color: white; text-decoration: none; border-radius: 6px; font-size: 0.9rem; font-weight: 500; transition: background-color 0.2s; }.explorer-link:hover { background-color: #3182ce; }@media (max-width: 768px) { .charts-container { grid-template-columns: 1fr; } .price-container { flex-direction: column; align-items: center; } }Step 3: Chart Management Module
Section titled “Step 3: Chart Management Module”Before writing our main script, let’s create a separate module to handle all chart-related operations. This keeps our code organized with Arkiv logic in script.js and chart management in charts.js. Copy and paste the following into charts.js:
Directorybackend/
- index.js
- .env
- package.json
Directoryfrontend/
- index.html
- style.css
- script.js
- charts.js
// Import Chart.js and plugins from CDNimport {Chart, registerables} from 'https://cdn.jsdelivr.net/npm/chart.js@4/+esm';
// Chart instanceslet marketCapChart, btcPriceChart, ethPriceChart, glmPriceChart;
// Helper function to format market cap valuesfunction formatMarketCap(value) { if (value >= 1e12) return '$' + (value / 1e12).toFixed(1) + 'T'; if (value >= 1e9) return '$' + (value / 1e9).toFixed(1) + 'B'; if (value >= 1e6) return '$' + (value / 1e6).toFixed(0) + 'M'; return '$' + value.toLocaleString();}
// Plugin to draw labels above bars (only for bar charts)const afterDatasetsDraw = { id: 'afterDatasetsDraw', afterDatasetsDraw(chart) { // Only apply to bar charts if (chart.config.type !== 'bar') { return; }
const { ctx, chartArea: { top }, scales: { x, y } } = chart;
chart.data.datasets.forEach((dataset, datasetIndex) => { const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach((bar, index) => { const value = dataset.data[index]; const text = formatMarketCap(value);
ctx.save(); ctx.font = 'bold 12px sans-serif'; ctx.fillStyle = '#1f2937'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
// Draw text above the bar ctx.fillText(text, bar.x, bar.y - 5); ctx.restore(); }); }); }};
// Initialize all chart instancesexport function initializeCharts() { Chart.register(...registerables); Chart.register(afterDatasetsDraw);
// Configuration for line charts (price history)const lineChartConfig = (label, color) => ({ type: 'line', data: { labels: [], datasets: [{ label, data: [], borderColor: color, backgroundColor: color + '20', tension: 0.4, fill: true, }] }, options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: false } } }});
// Market cap bar chart showing latest values for each tokenmarketCapChart = new Chart( document.getElementById('marketCapChart'), { type: 'bar', data: { labels: ['Bitcoin', 'Ethereum', 'Golem'], datasets: [{ label: 'Market Cap (USD)', data: [0, 0, 0], backgroundColor: ['#f59e0b', '#8b5cf6', '#10b981'], borderColor: ['#d97706', '#7c3aed', '#059669'], borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, layout: { padding: { top: 30, bottom: 10, left: 10, right: 10 } }, scales: { y: { type: 'logarithmic', beginAtZero: false, min: 1e6, ticks: { callback: function(value) { if (value >= 1e12) return '$' + (value / 1e12).toFixed(1) + 'T'; if (value >= 1e9) return '$' + (value / 1e9).toFixed(1) + 'B'; if (value >= 1e6) return '$' + (value / 1e6).toFixed(0) + 'M'; return '$' + value.toLocaleString(); } } } } } });
// Initialize price history line chartsbtcPriceChart = new Chart( document.getElementById('btcPriceChart'), lineChartConfig('Bitcoin Price (USD)', '#f59e0b'));ethPriceChart = new Chart( document.getElementById('ethPriceChart'), lineChartConfig('Ethereum Price (USD)', '#8b5cf6'));glmPriceChart = new Chart( document.getElementById('glmPriceChart'), lineChartConfig('Golem Price (USD)', '#10b981'));}
// Update a line chart with new dataexport function updateChart(chart, data, priceKey) {const labels = data.map(d => { const date = new Date(d.timestamp); return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});}).reverse();const prices = data.map(d => d[priceKey]).reverse();
chart.data.labels = labels;chart.data.datasets[0].data = prices;chart.update();}
// Update market cap bar chart with latest valuesexport function updateMarketCapChart(btcData, ethData, glmData) {const latestValues = [ btcData.length > 0 ? btcData[0].marketCap : 0, ethData.length > 0 ? ethData[0].marketCap : 0, glmData.length > 0 ? glmData[0].marketCap : 0];
marketCapChart.data.datasets[0].data = latestValues;marketCapChart.update();}
// Render price boxes with current prices and 24h changesexport function updatePriceBoxes(btcData, ethData, glmData) { const container = document.getElementById('price-container'); container.innerHTML = '';
const tokens = [ { name: 'Bitcoin', data: btcData, symbol: 'BTC' }, { name: 'Ethereum', data: ethData, symbol: 'ETH' }, { name: 'Golem', data: glmData, symbol: 'GLM' } ];
tokens.forEach(token => { if (token.data.length === 0) return;
const latest = token.data[0]; const priceChange = latest.change24h || 0; const changeClass = priceChange >= 0 ? 'positive' : 'negative'; const changeSign = priceChange >= 0 ? '+' : ''; const explorerUrl = `https://explorer.kaolin.hoodi.arkiv.network/entity/${latest.entityKey}?tab=data`;
const box = document.createElement('div'); box.className = 'price-box'; box.innerHTML = ` <div class="token-name">${token.name} (${token.symbol})</div> <div class="current-price">$${latest.price.toLocaleString()}</div> <div class="price-change ${changeClass}"> ${changeSign}${priceChange.toFixed(2)}% (24h) </div> <a href="${explorerUrl}" target="_blank" class="explorer-link">View on Arkiv Explorer →</a> `; container.appendChild(box); });}
// Export chart instances for direct access if neededexport function getCharts() {return { marketCapChart, btcPriceChart, ethPriceChart, glmPriceChart };}Step 4: Client Setup and Imports
Section titled “Step 4: Client Setup and Imports”In script.js, start by importing the Arkiv SDK and chart functions. Set your wallet address at the top so we only fetch entities you created. Because this is a browser environment, we import the SDK directly from a CDN like esm.sh.
Directorybackend/
- index.js
- .env
- package.json
Directoryfrontend/
- index.html
- style.css
- script.js
- charts.js
// TODO: Replace with your wallet address from the backendconst USER_ADDRESS = '0xYourAddressHere';
if (!USER_ADDRESS || USER_ADDRESS === '0xYourAddressHere') { alert('Please set your USER_ADDRESS in script.js to your wallet address!'); throw new Error('USER_ADDRESS not configured');}
// Import Arkiv SDK for blockchain data accessimport { createPublicClient, http } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/query?target=es2022&bundle-deps";import { kaolin } from 'https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/chains?target=es2022&bundle-deps';
// Public client can only read data, no private key neededconst client = createPublicClient({ chain: kaolin, transport: http(),});
console.log('Arkiv client initialized for address:', USER_ADDRESS);Step 5: Querying Data from Arkiv
Section titled “Step 5: Querying Data from Arkiv”Now add the query function that fetches entities from Arkiv. Notice we query by the token attribute our backend set (e.g., 'bitcoin', 'ethereum', 'golem') and filter by .ownedBy() to only get your entities.
// TODO: Replace with your wallet address from the backendconst USER_ADDRESS = '0xYourAddressHere';
if (!USER_ADDRESS || USER_ADDRESS === '0xYourAddressHere') { alert('Please set your USER_ADDRESS in script.js to your wallet address!'); throw new Error('USER_ADDRESS not configured');}
// Import Arkiv SDK for blockchain data accessimport { createPublicClient, http } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/query?target=es2022&bundle-deps";import { kaolin } from 'https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/chains?target=es2022&bundle-deps';
// Public client can only read data, no private key neededconst client = createPublicClient({ chain: kaolin, transport: http(),});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchainasync function fetchTokenDataFromArkiv(tokenId) { const query = client.buildQuery(); const result = await query .where(eq('token', tokenId)) .ownedBy(USER_ADDRESS) .withPayload(true) .fetch();
return result.entities .map(e => ({...e.toJson(), entityKey: e.key})) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));}Step 6: Putting It All Together
Section titled “Step 6: Putting It All Together”Finally, create the main update function that fetches data for all cryptocurrencies and updates the UI. Initialize everything when the page loads and set up automatic refreshes every 15 seconds.
Directorybackend/
- index.js
- .env
- package.json
Directoryfrontend/
- index.html
- style.css
- script.js
- charts.js
// TODO: Replace with your wallet address from the backendconst USER_ADDRESS = '0xYourAddressHere';
if (!USER_ADDRESS || USER_ADDRESS === '0xYourAddressHere') { alert('Please set your USER_ADDRESS in script.js to your wallet address!'); throw new Error('USER_ADDRESS not configured');}
// Import Arkiv SDK for blockchain data accessimport { createPublicClient, http } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/query?target=es2022&bundle-deps";import { kaolin } from 'https://esm.sh/@arkiv-network/sdk@0.3.2-dev.1/chains?target=es2022&bundle-deps';
// Import chart management functionsimport { initializeCharts, updateChart, updateMarketCapChart, updatePriceBoxes, getCharts} from './charts.js';
// Public client can only read data, no private key neededconst client = createPublicClient({ chain: kaolin, transport: http(),});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchainasync function fetchTokenDataFromArkiv(tokenId) { const query = client.buildQuery(); const result = await query .where(eq('token', tokenId)) .ownedBy(USER_ADDRESS) .withPayload(true) .fetch();
return result.entities .map(e => ({...e.toJson(), entityKey: e.key})) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));}
// Main update function to refresh all UI elementsasync function updateUI() { try { // Fetch data for all three cryptocurrencies const [btcData, ethData, glmData] = await Promise.all([ fetchTokenDataFromArkiv('bitcoin'), fetchTokenDataFromArkiv('ethereum'), fetchTokenDataFromArkiv('golem') ]);
// Update UI if we have any data if (btcData.length > 0 || ethData.length > 0 || glmData.length > 0) { updatePriceBoxes(btcData, ethData, glmData); updateMarketCapChart(btcData, ethData, glmData);
// Get chart instances and update price history charts const { btcPriceChart, ethPriceChart, glmPriceChart } = getCharts(); if (btcData.length > 0) updateChart(btcPriceChart, btcData, 'price'); if (ethData.length > 0) updateChart(ethPriceChart, ethData, 'price'); if (glmData.length > 0) updateChart(glmPriceChart, glmData, 'price');
console.log('UI updated successfully'); } } catch (error) { console.error('Error updating UI:', error); }}
// Initialize on page loadwindow.addEventListener('DOMContentLoaded', () => { initializeCharts(); updateUI(); setInterval(updateUI, 15000); // Refresh every 15 seconds});In the next step, we’ll run the frontend and see the data in action!