Replace MetricsGraphics with Chart.js v4 for improved performance

- 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
This commit is contained in:
Claude
2025-11-10 04:46:41 +00:00
parent 5d4f0195c2
commit 76eb9b39a3
8 changed files with 224 additions and 8427 deletions

20
public/static/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,548 +0,0 @@
.mg-active-datapoint {
fill: black;
font-size: 0.9rem;
font-weight: 400;
opacity: 0.8;
}
.mg-area-color {
fill: #000;
}
.mg-area1-color {
fill: #0000ff;
}
.mg-area2-color {
fill: #05b378;
}
.mg-area3-color {
fill: #db4437;
}
.mg-area4-color {
fill: #f8b128;
}
.mg-area5-color {
fill: #5c5c5c;
}
.mg-area6-color {
fill: steelblue;
}
.mg-area7-color {
fill: #f673bf;
}
.mg-area8-color {
fill: #0b73b0;
}
.mg-area9-color {
fill: #006400;
}
.mg-area10-color {
fill: #92514f;
}
text.mg-barplot-group-label {
font-weight:900;
}
.mg-barplot rect.mg-bar {
shape-rendering: auto;
}
.mg-barplot rect.mg-bar.default-bar {
fill: #b6b6fc;
}
.mg-barplot rect.mg-bar.default-active {
fill: #9e9efc;
}
.mg-barplot .mg-bar-prediction {
fill: #5b5b5b;
}
.mg-barplot .mg-bar-baseline {
stroke: #5b5b5b;
stroke-width: 2;
}
.mg-bar-target-element {
font-size:11px;
padding-left:5px;
padding-right:5px;
font-weight:300;
}
.mg-baselines line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-baselines text {
fill: black;
font-size: 0.9rem;
opacity: 0.6;
stroke: none;
}
.mg-baselines-small text {
font-size: 0.6rem;
}
.mg-category-guides line {
stroke: #b3b2b2;
}
.mg-header {
cursor: default;
font-size: 1.2rem;
}
.mg-header .mg-chart-description {
fill: #ccc;
font-family: FontAwesome;
font-size: 1.2rem;
}
.mg-header .mg-warning {
fill: #ccc;
font-family: FontAwesome;
font-size: 1.2rem;
}
.mg-points circle {
opacity: 0.65;
}
.mg-popover {
font-size: 0.95rem;
}
.mg-popover-content {
cursor: auto;
line-height: 17px;
}
.mg-data-table {
margin-top: 30px;
}
.mg-data-table thead tr th {
border-bottom: 1px solid darkgray;
cursor: default;
font-size: 1.1rem;
font-weight: normal;
padding: 5px 5px 8px 5px;
text-align: right;
}
.mg-data-table thead tr th .fa {
color: #ccc;
padding-left: 4px;
}
.mg-data-table thead tr th .popover {
font-size: 1rem;
font-weight: normal;
}
.mg-data-table .secondary-title {
color: darkgray;
}
.mg-data-table tbody tr td {
margin: 2px;
padding: 5px;
vertical-align: top;
}
.mg-data-table tbody tr td.table-text {
opacity: 0.8;
padding-left: 30px;
}
.mg-y-axis line.mg-extended-yax-ticks {
opacity: 0.4;
}
.mg-x-axis line.mg-extended-xax-ticks {
opacity: 0.4;
}
.mg-histogram .axis path,
.mg-histogram .axis line {
fill: none;
opacity: 0.7;
shape-rendering: auto;
stroke: #ccc;
}
tspan.hist-symbol {
fill: #9e9efc;
}
.mg-histogram .mg-bar rect {
fill: #b6b6fc;
shape-rendering: auto;
}
.mg-histogram .mg-bar rect.active {
fill: #9e9efc;
}
.mg-least-squares-line {
stroke: red;
stroke-width: 1px;
}
.mg-lowess-line {
fill: none;
stroke: red;
}
.mg-rollover-rect * {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mg-line-color {
stroke: #000;
}
.mg-hover-line-color {
fill: #000;
}
.mg-line1-color {
stroke: #4040e8;
}
.mg-hover-line1-color {
fill: #4040e8;
}
.mg-line2-color {
stroke: #05b378;
}
.mg-hover-line2-color {
fill: #05b378;
}
.mg-line3-color {
stroke: #db4437;
}
.mg-hover-line3-color {
fill: #db4437;
}
.mg-line4-color {
stroke: #f8b128;
}
.mg-hover-line4-color {
fill: #f8b128;
}
.mg-line5-color {
stroke: #5c5c5c;
}
.mg-hover-line5-color {
fill: #5c5c5c;
}
.mg-line6-color {
stroke: steelblue;
}
.mg-hover-line6-color {
fill: steelblue;
}
.mg-line7-color {
stroke: #f673bf;
}
.mg-hover-line7-color {
fill: #f673bf;
}
.mg-line8-color {
stroke: #0b73b0;
}
.mg-hover-line8-color {
fill: #0b73b0;
}
.mg-line9-color {
stroke: #006400;
}
.mg-hover-line9-color {
fill: #006400;
}
.mg-line10-color {
stroke: #92514f;
}
.mg-hover-line10-color {
fill: #92514f ;
}
.mg-line-legend text {
font-size: 0.9rem;
font-weight: 300;
stroke: none;
}
.mg-line-legend-color {
color: #000;
fill: #000;
}
.mg-line1-legend-color {
color: #4040e8;
fill: #4040e8;
}
.mg-line2-legend-color {
color: #05b378;
fill: #05b378;
}
.mg-line3-legend-color {
color: #db4437;
fill: #db4437;
}
.mg-line4-legend-color {
color: #f8b128;
fill: #f8b128;
}
.mg-line5-legend-color {
color: #5c5c5c;
fill: #5c5c5c;
}
.mg-line6-legend-color {
color: steelblue;
fill: steelblue;
}
.mg-line7-legend-color {
color: #f673bf;
fill: #f673bf;
}
.mg-line8-legend-color {
color: #0b73b0;
fill: #0b73b0;
}
.mg-line9-legend-color {
color: #006400;
fill: #006400;
}
.mg-line10-legend-color {
color: #92514f;
fill: #92514f;
}
.mg-main-area-solid svg .mg-main-area {
fill: #ccccff;
opacity: 1;
}
.mg-markers line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-markers text {
fill: black;
font-size: 0.8rem;
opacity: 0.6;
}
.mg-missing-text {
opacity: 0.9;
}
.mg-missing-background {
stroke: blue;
fill: none;
stroke-dasharray: 10,5;
stroke-opacity: 0.05;
stroke-width: 2;
}
.mg-missing .mg-main-line {
opacity: 0.1;
}
.mg-missing .mg-main-area {
opacity: 0.03;
}
path.mg-main-area {
opacity: 0.2;
stroke: none;
}
path.mg-confidence-band {
fill: #ccc;
opacity: 0.4;
stroke: none;
}
path.mg-main-line {
fill: none;
opacity: 0.8;
stroke-width: 1.1px;
}
.mg-points circle {
fill-opacity: 0.4;
stroke-opacity: 1;
}
circle.mg-points-mono {
fill: #0000ff;
stroke: #0000ff;
}
tspan.mg-points-mono {
fill: #0000ff;
stroke: #0000ff;
}
/* a selected point in a scatterplot */
.mg-points circle.selected {
fill-opacity: 1;
}
.mg-highlight circle {
fill-opacity: 0;
stroke-width: 4px;
stroke-opacity: 0.3;
}
.mg-voronoi path {
fill: none;
pointer-events: all;
stroke: none;
stroke-opacity: 0.1;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mg-x-rug-mono,
.mg-y-rug-mono {
stroke: black;
}
.mg-x-axis line,
.mg-y-axis line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-x-axis text,
.mg-y-axis text,
.mg-histogram .axis text {
fill: black;
font-size: 0.9rem;
opacity: 0.6;
}
.mg-x-axis .label,
.mg-y-axis .label,
.mg-axis .label {
font-size: 0.8rem;
text-transform: uppercase;
font-weight: 400;
}
.mg-x-axis-small text,
.mg-y-axis-small text,
.mg-active-datapoint-small {
font-size: 0.6rem;
}
.mg-x-axis-small .label,
.mg-y-axis-small .label {
font-size: 0.65rem;
}
.mg-european-hours {
}
.mg-year-marker text {
fill: black;
font-size: 0.7rem;
opacity: 0.6;
}
.mg-year-marker line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-year-marker-small text {
font-size: 0.6rem;
}
.mg-brush-container {
cursor: crosshair;
}
.mg-brush-container .mg-brushing {
cursor: ew-resize;
}
.mg-brushed, .mg-brushed * {
cursor: zoom-out;
}
.mg-brush rect.mg-extent {
fill: rgba(0, 0, 0, 0.3);
}
.mg-brushing-in-progress {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,23 +7,18 @@
<link rel="stylesheet" type="text/css" href="/static/skeleton.css"> <link rel="stylesheet" type="text/css" href="/static/skeleton.css">
<link rel="stylesheet" type="text/css" href="/static/normalize.css"> <link rel="stylesheet" type="text/css" href="/static/normalize.css">
<link rel="stylesheet" type="text/css" href="/static/metricsgraphics.css">
<link rel="shortcut icon" type="image/png" href="/static/favicon.png" /> <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"> <link href="https://fonts.googleapis.com/css?family=Open+Sans|Raleway&display=swap" rel="stylesheet">
<script src="/static/jquery.min.js"></script> <script src="/static/chart.min.js"></script>
<script src="/static/d3.v4.min.js"></script> <script src="/static/chartjs-adapter-date-fns.min.js"></script>
<script src="/static/metricsgraphics.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> <script async src="https://analytics.umami.is/script.js" data-website-id="d35ba036-3531-422c-be04-74711c97799c"></script>
<!-- JavaScript Bundle with Popper --> <!-- 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> <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> <style>
#historical svg { #historical {
overflow: visible !important; position: relative;
} width: 100%;
@media screen and (min-width: 393px) and (max-width: 393px) {
.mg-y-axis text {
transform: translateX(5px);
}
} }
</style> </style>
</head> </head>
@@ -135,6 +130,7 @@
</script> </script>
<div id='historical'> <div id='historical'>
<canvas id="chart"></canvas>
</div> </div>
<div class="container"> <div class="container">
@@ -164,103 +160,198 @@
</p> </p>
</div> </div>
<script> <script>
d3.json('/{{ data_file }}', function(data) { // Load data and initialize chart
data = MG.convert.date(data, 'date'); fetch('/{{ data_file }}')
.then(response => response.json())
var windowWidth = $(window).width(); .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) { if (windowWidth < 550) {
var graphHeight = windowWidth / 1.9047; graphHeight = windowWidth / 1.9047;
} else if (windowWidth < 1200) { } else if (windowWidth < 1200) {
var graphHeight = windowWidth * 0.8 / 1.9047; graphHeight = windowWidth * 0.8 / 1.9047;
} else { } else {
var graphHeight = 500; graphHeight = 500;
} }
console.log("graph height: " , graphHeight) console.log("graph height: ", graphHeight);
var qhLink = function() { // Event markers configuration
window.open('https://en.bitcoin.it/wiki/Controlled_supply', '_blank'); const qhLink = () => window.open('https://en.bitcoin.it/wiki/Controlled_supply', '_blank');
}; const qeLink = () => window.open('https://en.wikipedia.org/wiki/Quantitative_easing', '_blank');
var qeLink = function() { const markers = [{
window.open('https://en.wikipedia.org/wiki/Quantitative_easing', '_blank'); date: new Date('2008-11-25T00:00:00.000Z'),
}; label: 'QE1',
click: qeLink,
var markers = [{
'date': new Date('2008-11-25T00:00:00.000Z'),
'label': 'QE1',
'click': qeLink,
}, { }, {
'date': new Date('2010-11-03T00:00:00.000Z'), date: new Date('2010-11-03T00:00:00.000Z'),
'label': 'QE2', label: 'QE2',
'click': qeLink, click: qeLink,
}, { }, {
'date': new Date('2012-09-13T00:00:00.000Z'), date: new Date('2012-09-13T00:00:00.000Z'),
'label': 'QE3', label: 'QE3',
'click': qeLink, click: qeLink,
}, { }, {
'date': new Date('2012-11-28T00:00:00.000Z'), date: new Date('2012-11-28T00:00:00.000Z'),
'label': 'QH1', label: 'QH1',
'click': qhLink, click: qhLink,
}, { }, {
'date': new Date('2016-07-09T00:00:00.000Z'), date: new Date('2016-07-09T00:00:00.000Z'),
'label': 'QH2', label: 'QH2',
'click': qhLink, click: qhLink,
}, { }, {
'date': new Date('2019-10-11T00:00:00.000Z'), date: new Date('2019-10-11T00:00:00.000Z'),
'label': 'QE4', label: 'QE4',
'click': qeLink, click: qeLink,
}, { }, {
'date': new Date('2020-05-11T00:00:00.000Z'), date: new Date('2020-05-11T00:00:00.000Z'),
'label': 'QH3', label: 'QH3',
'click': qhLink, click: qhLink,
}]; }];
MG.data_graphic({ // Create pattern fill
title: '{{ subtitle }}', const canvas = document.getElementById('chart');
data: data, canvas.height = graphHeight;
full_width: true, const ctx = canvas.getContext('2d');
height: graphHeight,
left: 0, // Load the 100 EUR image for pattern fill
area: true, const patternImage = new Image();
color: '#FF9900', patternImage.src = '/static/assets/100eur.jpg';
markers: markers,
target: document.getElementById('historical'), patternImage.onload = function() {
xax_count: 16, const pattern = ctx.createPattern(patternImage, 'repeat');
yax_count: 12,
x_accessor: 'date', // Initialize Chart.js
y_accessor: '{{ rate_field }}', const chart = new Chart(ctx, {
y_scale_type: 'log', type: 'line',
y_extended_ticks: true, data: {
yax_units: ' sats', labels: data.map(d => d.date),
yax_units_append: true, datasets: [{
max_y: 1000000000, 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();
}
}); });
}
}
$(document).ready(function() {
d3.select('svg')
.data(data)
.append('defs')
.append('pattern')
.attr('id', 'losermoney')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', '100%')
.attr('height', '100%')
.append('image')
.attr('width', '100%')
.attr('height', '100%')
.attr('xlink:href', '/static/assets/100eur.jpg');
$('svg path')
.css('opacity', 1)
.css('stroke', 'green')
.attr('fill', 'url(#losermoney)');
});
}); });
};
})
.catch(error => console.error('Error loading chart data:', error));
</script> </script>
</body> </body>