Published
Edited
Jan 27, 2021
8 stars
Also listed in…
Interactivity
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
icons = {
return FileAttachment("taxonomy_2021-01-23@1.json").json(); // comment out to update
return fetchTaxonomy(); // to be stored as JSON
}
Insert cell
fetchTaxonomy = () => {
const requests = identifiers.map(id => fetchMetadata(id));
return Promise.all(requests);
}
Insert cell
identifiers = {
const symbols = sprite.querySelectorAll("symbol");
return [...symbols].map(symbol => symbol.id);
}
Insert cell
sprite = svg`${spriteLiteral}`
Insert cell
spriteLiteral = {
const url = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/bootstrap-icons.svg";
return fetch(url).then(response => response.text());
}
Insert cell
fetchMetadata = async (id) => {
const url = `https://raw.githubusercontent.com/twbs/icons/main/docs/content/icons/${id}.md`;
const yamlFrontMatter = await fetch(url).then(response => response.text());
const frontMatter = getFrontMatter(yamlFrontMatter);
return getMetadata(id, frontMatter);
}
Insert cell
getFrontMatter = (yamlFrontMatter) => {
// second YAML section marker ('---') gives null as second result
const [frontMatter] = yaml.loadAll(yamlFrontMatter);
return frontMatter;
}
Insert cell
yaml = await require("js-yaml@4")
Insert cell
getMetadata = (id, frontMatter) => {
const metadata = {id};
metadata.category = getCategory(frontMatter);
if (frontMatter.tags !== null) {
metadata.tags = frontMatter.tags;
}
return metadata;
}
Insert cell
getCategory = (frontMatter) => {
const unification = new Map([
["Deviecs", "Devices"],
["Misc", "Miscellaneous"]
]);
const getCategory = (frontMatter) => {
if (frontMatter.categories != null) {
let [category] = frontMatter.categories; // single category, so far
if (unification.has(category)) {
category = unification.get(category);
}
return category.toLowerCase();
}
return "unspecified";
}
return getCategory(frontMatter);
}
Insert cell
Insert cell
filteredIcons = {
if (filteredKeywordPhrases.length > 0) {
return filteredKeywordPhrases.map(phrase => iconsByKeywordPhrase.get(phrase));
}
const nothingFound = "emoji-dizzy emoticon";
return [iconsByKeywordPhrase.get(nothingFound)];
}
Insert cell
filteredKeywordPhrases = {
const filterString = form.filterString;
if (filterString.length > 0) {
return keywordPhrases.filter(phrase => phrase.includes(filterString));
}
return keywordPhrases;
}
Insert cell
keywordPhrases = [...iconsByKeywordPhrase.keys()]
Insert cell
iconsByKeywordPhrase = {
const specificIcons = form.hideFilledIcons ? nonFilledIcons : icons;
const keyValuePairs = specificIcons.map(icon => [getKeywordPhrase(icon), icon]);
return new Map(keyValuePairs);
}
Insert cell
nonFilledIcons = icons.filter(icon => !icon.id.includes("-fill"))
Insert cell
getKeywordPhrase = (icon) => {
let phrase = icon.id;
if (icon.tags !== undefined) {
for (const tag of icon.tags) {
if (!phrase.includes(tag)) {
phrase += ` ${tag}`;
}
}
}
return phrase;
}
Insert cell
Insert cell
filteredCategories = [...filteredIconsByCategory.keys()].sort()
Insert cell
filteredIconsByCategory = d3.group(filteredIcons, d => d.category)
Insert cell
Insert cell
createForm = () => {
const form = html`
<form>
<input name="filterString" type="text" placeholder="Start typing to filter…">
<span class="checkbox">
<input name="groupIconsByCategory" type="checkbox" id="groupByCategoryCheckbox" checked>
<label for="groupByCategoryCheckbox">Group icons by category</label>
</span>
<span class="checkbox">
<input name="hideFilledIcons" type="checkbox" id="hideFilledCheckbox">
<label for="hideFilledCheckbox">Hide filled icons</label>
</span>
</form>
`;
form.onsubmit = (event) => event.preventDefault();
form.filterString.oninput = () => {
form.value.filterString = form.filterString.value;
form.dispatchEvent(new CustomEvent("input"));
}
form.filterString.onchange = () => {
form.filterString.blur();
}
form.groupIconsByCategory.onchange = () => {
form.value.groupIconsByCategory = form.groupIconsByCategory.checked;
form.dispatchEvent(new CustomEvent("input"));
}
form.hideFilledIcons.onchange = () => {
form.value.hideFilledIcons = form.hideFilledIcons.checked;
form.dispatchEvent(new CustomEvent("input"));
}
form.value = {
filterString: "",
groupIconsByCategory: true,
hideFilledIcons: false
};
return form;
}
Insert cell
showIcons = () => {
const container = d3.create("div");
if (form.groupIconsByCategory) {
container.attr("class", "categorized-icons");
addCategorizedIcons(container);
} else {
container.attr("class", "uncategorized-icons");
addIcons(container, filteredIcons);
}

return container.node();
}
Insert cell
d3 = require("d3-array@2", "d3-selection@2")
Insert cell
addCategorizedIcons = (parent) => filteredCategories.forEach(category => addIconCategory(parent, category))
Insert cell
addIconCategory = (parent, category) => {
const container = parent.append("div")
.attr("class", "icon-category");
container.append("span")
.attr("class", "category-name")
.text(category);

const icons = filteredIconsByCategory.get(category);
addIcons(container, icons);
}
Insert cell
addIcons = (parent, icons) => {
const container = parent.append("div")
.attr("class", "icons");

icons.forEach((icon) => addIcon(container, icon));
}
Insert cell
addIcon = (parent, icon) => {
const link = parent.append("a")
.attr("class", "inspector-link")
.attr("title", getTitle(icon))
.attr("href", `https://observablehq.com/@nikita-sharov/bootstrap-icon-inspector#${icon.id}`);

const svg = link.append("svg")
.attr("class", "icon");

svg.append("use")
.attr("href", `#${icon.id}`);
}
Insert cell
// remark: front matter titles are inconsistently spelled identifiers
getTitle = (icon) => {
const keywords = getKeywordPhrase(icon).split(" ");
return keywords.join(", ");
}
Insert cell
html`<style>
.checkbox {
margin-left: 10px;
}

label {
font-size: 0.85em;
}

.categorized-icons {
columns: 2 320px;
}

.icon-category {
break-inside: avoid-column;
margin-bottom: 20px;
}

.icons {
display: flex;
flex-wrap: wrap;
margin-left: -4px;
}

a[href].inspector-link {
color: #1b1e23;
display: block;
padding: 8px 4px;
}

a[href].inspector-link:hover {
color: #3182bd;
}

.icon {
display: block;
height: 16px;
width: 16px;
}

time,
.category-name {
font-family: sans-serif;
font-size: 14px;
text-transform: uppercase;
}

code {
background-color: #efefef;
padding-left: 3px;
padding-right: 3px;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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