OSM2IPFS/earth/inout/index_fichiers/app.js

773 lines
25 KiB
JavaScript
Raw Permalink 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 = 640, // outer width, in pixels
height = 400, // 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"]
,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);
console.log('expenses data :\n', data);
let expensesByRecipient = {};
let totalAmount = 0;
let transactions = [];
for (const hit of data.hits.hits) {
const tx = hit._source;
if (!(tx.recipient in expensesByRecipient)) {
expensesByRecipient[tx.recipient] = 0;
}
expensesByRecipient[tx.recipient] += tx.amount/100;
totalAmount += tx.amount;
transactions.push({
date: new Date(tx.medianTime).toLocaleDateString(),
walletId: pubkey,
income: 0,
outcome: tx.amount,
comment: "Expense Comment", // Add your logic to get the comment
});
}
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"]
,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);
console.log('incomes data :\n', data);
let incomesByIssuer = {};
let totalAmount = 0;
let transactions = [];
for (const hit of data.hits.hits) {
const tx = hit._source;
if (!(tx.issuer in incomesByIssuer)) {
incomesByIssuer[tx.issuer] = 0;
}
incomesByIssuer[tx.issuer] += tx.amount/100;
totalAmount += tx.amount;
transactions.push({
date: new Date(tx.medianTime).toLocaleDateString(),
walletId: pubkey,
income: tx.amount,
outcome: 0,
comment: "Income Comment", // Add your logic to get the comment
});
}
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");
};
// Function to export transactions to PDF
const exportToPDF = (transactions) => {
const pdf = new jsPDF();
pdf.text("Transaction Report", 20, 10);
// Define table columns
const columns = ["Date", "Wallet ID", "Income", "Outcome", "Comment"];
// Initialize y-position for table
let y = 20;
// Add table headers
columns.forEach((column, index) => {
pdf.text(column, 20 + index * 40, y);
});
// Increment y for the next row
y += 10;
// Add transactions to the table
transactions.forEach((transaction) => {
pdf.text(transaction.date, 20, y);
pdf.text(transaction.walletId, 60, y);
pdf.text(transaction.income.toString(), 100, y);
pdf.text(transaction.outcome.toString(), 140, y);
pdf.text(transaction.comment, 180, y);
// Increment y for the next row
y += 10;
});
// Save the PDF
pdf.save("transaction_report.pdf");
};
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);
};
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);
};
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 recipients :\n", nbRecipients);
console.log("nb 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);
});
}
};
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;
});