Published
Edited
Apr 5, 2022
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
targets = [
{ id: 'f021251' },
{ id: 'f01019' },
{ id: 'f01038' },
{ id: 'f01062', displayDepth: 8, minXAdjust: 0, width: 1000 },
{ id: 'f035300' },
{ id: 'f03637' },
{ id: 'f023836' },
{ id: 'f084877', displayDepth: 9 },
{ id: 'f035474' }
]
Insert cell
Insert cell
function childrenWithRegions (node) {
return node.leaves().filter(d => d.data.regions)
}
Insert cell
function firstAncestorWithRegions (node) {
for (const ancestor of node.ancestors()) {
if (childrenWithRegions(ancestor).length > 0) {
return ancestor
}
}
return null
}
Insert cell
function fillFactor (topAncestor, provider) {
// console.log(`fillFactor ancestor: ${topAncestor.data.id} ` +
// `SP: ${provider.data.id}`)
if (topAncestor === provider) {
return 1
}
let factor = 1
const ancestors = provider.ancestors().slice(1)
for (const ancestor of ancestors) {
const ancestorFillFactor = getFillFactor(ancestor)
factor = factor * ancestorFillFactor
// console.log(` Ancestor: ${ancestor.data.id} ${ancestorFillFactor.toFixed(2)} ${factor.toFixed(2)}`)
if (ancestor === topAncestor) break
}
return factor
}
Insert cell
function coverage (node) {
let total = 0
let withRegion = 0

if (node.children) {
for (const child of node.children) {
const [branchTotal, branchWithRegion] = coverage(child)
total += branchTotal
withRegion += branchWithRegion
}
} else {
total += node.data.qualityAdjPower
if (node.data.regions) {
withRegion += node.data.qualityAdjPower
}
}
return [total, withRegion]
}
Insert cell
function powerFromNode (node) {
let totalPower = 0
for (const provider of node.leaves()) {
totalPower += provider.data.qualityAdjPower
}
return totalPower
}
Insert cell
function getPartitions (node) {
const rows = []
if (node.children) {
for (const child of node.children) {
const [totalPower, coveredPower] = coverage(child)
rows.push({
id: child.data.id,
totalPower,
coveredPower
})
}
}
return rows
}
Insert cell
function getFillFactor (node) {
const partitions = getPartitions(node)
const totalPower = partitions.reduce(
(acc, {totalPower, coveredPower}) => acc + totalPower, 0
)
const coveredTotalPower = partitions.reduce(
(acc, {totalPower, coveredPower}) => coveredPower ? acc + totalPower : acc, 0
)
const fillFactor = totalPower / coveredTotalPower
return fillFactor
}
Insert cell
function getDelegates (node) {
const ancestor = firstAncestorWithRegions(node)
const delegates = childrenWithRegions(ancestor)
.map(d => {
const f = fillFactor(ancestor, d)
return {
regions: d.data.regions.join(','),
id: d.data.miner_id,
qualityAdjPower: d.data.qualityAdjPower,
fillFactor: f,
scaledPower: d.data.qualityAdjPower * f
}
})
.sort((a, b) => {
const regionComp = a.regions.localeCompare(b.regions)
if (regionComp) return regionComp
return Number(a.id.slice(1)) - Number(b.id.slice(1))
})
return delegates
}
Insert cell
async function hashProviderId (id) {
const encoder = new TextEncoder()
const data = encoder.encode(id)
const hash = await crypto.subtle.digest('SHA-256', data)
const hash2Bytes = new Uint8Array(hash.slice(0,2))
return hash2Bytes[0] * 256 + hash2Bytes[1]
}
Insert cell
async function matchDelegate (node) {
const delegates = getDelegates(node)
const totalScaledPower = delegates.reduce((acc, { scaledPower }) => acc + scaledPower, 0)
const hash = await hashProviderId(node.data.id)
let powerPosition = (hash / 255.0 / 255.0) * totalScaledPower
let lastId
for (const { id, scaledPower } of delegates) {
lastId = id
powerPosition -= scaledPower
if (powerPosition < 0) break
}
return lastId
}
Insert cell
async function getTreeWithDelegates (tree) {
for (const provider of tree.leaves()) {
if (!provider.data.regions) {
const delegateId = await matchDelegate(provider)
console.log(`J: ${provider.data.id} => ${delegateId}`)
provider.data.delegateId = delegateId
}
}
return tree
}
Insert cell
Insert cell
Insert cell
async function getTree (tree, path, { displayDepth = 3, minXAdjust = 100, dx = 10 }) {
const selectedSubtree = selectPath(tree, path)
const prunedTree = topOfTree(selectedSubtree, displayDepth)
const prunedTreeWithDelegates = await getTreeWithDelegates(prunedTree)
return Tree2(prunedTreeWithDelegates, {
padding: 5,
minXAdjust,
widthAdjust: 0,
dx,
tree: d3.tree,
sort: (a, b) => {
Number(a.data.id.slice(1)) - Number(b.data.id.slice(1))
},
label: d => {
const regions = d.data.children && d.data.children.regions.sort()
return (
(d.data.miner_id ?
`SP: ${d.data.miner_id} - ${bytes(d.data.qualityAdjPower, { mode: 'binary' })}` : d.data.id) +
(d.data.regions ?
` - ${d.data.regions.join(', ')}` : '') +
(d.data.children ?
` ... ${d.data.children.count} SPs, ` +
`${bytes(d.data.children.qualityAdjPower, { mode: 'binary' })}, ` +
`${regions.length > 0 ? regions.join(', ') : 'No regions'}` +
` - ${(d.data.children.coveredCount / d.data.children.count * 100).toFixed(1)}%`
: '')
)
},
delegateLabel: d => {
return (showDelegates && d.data.delegateId ? ` ⇨ ${d.data.delegateId} - ` +
prunedTreeWithDelegates
.find(d2 => d2.data.id === d.data.delegateId)
.data.regions.join(', ')
: '')
},
/*
link: d => {
*/
// let url = `${document.baseURI.replace(/\?.*/,'')}`
/* if (d.depth === 0) {
const oldPath = params.path ? params.path.split('/') : []
const newPath = oldPath.slice(0, oldPath.length - 1)
if (newPath.length > 0) {
url += `?path=${newPath.join('/')}`
}
} else {
url += `?path=` + (params.path ? `${params.path}/` : '') +
d.ancestors().map(d => d.data.id).reverse().slice(1).join('/')
}
return url
},
linkTarget: '_top'
*/
})
}
Insert cell
Insert cell
stratify = d3.stratify()
.id(d => d["address"])
.parentId(d => d["funded_from"])
Insert cell
fundingTree = stratify(fundingTreeData)
Insert cell
// fundingTreeWithDelegates = getTreeWithDelegates(fundingTree)
Insert cell
fundingTree.children
Insert cell
function selectPath (tree, path) {
let subTree = tree
if (!path) {
return tree
}

nextSubtree: for (const pathElement of path) {
for (const childNode of subTree.children) {
if (childNode.data.id === pathElement) {
subTree = childNode
continue nextSubtree
}
}
return null
}
return subTree.copy()
}
Insert cell
topOfTree = function (tree, levels) {
const nodeData = []
for (const descendant of tree) {
if (descendant.depth < levels) {
const data = {...descendant.data}
if (descendant.depth === 0) {
delete data.funded_from
}
if (descendant.depth === levels - 1
&& descendant.children) {
const regions = new Set()
let coveredCount = 0
for (const provider of descendant.leaves()) {
if (provider.data.regions && provider.data.regions.length > 0) {
coveredCount++
for (const region of provider.data.regions) {
regions.add(region)
}
}
}
data.children = {
count: descendant.count().value,
coveredCount,
qualityAdjPower: descendant.sum(d => d.qualityAdjPower).value,
regions: [...regions]
}
}
nodeData.push(data)
}
}
return stratify(nodeData)
}
Insert cell
Insert cell
Insert cell
import { graph as graph2 } from '@jimpick/d3-hierarchy-wider-svg/2'
Insert cell
import { graph } from '@d3/d3-hierarchy'
Insert cell
import { Tree } from '@d3/tree'
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/tree
function Tree2(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 parent’s identifier
children, // if hierarchical data, given a d in data, returns its children
tree = d3.tree, // layout algorithm (typically d3.tree or d3.cluster)
sort, // how to sort nodes prior to layout (e.g., (a, b) => d3.descending(a.height, b.height))
label, // given a node d, returns the display name
delegateLabel,
title, // given a node d, returns its hover text
link, // given a node d, its link (if any)
linkTarget = "_blank", // the target attribute for links (if any)
width = 640, // outer width, in pixels
height, // outer height, in pixels
r = 3, // radius of nodes
padding = 1, // horizontal padding for first and last column
fill = "#999", // fill for nodes
fillOpacity, // fill opacity for nodes
stroke = "#555", // stroke for links
strokeWidth = 1.5, // stroke width for links
strokeOpacity = 0.4, // stroke opacity for links
strokeLinejoin, // stroke line join for links
strokeLinecap, // stroke line cap for links
halo = "#fff", // color of label halo
haloWidth = 3, // padding around the labels
minXAdjust = 0,
widthAdjust = 0,
dx = 10
} = {}) {
// 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.
const root = path != null ? d3.stratify().path(path)(data)
: id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children);

// Compute labels and titles.
const descendants = root.descendants();
const L = label == null ? null : descendants.map(d => label(d.data, d));
const DL = delegateLabel == null ? null : descendants.map(d => delegateLabel(d.data, d));

// Sort the nodes.
if (sort != null) root.sort(sort);

// Compute the layout.
// const dx = 10;
const dy = width / (root.height + padding);
tree().nodeSize([dx, dy])(root);

// Center the tree.
let x0 = Infinity;
let x1 = -x0;
root.each(d => {
if (d.x > x1) x1 = d.x;
if (d.x < x0) x0 = d.x;
});

// Compute the default height.
if (height === undefined) height = x1 - x0 + dx * 2;

const svg = d3.create("svg")
.attr("viewBox", [-dy * padding / 2 + minXAdjust, x0 - dx, width + widthAdjust, 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);

svg.append("g")
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));

const node = svg.append("g")
.selectAll("a")
.data(root.descendants())
.join("a")
.attr("xlink:href", link == null ? null : d => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr("transform", d => `translate(${d.y},${d.x})`);

node.append("circle")
.attr("fill", d => d.children ? stroke : fill)
.attr("r", r);

if (title != null) node.append("title")
.text(d => title(d.data, d))

const resultNode = svg.node();

if (L) {
const texts = node.append("text")
.attr("dy", "0.32em")
.attr("x", d => d.children ? -6 : 6)
.attr("text-anchor", d => d.children ? "end" : "start")
.text((d, i) => L[i])

if (DL) {
texts.append("tspan")
.attr("fill", "blue")
.text((d, i) => ` ${DL[i]}`)
}

texts.on("click", (event, [d]) => {
console.log("Click", d.data.data.id)
resultNode.selectedNode = d
resultNode.dispatchEvent(new CustomEvent("input", {bubbles: true}))
})
}

/*
.call(text => text.clone(true))
.attr("fill", "none")
.attr("stroke", halo)
.attr("stroke-width", haloWidth) */

resultNode.selectedNode = null;
return resultNode;
}
Insert cell
params = [...(new URLSearchParams(location.search.substring(1))).entries()].reduce((acc, [key, value]) => ({ [key]: value, ...acc }), {})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more