OSM2IPFS/earth/g1gate/index_fichiers/app.js

806 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
export const DU = 10.68;
export const MAX_NB_TX = 200;
let txLimit = MAX_NB_TX;
let minTime = null;
export const CESIUM_G1_NODES = [
"https://g1.data.brussels.ovh",
"https://g1.data.e-is.pro",
"https://g1.data.cuates.net",
"https://g1.data.madeirawonders.com",
// "https://g1.data.pini.fr" // Could not resolve hostname
// "https://g1.data.le-sou.org" // vide
// "https://abyayala.g1labs.net" // /g1/movement returns "404 Not Found"
// "https://g1.data.presles.fr", // CORS
// "https://cesiumplus971.dns1.us", // CORS
// "https://g1.data.mithril.re", // /g1/movement returns "403 Forbidden"
];
let chartColors = [
'#FFC431',
'#548AFF',
'#FF826E',
'#67B0C3',
'#CFD8DC',
'#5CD6B3'
];
export const Treemap = (
// Copyright 2021-2023 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/treemap
function Treemap(data, { // data is either tabular (array of objects) or hierarchy (nested objects)
path, // as an alternative to id and parentId, returns an array identifier, imputing internal nodes
id = Array.isArray(data) ? d => d.id : null, // if tabular data, given a d in data, returns a unique identifier (string)
parentId = Array.isArray(data) ? d => d.parentId : null, // if tabular data, given a node d, returns its parents identifier
children, // if hierarchical data, given a d in data, returns its children
value, // given a node d, returns a quantitative value (for area encoding; null for count)
sort = (a, b) => d3.descending(a.value, b.value), // how to sort nodes prior to layout
label, // given a leaf node d, returns the name to display on the rectangle
group, // given a leaf node d, returns a categorical value (for color encoding)
title, // given a leaf node d, returns its hover text
link, // given a leaf node d, its link (if any)
linkTarget = "_blank", // the target attribute for links (if any)
tile = d3.treemapBinary, // treemap strategy
width = 480, // outer width, in pixels
height = 360, // outer height, in pixels
margin = 0, // shorthand for margins
marginTop = margin, // top margin, in pixels
marginRight = margin, // right margin, in pixels
marginBottom = margin, // bottom margin, in pixels
marginLeft = margin, // left margin, in pixels
padding = 1, // shorthand for inner and outer padding
paddingInner = padding, // to separate a node from its adjacent siblings
paddingOuter = padding, // shorthand for top, right, bottom, and left padding
paddingTop = paddingOuter, // to separate a nodes top edge from its children
paddingRight = paddingOuter, // to separate a nodes right edge from its children
paddingBottom = paddingOuter, // to separate a nodes bottom edge from its children
paddingLeft = paddingOuter, // to separate a nodes left edge from its children
round = true, // whether to round to exact pixels
colors = d3.schemeTableau10, // array of colors
zDomain, // array of values for the color scale
fill = "#ccc", // fill for node rects (if no group color encoding)
fillOpacity = group == null ? null : 0.6, // fill opacity for node rects
stroke, // stroke for node rects
strokeWidth, // stroke width for node rects
strokeOpacity, // stroke opacity for node rects
strokeLinejoin, // stroke line join for node rects
} = {}) {
// If id and parentId options are specified, or the path option, use d3.stratify
// to convert tabular data to a hierarchy; otherwise we assume that the data is
// specified as an object {children} with nested objects (a.k.a. the “flare.json”
// format), and use d3.hierarchy.
// We take special care of any node that has both a value and children, see
// https://observablehq.com/@d3/treemap-parent-with-value.
const stratify = data => (d3.stratify().path(path)(data)).each(node => {
if (node.children?.length && node.data != null) {
const child = new d3.Node(node.data);
node.data = null;
child.depth = node.depth + 1;
child.height = 0;
child.parent = node;
child.id = node.id + "/";
node.children.unshift(child);
}
});
const root = path != null ? stratify(data)
: id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children);
// Compute the values of internal nodes by aggregating from the leaves.
value == null ? root.count() : root.sum(d => Math.max(0, d ? value(d) : null));
// Prior to sorting, if a group channel is specified, construct an ordinal color scale.
const leaves = root.leaves();
const G = group == null ? null : leaves.map(d => group(d.data, d));
if (zDomain === undefined) zDomain = G;
zDomain = new d3.InternSet(zDomain);
const color = group == null ? null : d3.scaleOrdinal(zDomain, colors);
// Compute labels and titles.
const L = label == null ? null : leaves.map(d => label(d.data, d));
const T = title === undefined ? L : title == null ? null : leaves.map(d => title(d.data, d));
// Sort the leaves (typically by descending value for a pleasing layout).
if (sort != null) root.sort(sort);
// Compute the treemap layout.
d3.treemap()
.tile(tile)
.size([width - marginLeft - marginRight, height - marginTop - marginBottom])
.paddingInner(paddingInner)
.paddingTop(paddingTop)
.paddingRight(paddingRight)
.paddingBottom(paddingBottom)
.paddingLeft(paddingLeft)
.round(round)
(root);
const svg = d3.create("svg")
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
const node = svg.selectAll("a")
.data(leaves)
.join("a")
.attr("xlink:href", link == null ? null : (d, i) => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.x0},${d.y0})`);
node.append("rect")
.attr("fill", color ? (d, i) => color(G[i]) : fill)
.attr("fill-opacity", fillOpacity)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linejoin", strokeLinejoin)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);
if (T) {
node.append("title").text((d, i) => T[i]);
}
if (L) {
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;
node.append("clipPath")
.attr("id", (d, i) => `${uid}-clip-${i}`)
.append("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0);
node.append("text")
.attr("clip-path", (d, i) => `url(${new URL(`#${uid}-clip-${i}`, location)})`)
.selectAll("tspan")
.data((d, i) => `${L[i]}`.split(/\n/g))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, D) => `${(i === D.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
.text(d => d);
}
return Object.assign(svg.node(), {scales: {color}});
}
);
export const shuffle = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
export const G12DU = (amount) => {
return (Math.round(amount / DU * 100) / 100);
};
export const query__expenses = (walletPk, minTime, size = MAX_NB_TX) => {
return {
_source: ["amount", "recipient", "comment", "medianTime"]
,sort: [
{
"medianTime": "desc"
}
]
,size: size
,query: {
bool: {
filter: [
{
range: {
"medianTime": {
gte: minTime
}
}
}
,{
term: {
"issuer": walletPk
}
}
]
}
}
};
};
export const fetchExpenses = async (pubkey, minTime, limit = MAX_NB_TX) => {
shuffle(CESIUM_G1_NODES); // Mélanger la liste des noeuds
for (let node of CESIUM_G1_NODES) {
try {
const url = `${node}/g1/movement/_search`;
let queryBody = query__expenses(pubkey, minTime, limit);
console.log('expenses queryBody : \n', JSON.stringify(queryBody));
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryBody)
});
const data = await response.json();
console.log('node for the expenses of :\n', pubkey, '\n', node);
// expensesByRecipient initialize table
let expensesByRecipient = {};
let totalAmount = 0;
for (const hit of data.hits.hits) {
const tx = hit._source;
// tx expense example : Object { amount: 18000, medianTime: 1670155095, recipient: "9e7rdkfNQetyyKudJz9aRSQQV6HeebeZFsj3Ha5MsMZ1", comment: "Bolso cartera marron" }
console.log('tx expense :\n', tx);
if (!(tx.recipient in expensesByRecipient)) {
expensesByRecipient[tx.recipient] = 0;
}
expensesByRecipient[tx.recipient] += tx.amount/100;
totalAmount += tx.amount;
}
totalAmount = totalAmount/100;
return {
expensesTotalAmount: totalAmount
,expensesByRecipient: expensesByRecipient
};
} catch (error) {
console.error(`Failed to fetch data from ${node}: ${error}`);
// Si une erreur se produit, passez simplement au noeud suivant
}
}
throw new Error("Failed to fetch data from all nodes");
};
export const query__incomes = (walletPk, minTime, size = MAX_NB_TX) => {
return {
_source: ["amount", "issuer", "comment", "medianTime"]
,sort: [
{
"medianTime": "desc"
}
]
,size: size
,query: {
bool: {
filter: [
{
range: {
"medianTime": {
gte: minTime
}
}
}
,{
term: {
"recipient": walletPk
}
}
]
}
}
};
};
export const fetchIncomes = async (pubkey, minTime, limit = MAX_NB_TX) => {
shuffle(CESIUM_G1_NODES); // Mélanger la liste des noeuds
for (let node of CESIUM_G1_NODES) {
try {
const url = `${node}/g1/movement/_search`;
let queryBody = query__incomes(pubkey, minTime, limit);
console.log('incomes queryBody : \n', JSON.stringify(queryBody));
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryBody)
});
const data = await response.json();
console.log('node for the incomes of :\n', pubkey, '\n', node);
// incomesByIssuer initialize table
let incomesByIssuer = {};
let totalAmount = 0;
for (const hit of data.hits.hits) {
const tx = hit._source;
// tx income example : Object { amount: 40000, medianTime: 1693753222, comment: "Huevos gotas", issuer: "HpztnzdDP1FEiS4bnXVzQ3BQBAfRiMUMhHK7xzTGsVqe" }
console.log('tx income :\n', tx);
if (!(tx.issuer in incomesByIssuer)) {
incomesByIssuer[tx.issuer] = 0;
}
incomesByIssuer[tx.issuer] += tx.amount/100;
totalAmount += tx.amount;
}
totalAmount = totalAmount/100;
return {
incomesTotalAmount: totalAmount
,incomesByIssuer: incomesByIssuer
};
} catch (error) {
console.error(`Failed to fetch data from ${node}: ${error}`);
// Si une erreur se produit, passez simplement au noeud suivant
}
}
throw new Error("Failed to fetch data from all nodes");
};
export const query__cesium_profile = (pubkey) => {
return {
_source: [
"title",
"issuer"
]
,query: {
bool: {
filter: [
{term: {"_type": "profile"}}
]
,should: [
{ term: { "issuer": pubkey } },
]
,minimum_should_match: 1
}
}
};
};
export const query__cesium_profiles = (pubkeys, limit = 200) => {
return {
size: limit
,_source: ["title"]
,query: {
bool: {
filter: [
{term: {"_type": "profile"}}
]
,should: [
...pubkeys.map(pk => ({ term: { "issuer": pk } })),
]
,minimum_should_match: 1
}
}
};
};
export const fetchCesiumProfile = async (pubkey) => {
shuffle(CESIUM_G1_NODES); // Mélanger la liste des noeuds
for (let node of CESIUM_G1_NODES) {
try {
const url = `${node}/user/profile/_search`;
let queryBody = query__cesium_profile(pubkey);
console.log('cesium_profile queryBody : \n', JSON.stringify(queryBody));
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryBody)
});
const data = await response.json();
if (data.hits.hits[0] == undefined) {
return null;
}
return data.hits.hits[0]._source;
} catch (error) {
console.error(`Failed to fetch data from ${node}: ${error}`);
// Si une erreur se produit, passez simplement au noeud suivant
}
}
throw new Error("Failed to fetch data from all nodes");
};
export const fetchCesiumProfiles = async (pubkeys, limit) => {
shuffle(CESIUM_G1_NODES); // Mélanger la liste des noeuds
for (let node of CESIUM_G1_NODES) {
try {
const url = `${node}/user/profile/_search`;
let queryBody = query__cesium_profiles(pubkeys, limit);
console.log('cesium_profiles queryBody : \n', JSON.stringify(queryBody));
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryBody)
});
const data = await response.json();
let profiles = [];
for (const hit of data.hits.hits) {
profiles[hit._id] = {
title: hit._source.title
};
}
return profiles;
} catch (error) {
console.error(`Failed to fetch data from ${node}: ${error}`);
// Si une erreur se produit, passez simplement au noeud suivant
}
}
throw new Error("Failed to fetch data from all nodes");
};
export const displayExpenses = (expensesByRecipient, expensesTotalAmount, recipientsCesiumProfiles, chartColors, currentPubkey, currentProfile) => {
let screenElt = document.querySelector('#expenses');
screenElt.innerHTML = '';
let currentProfileTitleElt = document.createElement('h2');
screenElt.append(currentProfileTitleElt);
let title = null;
if (currentProfile == undefined) {
title = 'Dépenses du portefeuille <code>' + currentPubkey.substr(0, 8) + '</code> = '+ expensesTotalAmount + ' Ğ1';
} else {
title = 'Dépenses de <q>' + currentProfile.title + '</q> <code>' + currentPubkey.substr(0, 8) + '</code> = '+ expensesTotalAmount + ' Ğ1' ;
}
currentProfileTitleElt.innerHTML = title;
let svgContainer = document.createElement('article');
screenElt.append(svgContainer);
svgContainer.classList.add("svg-container");
let chartData = [];
// Formatting data
for (const recipient in expensesByRecipient) {
let recipientObj = {};
recipientObj.pk = recipient;
recipientObj.amount = expensesByRecipient[recipient];
let numberOptions = { roundingMode: 'ceil', minimumFractionDigits: 0, maximumFractionDigits: 0 };
recipientObj.displayedAmount = G12DU(recipientObj.amount).toLocaleString('fr-FR', numberOptions) + ' DU';
if (recipientsCesiumProfiles[recipient] != undefined
&& recipientsCesiumProfiles[recipient].title != undefined
) {
recipientObj.title = recipientsCesiumProfiles[recipient].title;
} else {
recipientObj.title = recipient.substr(0, 8);
}
chartData.push(recipientObj);
}
let chart = Treemap(chartData, {
path: d => d.title,
value: d => d.amount,
group: d => d.title,
label: (d, n) => d.title,
title: (d, n) => d.displayedAmount,
link: (d, n) => '#' + d.pk + '',
linkTarget: '_self',
tile: d3.treemapSquarify,
width: 1280,
height: 720,
padding: 0,
colors: chartColors,
fillOpacity: 1
});
svgContainer.append(chart);
screenElt.scrollIntoView({behavior: 'smooth'}, true);
// Display transaction list
const expensesList = document.getElementById("expensesList");
expensesList.innerHTML = "";
for (const recipient in expensesByRecipient) {
const listItem = document.createElement("li");
listItem.textContent = `To: ${recipientsCesiumProfiles[recipient]?.title || recipient}, Amount: ${expensesByRecipient[recipient]} Ğ1`;
expensesList.appendChild(listItem);
}
// Display total expenses
const totalExpensesAmount = document.getElementById("totalExpensesAmount");
totalExpensesAmount.textContent = expensesTotalAmount.toFixed(2);
};
export const displayIncomes = (incomesByIssuer, incomesTotalAmount, issuersCesiumProfiles, chartColors, currentPubkey, currentProfile) => {
let screenElt = document.querySelector('#incomes');
screenElt.innerHTML = '';
let currentProfileTitleElt = document.createElement('h2');
screenElt.append(currentProfileTitleElt);
let title = null;
if (currentProfile == undefined) {
title = 'Revenus du portefeuille <code>' + currentPubkey.substr(0, 8) + '</code> = '+ incomesTotalAmount + ' Ğ1';
} else {
title = 'Revenus de <q>' + currentProfile.title + '</q> <code>' + currentPubkey.substr(0, 8) + '</code> = ' + incomesTotalAmount + ' Ğ1';
}
currentProfileTitleElt.innerHTML = title;
let svgContainer = document.createElement('article');
screenElt.append(svgContainer);
svgContainer.classList.add("svg-container");
let chartData = [];
// Formatting data
for (const issuer in incomesByIssuer) {
let issuerObj = {};
issuerObj.pk = issuer;
issuerObj.amount = incomesByIssuer[issuer];
let numberOptions = { roundingMode: 'ceil', minimumFractionDigits: 0, maximumFractionDigits: 0 };
issuerObj.displayedAmount = G12DU(issuerObj.amount).toLocaleString('fr-FR', numberOptions) + ' DU';
if (issuersCesiumProfiles[issuer] != undefined
&& issuersCesiumProfiles[issuer].title != undefined
) {
issuerObj.title = issuersCesiumProfiles[issuer].title;
} else {
issuerObj.title = issuer.substr(0, 8);
}
chartData.push(issuerObj);
}
let chart = Treemap(chartData, {
path: d => d.title,
value: d => d.amount,
group: d => d.title,
label: (d, n) => d.title,
title: (d, n) => d.displayedAmount,
link: (d, n) => '#' + d.pk + '',
linkTarget: '_self',
tile: d3.treemapSquarify,
width: 1280,
height: 720,
padding: 0,
colors: chartColors,
fillOpacity: 1
});
svgContainer.append(chart);
screenElt.scrollIntoView({behavior: 'smooth'}, true);
// Display transaction list
const incomesList = document.getElementById("incomesList");
incomesList.innerHTML = "";
for (const issuer in incomesByIssuer) {
const listItem = document.createElement("li");
listItem.textContent = `From: ${issuersCesiumProfiles[issuer]?.title || issuer}, Amount: ${incomesByIssuer[issuer]} Ğ1`;
incomesList.appendChild(listItem);
}
// Display total incomes
const totalIncomesAmount = document.getElementById("totalIncomesAmount");
totalIncomesAmount.textContent = incomesTotalAmount.toFixed(2);
};
let formElt = document.querySelector('form#explore');
const treemapIt = async (pubkey, minTime, maxNbTx = MAX_NB_TX) => {
let dotsPos = pubkey.indexOf(':');
if (dotsPos != -1) {
pubkey = pubkey.substr(0, dotsPos);
}
// Change pubkey in form
const pubkeyElement = document.querySelector('input[name="pubkey"]');
pubkeyElement.value = pubkey
let { expensesTotalAmount, expensesByRecipient } = await fetchExpenses(pubkey, minTime, maxNbTx);
let { incomesTotalAmount, incomesByIssuer } = await fetchIncomes(pubkey, minTime, maxNbTx);
let nbRecipients = Object.keys(expensesByRecipient).length;
let nbIssuers = Object.keys(incomesByIssuer).length;
console.log("nb Expences recipients :\n", nbRecipients);
console.log("nb Income Issuers :\n", nbIssuers);
let recipientsList = Object.keys(expensesByRecipient);
let recipientsCesiumProfiles = await fetchCesiumProfiles(recipientsList, maxNbTx);
let issuersList = Object.keys(incomesByIssuer);
let issuersCesiumProfiles = await fetchCesiumProfiles(issuersList, maxNbTx);
let currentProfile = await fetchCesiumProfile(pubkey);
console.log('currentProfile :\n', currentProfile);
displayExpenses(expensesByRecipient, expensesTotalAmount, recipientsCesiumProfiles, chartColors, pubkey, currentProfile);
displayIncomes(incomesByIssuer, incomesTotalAmount, issuersCesiumProfiles, chartColors, pubkey, currentProfile);
let svg = document.querySelector('#expenses svg');
let links = svg.querySelectorAll("a");
for (const link of links) {
link.addEventListener('click', (linkEvent) => {
// linkEvent.currentTarget.preventDefault();
console.log('linkEvent.currentTarget :\n', linkEvent.currentTarget);
let pubkey = linkEvent.currentTarget.getAttribute('href').substr(1);
// treemapIt(pubkey, minDate);
});
}
};
// Function to export transactions to PDF
const exportToPDF = (transactions) => {
const pdf = new jsPDF({
orientation: 'landscape', // Set orientation to landscape
unit: 'mm',
format: 'a4',
});
pdf.text("Transaction Report", 10, 10);
// Define table columns
const columns = ["Date", "Recipient", "Outcome", "Issuer", "Incoming", "Total"];
// Initialize y-position for the table
let y = 20;
// Add table headers
columns.forEach((column, index) => {
pdf.text(column, 10 + index * 40, y);
});
// Increment y for the next row
y += 10;
// Add transactions to the table
transactions.forEach((transaction) => {
pdf.text(transaction.medianTime, 10, y);
pdf.text(transaction.recipient || "", 50, y);
pdf.text(transaction.amount, 90, y);
pdf.text(transaction.issuer || "", 130, y);
pdf.text(transaction.amount, 170, y);
// Increment y for the next row
y += 10;
// Add a new page if the table exceeds one page
if (y > 280) {
pdf.addPage();
y = 20;
// Add table headers for the new page
columns.forEach((column, index) => {
pdf.text(column, 10 + index * 40, y);
});
// Increment y for the next row on the new page
y += 10;
}
});
// Save the PDF
pdf.save("transaction_report.pdf");
};
const getPkInHash = () => {
let hash = window.location.hash;
hash = hash.substring(1);
return hash;
};
window.addEventListener("popstate", (popEvent) => {
let pk = getPkInHash();
console.log('\n\n\npubkey :\n', pk);
if (pk != '') {
treemapIt(pk, minTime, txLimit);
}
});
formElt.addEventListener('submit', (formEvent) => {
formEvent.preventDefault();
txLimit = formEvent.target.querySelector('input[name="txLimit"]').value;
let minDateStr = formEvent.target.querySelector('input[name="minDate"]').value;
let minDate = new Date(minDateStr);
minTime = Math.floor(minDate.valueOf()/1000);
console.log('minTime :\n', minTime);
let pubkey = formEvent.target.querySelector('input[name="pubkey"]').value;
window.location = '#';
window.location = '#' + pubkey;
});
window.addEventListener('DOMContentLoaded', (loadEvent) => {
let formElt = document.getElementById('explore');
let minDateElt = formElt.querySelector('input[name="minDate"]');
let now = Date();
console.log('now : \n', now);
let aMonthAgo = new Date(now);
aMonthAgo.setMonth(aMonthAgo.getMonth() - 1);
console.log('aMonthAgo : ', aMonthAgo);
console.log('aMonthAgo.getMonth() :\n', aMonthAgo.getMonth());
let dateStr = aMonthAgo.getFullYear() + '-' + (aMonthAgo.getMonth()+1).toString().padStart(2,0) + '-' + aMonthAgo.getDate().toString().padStart(2,0);
console.log('dateStr : ', dateStr);
minDateElt.value = dateStr;
});