Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
bertani authored Sep 12, 2024
1 parent 010148f commit 6d14f51
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 0 deletions.
174 changes: 174 additions & 0 deletions chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Description: This file contains the code to create the chord chart.

// Read the data from the json api, and create the chord chart
url = "https://hashi-explorer.xyz/iframe/api/query"

fetch(url).then(response => response.json()).then(data => {
loadChordChart(data);
});

function beautifyTimeDiff(msDiff) {
const seconds = Math.floor(msDiff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const weeks = Math.floor(days / 7);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);

if (seconds < 60) return `~ ${seconds} second${seconds !== 1 ? 's' : ''} ago`;
if (minutes < 60) return `~ ${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
if (hours < 24) return `~ ${hours} hour${hours !== 1 ? 's' : ''} ago`;
if (days < 7) return `~ ${days} day${days !== 1 ? 's' : ''} ago`;
if (weeks < 5) return `~ ${weeks} week${weeks !== 1 ? 's' : ''} ago`;
if (months < 12) return `~ ${months} month${months !== 1 ? 's' : ''} ago`;
return `~ ${years} year${years !== 1 ? 's' : ''} ago`;
}

function beatifyChainName(chain){
if (chain == "bnb") return "BNB Chain";
if (chain == "gnosis") return "Gnosis Chain";
if (chain == "polygon") return "Polygon";
if (chain == "base") return "Base";
if (chain == "optimism") return "Optimism";
if (chain == "arbitrum") return "Arbitrum";
return chain;
}




function loadChordChart(odata){
const THRESHOLD = 1000 * 60 * 60 * 24 * 7 * 6; // 6 weeks
// convert odata to data
data = [];
for (var i = 0; i < odata.length; i++) {
value = 1; //odata[i].value;
age = Math.floor((new Date() - new Date(odata[i].last_agreed_block_time)));
if (age > THRESHOLD) continue;
data.push({source: beatifyChainName(odata[i].source_chain), target: beatifyChainName(odata[i].target_chain), value: value, age: age});
}

//data.push({source: "BNB Chain", target: "Gnosis Chain", value: 1, age: 1000*3600});
//data.push({target: "BNB Chain", source: "Gnosis Chain", value: 1, age: 0});

function getAge(source, destination){
for (var i = 0; i < data.length; i++) {
if (data[i].source == source && data[i].target == destination){
return beautifyTimeDiff(data[i].age);
}
}
return -1;
}

function opacityFor(source, destination){
for (var i = 0; i < data.length; i++) {
if (data[i].source == source && data[i].target == destination){
age = data[i].age;
if (age < 1000 * 60 * 15) return 0.75; // up to 15 minutes
if (age < 1000 * 60 * 60) return 0.65; // up to 1 hour
if (age < 1000 * 60 * 60 * 6) return 0.55; // up to 6 hours
if (age < 1000 * 60 * 60 * 24) return 0.5; // 6-24 hours
if (age < 1000 * 60 * 60 * 24 * 3) return 0.4; // 1-3 days
if (age < 1000 * 60 * 60 * 24 * 7) return 0.3; // 3-7 days
if (age < 1000 * 60 * 60 * 24 * 7 * 4) return 0.2; // 1-4 weeks
return 0.1;
}
}
return 0;
}

const width = 1080; // Base width for calculations
const height = width; // Maintain square aspect ratio
const innerRadius = Math.min(width, height) * 0.5 - 20;
const outerRadius = innerRadius + 6;

const names = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
const index = new Map(names.map((name, i) => [name, i]));
const matrix = Array.from(index, () => new Array(names.length).fill(0));

for (const { source, target, value } of data) {
matrix[index.get(source)][index.get(target)] += value;
}

const chord = d3.chordDirected()
.padAngle(12 / innerRadius)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);

const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);

const ribbon = d3.ribbonArrow()
.radius(innerRadius - 0.5)
.padAngle(0.01) // Very small padding, arrows almost touch
.headRadius(innerRadius * 0.1); // Moderately pointed arrowhead for a balanced look

const colors = d3.schemeCategory10;

const formatValue = x => `${x.toFixed(0)}B`;

const svg = d3.select("#chart").append("svg")
.attr("viewBox", [-width / 2 - 200, -height / 2 - 200, width + 400, height + 400].join(' ')) // Added extra padding
.attr("preserveAspectRatio", "xMidYMid meet") // Keeps aspect ratio when resizing
//.attr("style", "font: 10px sans-serif;");

// Create the tooltip reference
const tooltip = d3.select("#tooltip");

const chords = chord(matrix);

// Draw ribbons (the arcs that connect groups)
svg.append("g")
.selectAll("path")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill-opacity", function (d){ return opacityFor(names[d.source.index], names[d.target.index]) })
.attr("fill", function(d){ return colors[d.source.index] })
.style("mix-blend-mode", "multiply")
.on("mouseover", (event, d) => {
tooltip.style("opacity", 1) // Show tooltip
.html(`The last <b>${names[d.source.index]}</b> block header was propagated on <b>${names[d.target.index]}</b> `+getAge(names[d.source.index], names[d.target.index])) // Update tooltip content
.style("left", (event.pageX + 10) + "px") // Position tooltip near mouse
.style("top", (event.pageY - 10) + "px");

d3.select(event.target)
.attr("fill-opacity", 1); // Highlight the selected arrow by changing its opacity
})
.on("mousemove", (event) => {
tooltip.style("left", (event.pageX + 10) + "px") // Move tooltip with the mouse
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", (event, d) => {
tooltip.style("opacity", 0); // Hide tooltip

d3.select(event.target)
.attr("fill-opacity", opacityFor(names[d.source.index], names[d.target.index])); // Reset the opacity of the arrow
})
//.append("title")
//.text(d => `${names[d.source.index]} owes ${names[d.target.index]} ${formatValue(d.source.value)}`);

// Draw groups (the outer arc for each group)
const group = svg.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");

group.append("path")
.attr("d", arc)
.attr("fill", function(d){ return colors[d.index] })
.attr("stroke", "#fff");

group.append("text")
.attr("dy", -3)
.attr("transform", d => {
const angle = (d.startAngle + d.endAngle) / 2;
const rotate = angle * 180 / Math.PI - 90;
return `rotate(${rotate}) translate(${outerRadius + 10}) ${angle > Math.PI ? "rotate(180)" : ""}`;
})
.attr("text-anchor", d => (d.startAngle + d.endAngle) / 2 > Math.PI ? "end" : null)
.text(d => names[d.index]);

}
45 changes: 45 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#chart {
width:100%;
margin-top: auto;
max-width: 1080px;
height: auto;
}
svg {
width: 100%;
height: auto; /* Ensure aspect ratio is maintained */
}
text {
font-size: 35px; /* Adjust this value to make labels larger */
}
/* Tooltip styling */
.tooltip {
position: absolute;
background-color: white;
border: 1px solid #ddd;
padding: 5px;
font-size: 20px;
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
opacity: 0; /* Initially hidden */
transition: opacity 0.2s ease;
}
</style>
</head>
<body>
<div id="chart"></div>
<div id="tooltip" class="tooltip"></div> <!-- Tooltip div -->
<script src="chart.js"></script>
</body>
</html>

0 comments on commit 6d14f51

Please sign in to comment.