mirror of
https://github.com/aljazceru/satshkd-vercel.git
synced 2025-12-17 05:04:24 +01:00
- Replaced MetricsGraphics library with Chart.js v4 for faster chart rendering - Removed dependencies: MetricsGraphics, D3.js v4, and jQuery - Added Chart.js v4 with date-fns adapter and annotation plugin - Maintained all original chart features: - Logarithmic Y-axis scale - Time-series area chart with EUR/sats data - Historical event markers (QE1, QE2, QE3, QH1, QH2, QE4, QH3) - Custom pattern fill with 100 EUR image - Responsive height calculation - Y-axis units formatting - Improved chart performance with modern Canvas-based rendering - Replaced D3 JSON loading with native fetch API
358 lines
18 KiB
Handlebars
358 lines
18 KiB
Handlebars
<head>
|
|
<title>EURSAT</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
<!-- CSS only -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
|
|
|
|
<link rel="stylesheet" type="text/css" href="/static/skeleton.css">
|
|
<link rel="stylesheet" type="text/css" href="/static/normalize.css">
|
|
<link rel="shortcut icon" type="image/png" href="/static/favicon.png" />
|
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Raleway&display=swap" rel="stylesheet">
|
|
<script src="/static/chart.min.js"></script>
|
|
<script src="/static/chartjs-adapter-date-fns.min.js"></script>
|
|
<script src="/static/chartjs-plugin-annotation.min.js"></script>
|
|
<script async src="https://analytics.umami.is/script.js" data-website-id="d35ba036-3531-422c-be04-74711c97799c"></script>
|
|
<!-- JavaScript Bundle with Popper -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8" crossorigin="anonymous"></script>
|
|
<style>
|
|
#historical {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<header class="p-1 bg-dark text-white">
|
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="http://bitcoin.org.hk">
|
|
<img src="/static/bahk-logo-big-white.svg" alt="Bitcoin HK Logo" width="60">
|
|
</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="true" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarText">
|
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
<li class="nav-item">
|
|
<a class="nav-link active px-2 text-secondary" aria-current="page" href="http://sats.bitcoin.org.hk">Sats</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link px-2 text-white" href="http://blocks.bitcoin.org.hk">Blocks</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link px-2 text-white" href="https://rates.bitcoin.org.hk">Rates</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link px-2 text-white" href="http://stack.bitcoin.org.hk/">Stack</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link px-2 text-white" href="https://laisee.org">Laisee</a>
|
|
</li>
|
|
</ul>
|
|
<span class="navbar-item" style="color: #fff">
|
|
<a class="px-2" href="{{ lang1_link }}" style="color: rgb(6, 168, 231)"> {{ lang1 }} </a>{{#if lang2}} | <a class="px-2" href="{{ lang2_link }}" style="color: rgb(6, 168, 231)"> {{ lang2 }}</a>{{/if}}{{#if lang3}} | <a class="px-2" href="{{ lang3_link }}" style="color: rgb(6, 168, 231)"> {{ lang3 }}</a>{{/if}}{{#if lang4}} | <a class="px-2" href="{{ lang4_link }}" style="color: rgb(6, 168, 231)"> {{ lang4 }}</a>{{/if}}{{#if lang5}} | <a class="px-2" href="{{ lang5_link }}" style="color: rgb(6, 168, 231)"> {{ lang5 }}</a>{{/if}}{{#if lang6}} | <a class="px-2" href="{{ lang6_link }}" style="color: rgb(6, 168, 231)"> {{ lang6 }}</a>{{/if}}{{#if lang7}} | <a class="px-2" href="{{ lang7_link }}" style="color: rgb(6, 168, 231)"> {{ lang7 }}</a>{{/if}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<div class="container" style="margin-top: 40px;">
|
|
<div class="row">
|
|
<center>
|
|
<h3> {{ Title }} <span id="current"></span> sats</h3>
|
|
</center>
|
|
<script>
|
|
var currentPrice = 0;
|
|
var ws_ticker_id, t;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
startWebSocket();
|
|
}, false);
|
|
|
|
function startWebSocket() {
|
|
t = new WebSocket("wss://api-pub.bitfinex.com/ws/2");
|
|
|
|
t.onmessage = function(e) {
|
|
var i = JSON.parse(e.data);
|
|
if (i.event === 'subscribed' && i.channel == 'ticker') {
|
|
ws_ticker_id = i.chanId;
|
|
}
|
|
if (ws_ticker_id == i[0]) {
|
|
updatePrice(i);
|
|
}
|
|
}
|
|
|
|
t.onopen = function(e) {
|
|
console.log('Web socket open.');
|
|
t.send(JSON.stringify({
|
|
"event": "subscribe",
|
|
"channel": "ticker",
|
|
"symbol": "BTCUSD"
|
|
}));
|
|
setPingTimer();
|
|
}
|
|
|
|
t.onclose = function(e) {
|
|
console.log('Web socket closed. Attempting to reopen.');
|
|
setTimeout(function() {
|
|
startWebSocket();
|
|
}, 2000);
|
|
}
|
|
|
|
t.onerror = function(e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
function setPingTimer() {
|
|
setInterval(function() {
|
|
t.send("ping");
|
|
}, 10000);
|
|
}
|
|
|
|
function updatePrice(i) {
|
|
if (Array.isArray(i)) {
|
|
i.forEach(function(item) {
|
|
if (Array.isArray(item)) {
|
|
var btc_price = item[0];
|
|
currentPrice = Math.round((1 / btc_price) * 100000000 / {{ exchange_rate }}); // satoshis per currency unit
|
|
document.title = currentPrice.toLocaleString() + " sats";
|
|
document.querySelector('#current').textContent = currentPrice.toLocaleString();
|
|
|
|
}
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div id='historical'>
|
|
<canvas id="chart"></canvas>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<table class="u-full-width">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align:-webkit-left;font-size:2rem;">{{ date }} </th>
|
|
<th style="text-align:end;font-size:2rem;">{{ price }} </th>
|
|
<th style="text-align:end;font-size:2rem;white-space: pre;"> {{ percentchange }} </th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{#each yeardata}}
|
|
<tr>
|
|
<td style="text-align:-webkit-left">{{ this.year }}</td>
|
|
<td style="text-align:end">{{ this.sats }} sats </td>
|
|
<td style="text-align:end">{{ this.percent }} % </td>
|
|
</tr>
|
|
{{/each}}
|
|
</tbody>
|
|
</table>
|
|
|
|
|
|
<p style="font-size: small;">
|
|
* {{ footnote }}
|
|
</p>
|
|
</div>
|
|
<script>
|
|
// Load data and initialize chart
|
|
fetch('/{{ data_file }}')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Convert date strings to Date objects
|
|
data = data.map(d => ({
|
|
...d,
|
|
date: new Date(d.date)
|
|
}));
|
|
|
|
// Calculate responsive height
|
|
const windowWidth = window.innerWidth;
|
|
let graphHeight;
|
|
if (windowWidth < 550) {
|
|
graphHeight = windowWidth / 1.9047;
|
|
} else if (windowWidth < 1200) {
|
|
graphHeight = windowWidth * 0.8 / 1.9047;
|
|
} else {
|
|
graphHeight = 500;
|
|
}
|
|
console.log("graph height: ", graphHeight);
|
|
|
|
// Event markers configuration
|
|
const qhLink = () => window.open('https://en.bitcoin.it/wiki/Controlled_supply', '_blank');
|
|
const qeLink = () => window.open('https://en.wikipedia.org/wiki/Quantitative_easing', '_blank');
|
|
|
|
const markers = [{
|
|
date: new Date('2008-11-25T00:00:00.000Z'),
|
|
label: 'QE1',
|
|
click: qeLink,
|
|
}, {
|
|
date: new Date('2010-11-03T00:00:00.000Z'),
|
|
label: 'QE2',
|
|
click: qeLink,
|
|
}, {
|
|
date: new Date('2012-09-13T00:00:00.000Z'),
|
|
label: 'QE3',
|
|
click: qeLink,
|
|
}, {
|
|
date: new Date('2012-11-28T00:00:00.000Z'),
|
|
label: 'QH1',
|
|
click: qhLink,
|
|
}, {
|
|
date: new Date('2016-07-09T00:00:00.000Z'),
|
|
label: 'QH2',
|
|
click: qhLink,
|
|
}, {
|
|
date: new Date('2019-10-11T00:00:00.000Z'),
|
|
label: 'QE4',
|
|
click: qeLink,
|
|
}, {
|
|
date: new Date('2020-05-11T00:00:00.000Z'),
|
|
label: 'QH3',
|
|
click: qhLink,
|
|
}];
|
|
|
|
// Create pattern fill
|
|
const canvas = document.getElementById('chart');
|
|
canvas.height = graphHeight;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Load the 100 EUR image for pattern fill
|
|
const patternImage = new Image();
|
|
patternImage.src = '/static/assets/100eur.jpg';
|
|
|
|
patternImage.onload = function() {
|
|
const pattern = ctx.createPattern(patternImage, 'repeat');
|
|
|
|
// Initialize Chart.js
|
|
const chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map(d => d.date),
|
|
datasets: [{
|
|
label: '{{ subtitle }}',
|
|
data: data.map(d => d['{{ rate_field }}']),
|
|
borderColor: 'green',
|
|
backgroundColor: pattern,
|
|
fill: true,
|
|
tension: 0.1,
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '{{ subtitle }}',
|
|
font: {
|
|
size: 16
|
|
}
|
|
},
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.parsed.y.toLocaleString() + ' sats';
|
|
}
|
|
}
|
|
},
|
|
annotation: {
|
|
annotations: markers.reduce((acc, marker, idx) => {
|
|
acc['line' + idx] = {
|
|
type: 'line',
|
|
xMin: marker.date,
|
|
xMax: marker.date,
|
|
borderColor: 'rgba(255, 99, 132, 0.5)',
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
label: {
|
|
display: true,
|
|
content: marker.label,
|
|
position: 'start',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.8)',
|
|
color: 'white',
|
|
font: {
|
|
size: 10,
|
|
weight: 'bold'
|
|
},
|
|
padding: 4
|
|
},
|
|
click: function() {
|
|
marker.click();
|
|
}
|
|
};
|
|
return acc;
|
|
}, {})
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: {
|
|
unit: 'year',
|
|
displayFormats: {
|
|
year: 'yyyy'
|
|
}
|
|
},
|
|
ticks: {
|
|
maxTicksLimit: 16
|
|
},
|
|
grid: {
|
|
display: true
|
|
}
|
|
},
|
|
y: {
|
|
type: 'logarithmic',
|
|
max: 1000000000,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return value.toLocaleString() + ' sats';
|
|
},
|
|
maxTicksLimit: 12
|
|
},
|
|
grid: {
|
|
display: true
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
mode: 'nearest',
|
|
axis: 'x',
|
|
intersect: false
|
|
},
|
|
onClick: function(event, elements, chart) {
|
|
// Handle marker clicks
|
|
const canvasPosition = Chart.helpers.getRelativePosition(event, chart);
|
|
const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);
|
|
|
|
// Check if click is near any marker
|
|
markers.forEach(marker => {
|
|
const markerX = marker.date.getTime();
|
|
const tolerance = 365 * 24 * 60 * 60 * 1000; // 1 year tolerance
|
|
|
|
if (Math.abs(dataX - markerX) < tolerance) {
|
|
marker.click();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
};
|
|
})
|
|
.catch(error => console.error('Error loading chart data:', error));
|
|
</script>
|
|
|
|
</body>
|