Public
Edited
Feb 15, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
uiStyles = `
.welow-table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
}
.welow-table thead {}
.welow-table tbody {
max-height: 15em;
overflow: auto;
}
.welow-table tr td,
.welow-table tr th {
padding: 0.8em;
border: 2px solid #C3CFD9;
font-size: 1rem;
}
.welow-table thead tr th {
text-align: center;
}
`
Insert cell
searchResultsView = renderProductView(selectedProduct)
Insert cell
searchResults = searchProducts(terms)
Insert cell
testResults = searchProducts("pomme")
Insert cell
downloadButton = newDownloadButton(matchingOffAgribalyse, "matching-off-argibalyse.json")
Insert cell
Insert cell
Insert cell
formatColumns("Hello", "Wonderful", "World")
Insert cell
function formatColumns(...cells) {
const [first, ...others] = cells;
return htl.html`<div style="display: flex; margin-top: 1em; justify-content: center; gap: 1em;">
<div>${first}</div>
<div style="margin-left: auto;">${others}</div>
</div>`;
}
Insert cell
prepareIngredientsView(testResults[0].ingredients)
Insert cell
function prepareIngredientsView(ingredients, maxIngredientsNumber = 5) {
const compareIngredients = (a, b) => {
return b.ingredientFootprintCO2 - a.ingredientFootprintCO2;
};
ingredients = ingredients.map((d) => ({
...d,
ingredientFootprintCO2: +d.ingredientFootprintCO2
}));
ingredients.sort(compareIngredients);
let filteredIngredients = ingredients.slice(0, maxIngredientsNumber);
if (ingredients.length > maxIngredientsNumber) {
const lastIngredient = {
off_id: null,
off_name: "Others",
agb_ciqual_id: null,
agb_name: null,
perKgIngredientFootprintCO2: 0,
percent: 0,
quantity: 0,
ingredientFootprintCO2: 0
};
filteredIngredients[maxIngredientsNumber - 1] = lastIngredient;
for (let i = maxIngredientsNumber - 1; i < ingredients.length; i++) {
const currentIngredient = ingredients[i];
(lastIngredient.perKgIngredientFootprintCO2 += +currentIngredient.perKgIngredientFootprintCO2 || 0),
(lastIngredient.percent += +lastIngredient.percent || 0);
lastIngredient.quantity += +lastIngredient.quantity || 0;
lastIngredient.ingredientFootprintCO2 += +lastIngredient.ingredientFootprintCO2 || 0;
lastIngredient.quantity += +lastIngredient.quantity || 0;
}
}
return filteredIngredients;
}
Insert cell
function renderDonutLegends (productInfo) {
const { code, quantity, name, footprintCO2, productFootprintCO2Off } =
productInfo;

const ingredients=prepareIngredientsView(productInfo.ingredients)
const formatNumber=(number)=>{
const formatedNumber= new Intl.NumberFormat("fr-FR", {
maximumSignificantDigits: 2
}).format((number)*100);
return `${formatedNumber} g`
}

const ingredientName=(ingredient)=>ingredient.off_name?ingredient.off_name:ingredient.off_id


const content=htl.html`<div></div>`
const wrapper=htl.html`<div><style>
.square {
height: 1em;
width: 1em;
}
</style>
${content}</div>`;
const legend=ingredients.forEach((ingredient,index)=>{
content.appendChild(htl.html`<div style="
display: flex;
gap:1em;
flex-direction: row;
align-items: center;
">
<div class='square' style='background-color:${donutColors[index]}'></div>
<div>${ingredientName(ingredient)}</div>
<div style="margin-left:auto">${formatNumber(ingredient.ingredientFootprintCO2)}</div>
</div>`)
})
return wrapper
}
Insert cell
function renderProductView(productInfo) {
if (!productInfo) return htl.html`<em>Please search a product before</em>`;
const { code, quantity, name, productFootprintCO2, productFootprintCO2Off, ingredients } =
productInfo;
const ingredientsTable = Inputs.table(ingredients,{
header:{
off_id: "id",
off_name: "off name",
agb_ciqual_id: "id agribalyse",
agb_name: "off name",
perKgIngredientFootprintCO2: "co2e per kg",
percent: "%",
quantity: "mass (g)",
ingredientFootprintCO2: "co2e ingredient"
}
});

const formatNumber = (number) => {
const formatedNumber = new Intl.NumberFormat("fr-FR", {
maximumSignificantDigits: 2
}).format(number * 100);
return `${formatedNumber} g`;
};

const width = 400;
const height = 300;
const donutData = prepareIngredientsView(ingredients);
console.log({ donutData });
const co2DonutChart = DonutChart(
donutData, //.filter((d) => d.ingredientFootprintCO2 > 0),
{
name: (d) => d.off_id,
value: (d) => d.perKgIngredientFootprintCO2,
width,
title: (d) => "",
colors: donutColors,
height
}
);

const co2DonutLegends = renderDonutLegends(selectedProduct);
const bigNumber = formatNumber(productFootprintCO2);
return htl.html`<div>
<h3>[${code}] ${name}</h3>
<ul>
<li>Quantity: ${quantity} g</li>
<li>Footprint CO<sub>2</sub>e: ${formatNumber(productFootprintCO2)}</li>
<li>Footprint CO<sub>2</sub>e in OpenFoodFact: ${formatNumber(
productFootprintCO2Off
)} </li>
</ul>
${ingredientsTable}
<div style="display: flex;
justify-content: center;
align-items: center;">
<div style="display: flex;margin-top: 1em; justify-content: center; align-items: center; height:${height}">
<div>
${co2DonutChart}
</div>
<div style="display: flex; position: absolute; font-weight: bold; font-size: 3em;">
${bigNumber}
</div>
</div>
${co2DonutLegends}
</div>
</div>`;
}
Insert cell
Insert cell
function renderResultsTable(products) {
let selectedProduct = products[0];
const numberFormatter = new Intl.NumberFormat("fr-FR", {
maximumSignificantDigits: 2
});
const formatNumber = (v) =>
htl.html`<div style="text-align: right;">${numberFormatter.format(
+v || 0
)}</div>`;
const formatRow = (d) => {
let view;
const onClick = (e) => {
e.preventDefault();
e.stopPropagation();
selectedProduct = d;
e.target.dispatchEvent(
new CustomEvent("input", {
bubbles: true
})
);
};

const code = htl.html`<span>${d.code}</span>`;
const name = htl.html`<a href="#" onClick=${onClick}>${d.name}</a>`;
const quantity = formatNumber(d.quantity);
const productFootprintCO2 = formatNumber(d.productFootprintCO2);
const perKgFootprintCO2 = formatNumber(d.perKgFootprintCO2);
const productFootprintCO2Off = formatNumber(d.productFootprintCO2Off);
const perKgproductFootprintCO2Off = formatNumber(
d.perKgproductFootprintCO2Off
);

view = htl.html`<tr>
<td>${code}</td>
<td>${name}</td>
<td>${quantity}</td>
<td>${productFootprintCO2}</td>
<td>${perKgFootprintCO2}</td>
<td>${productFootprintCO2Off}</td>
<td>${perKgproductFootprintCO2Off}</td>
</tr>`;
return view;
};
const rows = products.map(formatRow);
const table = htl.html`<table class="welow-table">
<thead>
<tr>
<th rowspan="2">ID</th>
<th rowspan="2">Product Name</th>
<th rowspan="2">Quantity (g)</th>
<th colspan="2">CO<sub>2</sub>e Footprint<br/>Greenly</th>
<th colspan="2">CO<sub>2</sub>e Footprint<br/>Open Food Facts</th>
</tr>
<tr>
<th>Per Product</th>
<th>Per Kg</th>
<th>Per Product</th>
<th>Per Kg</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>`;
Object.defineProperty(table, "value", {
get: () => selectedProduct
});
return table;
// return Inputs.table(products);
}
Insert cell
Insert cell
function loadProductInfo(product) {
const ingredients = [];
let {
code,
product_name,
abbreviated_product_name_fr,
product_quantity = 1000,
carbon_footprint_percent_of_known_ingredients,
ecoscore_data = {}
} = product;
if (!product_name) product_name = abbreviated_product_name_fr; //console.log("--", product);
if (!code) return null;

const co2data = ecoscore_data.agribalyse || {};

const productAgribalyseInfo = getAgribalyseInfo();
let productFootprintCO2 = 0;
for (let ingredient of product.ingredients || []) {
let { id, percent_estimate, percent_max, percent_min } = ingredient;
const percent = percent_estimate || 0;
const {
off_name,
agb_ciqual_id,
agb_name,
agb_score_co2 = 0
} = getAgribalyseInfo(id);
const ingredientQuantity = (percent * product_quantity) / 100;
const perKgIngredientFootprintCO2 = agb_score_co2;
const ingredientFootprintCO2 = (perKgIngredientFootprintCO2 * ingredientQuantity) / 1000;
productFootprintCO2 += ingredientFootprintCO2;
const ingredientInfo = {
off_id: id,
off_name,
agb_ciqual_id,
agb_name,
percent,
quantity: ingredientQuantity,
ingredientFootprintCO2,
perKgIngredientFootprintCO2,
};
ingredients.push(ingredientInfo);
}

// console.log(">>", co2data, co2data.co2_total);
const productFootprintCO2Off=co2data.co2_total*product_quantity/1000;
const perKgFootprintCO2=productFootprintCO2*1000/product_quantity;
return {
code,
quantity: product_quantity,
name: product_name,
productFootprintCO2,
perKgFootprintCO2,
productFootprintCO2Off,
perKgproductFootprintCO2Off: co2data.co2_total,
ingredients
};
function getAgribalyseInfo(id) {
return indexOffAgribalyse[id] || {};
}
}
Insert cell
Insert cell
Insert cell
Insert cell
// function buildProductTable(data) {
// return Inputs.table(data, {
// columns: [
// "product_name",
// "brands",
// "co2_total",
// "co2_agriculture",
// "co2_consumption",
// "co2_distribution",
// "co2_packaging",
// "co2_processing",
// "co2_transportation"
// ],
// width: {
// product_name: "15em"
// },
// header: {
// product_name: "Name",
// brands: "Brand",
// co2_agriculture: "Agri",
// co2_consumption: "Consum",
// co2_distribution: "Distrib",
// co2_packaging: "Pack",
// co2_processing: "Process",
// co2_transportation: "Transport",
// co2_total: "Total"
// },
// format: {
// product_name: (name, idx, data) => {
// const url = data[idx].url;
// return htl.html`<a href="${url}" target="_blank">${name}</a>`;
// }
// // image: (url) => {
// // const img = new Image();
// // img.crossOrigin = "Anonymous";
// // img.src = url;
// // return img;
// // }
// }
// });
// }
Insert cell
Insert cell
Insert cell
Insert cell
$application = ({
template: `
<style>
@import url('https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css');
@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
</style>
<div style="margin:2em 10%;width: auto;max-width: 2000px;">
<img src="https://greenly.cdn.prismic.io/greenly/0b1b1cf4-95f3-46de-82c9-7a2fa2f14584_Logo.svg" style="height: 2em"/>
<div data-cell="viewof terms"></div>
<hr/>
<div data-cell="viewof selectedProduct"></div>
<div data-cell="searchResultsView"></div>
</div>
<style>
body {
font-family: 'Inter', sans-serif;
}
form.oi-ec050e {
width : auto;
}
.map-buttons button {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
a {
text-decoration: none;
color: #6557F5;
}
${uiStyles}
</style>
`
})
Insert cell
// applyTemplate($application.template, {
// "viewof terms": viewof terms,
// "viewof selectedProduct": viewof selectedProduct,
// searchResultsView: searchResultsView
// })
Insert cell
function applyTemplate(template, mapping = {}) {
const div = htl.html({ raw: [template] });
for (let [key, value] of Object.entries(mapping)) {
const elm = div.querySelector(`[data-cell="${key}"]`);
if (!elm) {
console.warn(`Cell [data-cell="${key}"] was not found`);
continue;
}
const node = toDomNode(value);
elm.appendChild(node);
}
return div;
function toDomNode(value, doc = document, containerElm = "span") {
if (value === undefined) return;
if (value === null) value = "";
if (typeof value === "number" || typeof value === "boolean")
value = String(value);
if (typeof value === "object" && !(value instanceof Node)) {
if (value !== null && value[Symbol.iterator]) {
const elm = doc.createElement(containerElm);
for (let val of value) {
elm.appendChild(toDomNode(val, doc));
}
value = elm;
} else {
value = Object.prototype.toString.call(value);
}
}
if (typeof value === "string") value = doc.createTextNode(value);
return value;
}
}
Insert cell
Insert cell
import { DonutChart } from "@d3/donut-chart"
Insert cell
async function newDownloadButton(data, filename = "export.json") {
if (!data) throw new Error("Array of data required as first argument");
const json = JSON.stringify(data, null, 2);
let downloadData = new Blob([json], { type: "text/json" });
const size = (downloadData.size / 1024).toFixed(0);
const button = DOM.download(
downloadData,
filename,
`Download ${filename} (~${size} KB)`
);
return button;
}
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