Skip to content

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.

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
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 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
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); }
.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; } }

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
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 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 needed
export function getCharts() {
return { marketCapChart, btcPriceChart, ethPriceChart, glmPriceChart };
}

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
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, 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 needed
const client = createPublicClient({
chain: kaolin,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);

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.

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, 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 needed
const client = createPublicClient({
chain: kaolin,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchain
async 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));
}

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
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, 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 functions
import {
initializeCharts,
updateChart,
updateMarketCapChart,
updatePriceBoxes,
getCharts
} from './charts.js';
// Public client can only read data, no private key needed
const client = createPublicClient({
chain: kaolin,
transport: http(),
});
console.log('Arkiv client initialized for address:', USER_ADDRESS);
// Fetch token data from Arkiv blockchain
async 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 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, we’ll run the frontend and see the data in action!