Visualizing the Data
This guide targets the Tiramisu testnet. Check Networks for current testnets and faucets.
The backend now publishes data. Build the frontend that consumes it. This step uses a public client from the Arkiv SDK, which reads data without a private key.
Step 1: HTML Structure
Section titled “Step 1: HTML Structure”First, create the skeleton of the dashboard in index.html. You 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 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); }.entity-key { display: inline-block; margin-top: 12px; color: #a0aec0; font-size: 0.8rem; font-family: monospace; }@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 you write the main script, create a separate module for chart operations. This module keeps the Arkiv logic in script.js apart from the chart management in charts.js. Copy this code 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 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> <div class="entity-key">${latest.entityKey.slice(0, 14)}...</div> `; 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, import the Arkiv SDK and the chart functions. Set your wallet address at the top so the app fetches only your entities. This is a browser environment, so 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 } from "https://esm.sh/@arkiv-network/sdk?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk/query?target=es2022&bundle-deps";import { tiramisu } from 'https://esm.sh/@arkiv-network/sdk/chains?target=es2022&bundle-deps';import { http } from 'https://esm.sh/viem?target=es2022';
// Public client can only read data, no private key neededconst client = createPublicClient({ chain: tiramisu, 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. It queries by the token attribute that the backend set, for example 'bitcoin', 'ethereum', or 'golem'. It also filters with .ownedBy() to fetch only 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 } from "https://esm.sh/@arkiv-network/sdk?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk/query?target=es2022&bundle-deps";import { tiramisu } from 'https://esm.sh/@arkiv-network/sdk/chains?target=es2022&bundle-deps';import { http } from 'https://esm.sh/viem?target=es2022';
// Public client can only read data, no private key neededconst client = createPublicClient({ chain: tiramisu, transport: http(),});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchainasync function fetchTokenDataFromArkiv(tokenId) { const result = await client .select({ key: true, payload: true }) .where(eq('token', tokenId)) .ownedBy(USER_ADDRESS) .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 a refresh 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 } from "https://esm.sh/@arkiv-network/sdk?target=es2022&bundle-deps";import { eq } from "https://esm.sh/@arkiv-network/sdk/query?target=es2022&bundle-deps";import { tiramisu } from 'https://esm.sh/@arkiv-network/sdk/chains?target=es2022&bundle-deps';import { http } from 'https://esm.sh/viem?target=es2022';
// 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: tiramisu, transport: http(),});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchainasync function fetchTokenDataFromArkiv(tokenId) { const result = await client .select({ key: true, payload: true }) .where(eq('token', tokenId)) .ownedBy(USER_ADDRESS) .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, you run the frontend and see the data in action.