Public
Edited
Apr 4, 2023
Importers
Insert cell
Insert cell
{
const w = 50
const svg = d3.create('svg')
.attr('width', w)
.attr('height', w)
.attr('viewBox', [-w/2, -w/2, w, w])

svg.append('rect')
.attr('x', -20)
.attr('y', -20)
.attr('width', 40)
.attr('height', 40)
.attr('rx', 10)
.style('fill', 'green')
const test_icon = await createFAIcon({fa_name: 'check', color: 'white'})

svg.append(() => test_icon.node())

return svg.node()
}
Insert cell
// createFAIcon = function (options = {}) {
// const size = options.size ?? 25
// const fa_name = options.fa_name ?? 'circle'
// const color = options.color ?? 'black'

// const temp_id = DOM.uid('icon-temp-id')

// const icon = d3.create("xhtml:i")
// .attr('class', `fas fa-${fa_name}`)
// .attr('id', temp_id.id)
// .attr('padding', 2)
// .style('color', d3.color(color))
// .style('font-size', `${size}px`)
// .style('display', 'inline-block')
// .style('vertical-align', 'middle')

// const container_height = size + 4

// const container_class = 'svg-faicon-container'
// const container = d3.create('svg:foreignObject')
// .attr('class', container_class)
// .attr("height", container_height)
// .attr('y', -size/2 -3)
// .style('text-align', 'center')
// .style('line-height', container_height+'px')

// elementReady(`#${temp_id.id}`).then(setContainerWidth)//el => setWidth.apply(el))

// function setContainerWidth() {
// const width = icon.node().getBoundingClientRect().width + 2
// icon.attr('id', null)
// container
// .attr("width", width)
// .attr("x", -width/2)
// }
// container.append(() => icon.node())
// return container
// }
Insert cell
createFAIcon = await createFAIconFactory()
Insert cell
createFAIconFactory = async () => {
const fa_styles_class = 'fa-stylesheet'
const fa_styles_id = DOM.uid('fa-styles')
d3.selectAll(`style.${fa_styles_class}`).remove()

const fa_styles = faStyle({solid:true}).then(styles =>
d3.select(document.head)
.append(() => styles)
.attr('id', fa_styles_id.id)
.attr('class', fa_styles_class)
)
const width_test_div_class = 'fa-icon-width-test-div'
d3.selectAll(`div.${width_test_div_class}`).remove()
const width_test_div_id = DOM.uid('fa-icon-width-test-div')
let width_test_div
function makeWidthTestDiv(id) { // probably this stuff should be abstracted out into a generic inspector
const div = d3.create('div')
.attr('id', id)
.attr('class', width_test_div_class)
.attr('width', 0)
.attr('height', 0)
.attr('visibility', 'hidden')
document.body.appendChild(div.node())
return div
}

await fa_styles
return function (options = {}) {
width_test_div = d3.select(`#${width_test_div_id.id}`).empty() ?
makeWidthTestDiv(width_test_div_id) :
width_test_div
const size = options.size ?? 25
const fa_name = options.fa_name ?? 'circle'
const color = options.color ?? 'black'

const temp_id = DOM.uid('icon-temp-id')

const icon = d3.create("xhtml:i")
.attr('class', `fas fa-${fa_name}`)
.attr('id', temp_id.id)
.attr('padding', 2)
.style('color', d3.color(color))
.style('font-size', `${size}px`)
.style('display', 'inline-block')
.style('vertical-align', 'middle')

const container = d3.create('svg:foreignObject')
const container_class = 'svg-faicon-container'

const test_element = width_test_div.append(() => icon.node().cloneNode(true))

elementReady(`#${temp_id.id}`, width_test_div.node()).then(el => {
const icon_width = el.getBoundingClientRect().width

const container_height = size + 4
const container_width = icon_width + 2

container
.attr('class', container_class)
.attr("height", container_height)
.attr("width", container_width)
.attr('y', -size/2 -3)
.attr("x", -container_width/2)
.style('text-align', 'center')
.style('line-height', container_height+'px')
})

// const element = elementReady(`#${temp_id.id}`, width_test_div.node())
// const icon_width = element.getBoundingClientRect().width
// const container_height = size + 4
// const container_width = icon_width + 2

// container
// .attr('class', container_class)
// .attr("height", container_height)
// .attr("width", container_width)
// .attr('y', -size/2 -3)
// .attr("x", -container_width/2)
// .style('text-align', 'center')
// .style('line-height', container_height+'px')

container.append(() => icon.node())

return container
}
}
Insert cell
problem`### Icon width race condition causes problems downstream
Because the element width isn't set till after it first gets rendered, any code using a pre-render approach to inspect its width will itself not read the correct width, because the check will happen before the width is set.

This suggests we do need a pre-render solution after all.`
Insert cell
problem.closed`### Rendering of FontAwesome icons depends on an async request

faStyle is an async function ultimately processing a request for the necessary styles from the FontAwesome servers or some other CDN. Consequently, any downstream function that relies on faStyle to be in place for correct rendering (like, for example, width inspection of icon elements) must itself wait for faStyle to resolve.

Since faStyle needs to be included separately in any notebook using these icons, any use case that relies on inspecting the icon for width or whatever must be able to monitor the status of the Promise returned by the faStyle call *in that notebook*. It therefore must have access to a reference to that return value. Hence a dependency like this one must also be passed a reference to the faStyle call from the relevant notebook, *or* the style must be applied to the DOM from within this function, with the appropriate await; anything downstream would then have to await the outcome of this function.`
Insert cell
conclusion`faStyle is now run from within the factory function that creates createFAIcon, and the relevant styles are appended to the DOM. Because Observable implicitly awaits promises between cells, we can do the await for createFAIcon in this notebook and it will implicitly be awaited by anything using it on import`
Insert cell
question`Can the elementReady test cause problems downstream? createFAIcon is no longer an async function but in theory the container it returns might not have had its width set etc by the time it returns. Do we even need this test? It should only be necessary if there is some delay to appending the element to the hidden DOM element surely, which there is no reason for here because we already established that the styles are ready. I guess if the browser is overloaded there could be some delay. But then I'm not doing anything downstream to cope with that at the moment because the width test stuff was all a bit of a pig with the cascading async stuff. Maybe if that stuff was a bit more explicit in its use of promises we could resolve it
`
Insert cell
Insert cell
Insert cell
icon_positioning_experiment = {
const svg = d3.create('svg')
.attr('viewBox', [-40, -30, 80, 60])

const icon_size = 25
const icon = await createFAIcon({fa_name: 'lightbulb', size:icon_size})
svg.append(() => icon.node())

svg.append('rect')
.attr('x', icon.attr('x'))
.attr('y', icon.attr('y'))
.attr('width', icon.attr('width'))
.attr('height', icon.attr('height'))
.style('stroke', 'red')
.style('opacity', 0.5)
.style('fill', 'none')

// svg.selectAll('foreignObject')
// .style('border-color', 'red')
// .style('border-style', 'solid')

svg.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 1)
.attr('fill', 'red')
return svg.node()
}
Insert cell
// check that fontawesome import is working
test_icon = (d3.create("xhtml:i")
.attr('class', `fas fa-circle label-icon`)
.style('color', d3.color('black'))
.style('font-size', '25px')
.style('display', 'inline-block')
.style('vertical-align', 'middle')
).node()
Insert cell
// fa_style = faStyle({solid: true})
Insert cell
import { style as faStyle } from "@airbornemint/fontawesome"
Insert cell
idea`### Finish modifying elementReady to take a parent parameter // Done?

Not sure exactly how this affects the querySelector lines

Possibly also write a generic inspector that takes a callback to retrieve the desired quantity`
Insert cell
// MIT Licensed
// Author: jwilson8767

/**
* Waits for an element satisfying selector to exist, then resolves promise with the element.
* Useful for resolving race conditions.
*
* @param selector
* @returns {Promise}
*/
function elementReady(selector, parent = document.documentElement) {
return new Promise((resolve, reject) => {
let el = parent.querySelector(selector);
if (el) {
resolve(el);
return
}
new MutationObserver((mutationRecords, observer) => {
// Query for elements matching the specified selector
Array.from(parent.querySelectorAll(selector)).forEach((element) => {
resolve(element);
//Once we have resolved we don't need the observer anymore.
observer.disconnect();
});
})
.observe(parent, {
childList: true,
subtree: true
});
});
}
Insert cell
import {idea, question, problem, conclusion, openThoughts, makeApi} from '@grahamsnyder/thought-process'
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