Public
Edited
Mar 29, 2024
Paused
14 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
searchedSet = new Set(searched)
Insert cell
COMMON_DOT_ATTRIBUTES = ({
x: (d) => d.umapEmbedding[0],
y: (d) => d.umapEmbedding[1],
tip: true,
href: (d) => url(d),
target: "_blank",
sort: { channel: "launch_year", order: "ascending" },
opacity: (d) => (searchedSet.has(d) ? 0.7 : 0.02),
fill: (d) => (d.mainAccords ? d.mainAccords[0] : "none"),
channels: TIP_CHANNELS,
tip: { anchor: "left" }
})
Insert cell
TIP_CHANNELS = ({
brand: "brand",
perfume: "perfume",
"launch year": "launch_year"
})
Insert cell
Insert cell
Insert cell
perfumes_rearranged.sqlite
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
parsedBigPerfumes.filter((d) => d.url == null)
Insert cell
parsedBigPerfumes = bigPerfumes
.map((d) => ({
...d,
imageUrl: IMAGE_PREFIX + d.image.substring(2),
url: url(d),
mainAccords: JSON.parse(d.main_accords),
notes: JSON.parse(d.notes)
}))
.map((d) => ({
...d,
slug: slug(d)
}))
Insert cell
brands = new Set(parsedBigPerfumes.map((d) => d.brand))
Insert cell
Insert cell
function url(perfume) {
const match = perfume.image.match(EXTRACT_ID_REGEX);

if (!match || !match[1]) {
return;
}

const id = match[1];

const brandSlug = perfume.brand.replaceAll(" ", "-");
const perfumeSlug = perfume.perfume.replaceAll(" ", "-");

return `https://www.fragrantica.com/perfume/${brandSlug}/${perfumeSlug}-${id}.html`;
}
Insert cell
url(parsedBigPerfumes[0])
Insert cell
function numberList(xs) {
let acc = "";
let i = 1;
for (const x of xs ?? []) {
acc += `${i}. ${x}\n`;
i++;
}
return acc;
}
Insert cell
function slug(perfume) {
const { mainAccords, notes } = perfume;

if (notes == null) {
return `# Fragrance description:
N/A`;
}

let notesSlug = undefined;

if (Array.isArray(notes)) {
notesSlug = "## Notes (in order of votes)\n";
notesSlug += numberList(notes);
} else {
notesSlug = "## Notes\n";

if (notes.top != null) {
notesSlug += "### Top notes (in order of votes)\n";
notesSlug += numberList(notes.top);
}

if (notes.middle != null) {
notesSlug += "### Middle notes (in order of votes)\n";
notesSlug += numberList(notes.middle);
}

if (notes.base != null) {
notesSlug += "### Base notes (in order of votes)\n";
notesSlug += numberList(notes.base);
}
}

return `# Fragrance description
## Main accords (in order of prominence)
${numberList(mainAccords)}
${notesSlug}`.trim();
}
Insert cell
slug(parsedBigPerfumes[0])
Insert cell
slug(parsedBigPerfumes[18])
Insert cell
Insert cell
parsedBigPerfumes.map((d) => d.slug)
Insert cell
tokens = parsedBigPerfumes
.map((d) => d.slug)
.join("\n")
.split(" ").length
Insert cell
pages = tokens / 800
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
mutable accumulatedEmbeddings = []
Insert cell
{
// mutable accumulatedEmbeddings = [];

// for (const chunk of _.chunk(parsedBigPerfumes, 1000)) {
// const embeddings = await embed(chunk.map((d) => d.slug));

// const chunkZip = [];
// for (let i = 0; i < chunk.length; ++i) {
// chunkZip.push({
// ...chunk[i],
// openAiEmbedding: embeddings.data[i].embedding
// });
// }

// mutable accumulatedEmbeddings = [
// ...mutable accumulatedEmbeddings,
// ...chunkZip
// ];

// await Promises.delay(100000);
// }
}
Insert cell
Insert cell
umapProgress(500)
Insert cell
// tidy = (
// await zipWithUmap(accumulatedEmbeddings, (d) => d.openAiEmbedding, {
// // nNeighbors: accumulatedEmbeddings.length,
// // minDist: 0,
// // spread: 0
// })
// )
// .filter((d) => !!d.mainAccords)
// .map((d) => ({ ...d, openAiEmbedding: undefined }))
Insert cell
Insert cell
tidy0DistUmapParams = FileAttachment("tidy.json").json()
Insert cell
tidyNoUmapParams = FileAttachment("tidy (1).json").json()
Insert cell
tidy = tidyNoUmapParams
Insert cell
forImport = tidy
.map((d) => ({
...d,
brand_name: d.brand,
name: d.perfume,
image_url: d.imageUrl,
main_accords: d.mainAccords
}))
.map((d) => {
delete d.perfume;
delete d.brand;
delete d.image;
delete d.imageUrl;
delete d.mainAccords;
delete d.longevity;
delete d.main_accods;
delete d.sillage;
return d;
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
palette = FileAttachment("palette.png").image()
Insert cell
Insert cell

async function getAllColorHexesFromImage(imgElement) {
// Make sure the image is loaded
if (!imgElement.complete) {
throw new Error("The image has not finished loading.");
}

// Create a canvas element
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

// Set canvas dimensions to match image dimensions
canvas.width = imgElement.naturalWidth;
canvas.height = imgElement.naturalHeight;

// Draw the image onto the canvas
ctx.drawImage(imgElement, 0, 0);

// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;

// Object to store color hexes with their pixel count
const colorHexes = {};

// Loop through every pixel and extract the color
for (let i = 0; i < data.length; i += 4) {
// Convert RGBA to HEX
let hex =
"#" +
("0" + data[i].toString(16)).slice(-2) + // R
("0" + data[i + 1].toString(16)).slice(-2) + // G
("0" + data[i + 2].toString(16)).slice(-2); // B

// If this color is not yet in the object, add it with a count of 1
// Otherwise, increment the count
if (!colorHexes.hasOwnProperty(hex)) {
colorHexes[hex] = 1;
} else {
colorHexes[hex]++;
}
}

return colorHexes;
}
Insert cell
hexesByPixelCount = getAllColorHexesFromImage(palette)
Insert cell
allHexesThatMeetThreshold = Object.keys(
Object.fromEntries(
Object.entries(hexesByPixelCount).filter(([hex, count]) => count > 1000)
)
)
Insert cell
{
if (Object.keys(allHexesThatMeetThreshold).length != NAMES.length) {
throw new Error("Assertion failure: mismatched lengths");
}
}
Insert cell
accordColorMap = {
const acc = {};
for (let i = 0; i < NAMES.length; ++i) {
acc[NAMES[i]] = allHexesThatMeetThreshold[i];
}

acc["sweet"] = "#D63F3C"; // absurd bug override, no idea how it failed to map properly
return acc;
}
Insert cell
COLOR_DOMAIN = Object.keys(accordColorMap)
Insert cell
COLOR_RANGE = Object.values(accordColorMap)
Insert cell
Insert cell
spans = Object.entries(accordColorMap).map(
([accord, hex]) =>
htl.html`<span style="background-color:${hex}; padding: 5px; display: block; width: 400px; text-align: center; vertical-align: middle;">${accord}</span>`
)
Insert cell
<div>
${spans}
</div>
Insert cell
Insert cell
Insert cell
Insert cell
perfume_data_combined.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
parsedPerfumes = perfume_data_combined1.map((d) => ({
...d,
mainAccords: JSON.parse(d["main accords"].replaceAll("'", '"'))
}))
Insert cell
accordDropoffs = {
const acc = [];
for (const perfume of parsedPerfumes) {
let i = 0;
for (const [accord, amount] of Object.entries(perfume.mainAccords)) {
acc.push({ perfume, name: perfume.name, accord, amount, i });
i++;
}
}

return acc;
}
Insert cell
Plot.plot({
color: {
unknown: "#DDD",
range: COLOR_RANGE,
domain: COLOR_DOMAIN
},
marks: [
Plot.line(accordDropoffs, {
x: "i",
stroke: "accord",
y: "amount",
z: "name",
opacity: 0.13
}),
Plot.boxY(accordDropoffs, { x: "i", y: "amount", opacity: 0.8 })
]
})
Insert cell
INDEX_AMOUNTS = {
const acc = [];
for (const i of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) {
acc.push(
_.mean(accordDropoffs.filter((a) => a.i == i).map((a) => a.amount))
);
}

return acc;
}
Insert cell
Insert cell
allAccords = [...new Set(parsedBigPerfumes.flatMap((d) => d.mainAccords))]
Insert cell
parsedBigPerfumesWithVectors = parsedBigPerfumes.map((d) => {
const vectorAccords = [];
for (const accord of allAccords) {
let i = -1;

if (d.mainAccords != null) {
i = d.mainAccords.indexOf(accord);
}

vectorAccords.push(INDEX_AMOUNTS[i] ?? 0);
}

return {
...d,
vectorAccords
};
})
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more