Skip to content

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.

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
frontend/index.html
<!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>

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
frontend/style.css
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; } }

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
frontend/charts.js
// Import Chart.js and plugins from CDN
import {Chart, registerables} from 'https://cdn.jsdelivr.net/npm/chart.js@4/+esm';
// Chart instances
let marketCapChart, btcPriceChart, ethPriceChart, glmPriceChart;
// Helper function to format market cap values
function 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 instances
export 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 token
marketCapChart = 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 charts
btcPriceChart = 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 data
export 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 values
export 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 changes
export 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 needed
export function getCharts() {
return { marketCapChart, btcPriceChart, ethPriceChart, glmPriceChart };
}

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
frontend/script.js
// TODO: Replace with your wallet address from the backend
const 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 access
import { 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 needed
const client = createPublicClient({
chain: tiramisu,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);

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.

frontend/script.js
// TODO: Replace with your wallet address from the backend
const 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 access
import { 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 needed
const client = createPublicClient({
chain: tiramisu,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchain
async 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));
}

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
frontend/script.js
// TODO: Replace with your wallet address from the backend
const 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 access
import { 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 functions
import {
initializeCharts,
updateChart,
updateMarketCapChart,
updatePriceBoxes,
getCharts
} from './charts.js';
// Public client can only read data, no private key needed
const client = createPublicClient({
chain: tiramisu,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchain
async 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 elements
async 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 load
window.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.