Published
Edited
Mar 3, 2022
2 forks
1 star
Insert cell
# Sunburst, Circle packed chart

The sunburst chart is used along with hierarchicaly typed data.

## Exploration of Data and Preparation
We prepare our data is first so to get a hierarchy consisting of
> GROUP->SUBGROUPS->FOODS_OF_SUBGROUP.

## Partie Sunburst
Insert cell
fooDB
Insert cell
fooDB_group = d3.group(fooDB,
d => d.food_group.toLowerCase(),
d => d.food_subgroup.toLowerCase(),
// d => d.name
);
Insert cell
fooDB_rollup = d3.rollup(fooDB,
v => d3.count(v, d => d),
v => Array.from(v, d=>d.name) ,
d => d.food_group.toLowerCase(),
d => d.food_subgroup.toLowerCase()
)
Insert cell
hierarchy1 = d3.hierarchy([null, fooDB_group]).count(([, value]) => value).sort((a,b)=> b.value - a.value)
Insert cell
d3.map(hierarchy.data, (d,i)=> i==0 ? "All" : d) //not useful for children
Insert cell
hierarchy = d3.hierarchy(fooDB_group).count().sort((a, b) => d3.descending(a.value, b.value))
Insert cell
hierarchy.leaves().map(d => d.data)
Insert cell
data_hierarchy = d3.hierarchy(data)
Insert cell
group_el0 = Object.entries(data_hierarchy.data)[0][1]
Insert cell
subgroup_el0 = Object.entries(group_el0)[0]
Insert cell
foods_el0 =subgroup_el0[1]
Insert cell
foods_el0.length
Insert cell
chart = {
}
Insert cell
Insert cell
Insert cell
### Exploration rapide
Ayant une hiérarchie donnée, la méthode de navigation de la structure est flexible, nous pouvons par exemple penser a une approche *top down*; racine vers feuilles, ou inversement; en *bottom up*.
Notre approche sera une approche top down. Nous utiliserons l'attribut *children* pour naviguer la hiérarchie notamment pour la concaténation des éléments au DOM, chaque nœud porte l'information de ses coordonnées *(x,y)* et de son rayon *r*.

L'hauteur de la racine de notre hiérarchie est de 3.
Notre racine retourne un *undefined*, ses enfants directes; un groupe nutritif, dont les descendants sont les différents aliments.
Insert cell
explo = { return {
'root&children':[hierarchy2bis.data, hierarchy2bis.children],
'1stchild&children':[hierarchy2bis.children[0].data[0], hierarchy2bis.children[0].children],
'1stgrdchild&children':[hierarchy2bis.children[0].children[0].data[0], hierarchy2bis.children[0].children[0].children],
'1stgedchASleaf':[hierarchy2bis.leaves()[0].data[1]]
}}
Insert cell
hierarchy2bis
Insert cell
Insert cell
function cp_chart (data, {
children,
space = 300,
width = 500+space,
height = 500+(space/2),
fill = "white",
fillOpacity = .9,
stroke = "white",
padding = 3,
vb_pad = 10,
strokeWidth,
strokeOpacity,
} = {}) {
if(!(data instanceof d3.hierarchy))
throw 'Pump the jam'

const W = width - width/10
const H = height- height/10
const groupes = d3.map(data.children, d => d.data[0])
const color = d3.scaleOrdinal().domain(Array.from(groupes)).range(d3.schemeTableau10)
// Compute the layout.
function pack_data2() {
return d3.pack()
.size([W, H])
.padding(padding) // inter-cercles
(data);
}
const packed = pack_data2();
// Compute labels and titles.
const descendants = packed.descendants();
const feuilles = packed.leaves();
feuilles.forEach((d,i) => d.index = i);
let title = (d) => d.height == 0 ?
`${d.parent.data[0]}\n${[...d.data[0].split(',')]}\n`
: `${d.data[0]}\n` ;
// DOM
const svg = d3.create("svg")
.attr("viewBox", [-1, -vb_pad*2, width, height])
.attr('width', width)
.attr('height', height)
.attr('font-size', 10)
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.style('border', '1px solid black')
.style('stroke', 'black')
.style("fill", "none")
.style("stroke-width", '.03px');
const leaf = svg.selectAll("g")
.data(descendants).enter()
.append("g")
.attr("transform", d => `translate(${d.x},${d.y})`);
const circle = leaf.append("circle")
.attr("r", d => d.r)
.attr("fill", d => d.children ? d.height == 1 ? null : 'black' : fill)
.attr("fill-opacity", d => d.children ? null : fillOpacity)
.attr("stroke", d => d.height == 1 ? color(d.data[0]) : null)
.attr("stroke-width", d => d.height == 1 ? `${d3.max([4, padding-3])}px` : null)
.on("click", handleClick)
// title
.append("title")
.text(d => d.height < 2 ? title(d) : null)

// clef
const clef = svg.append("clef").attr("transform", `translate(${0},10)`)
svg.selectAll("text.clefs")
.data(groupes).enter()
.append("text")
.attr("class", "clefs")
.attr("font-size", "10px")
.attr("fill", "black")
.attr("x", width - 85)
.attr("y", (d, i) => i * 25 + 20)
.attr("text-align", 'left')
.text(d => d)
svg.selectAll("rect").data(groupes)
.enter()
.append("rect")
.attr("x", width - 105)
.attr("y", (d, i) => i * 25 + 25)
.attr("width", 40)
.attr("height", 10)
.attr("fill", d => color(d))
.attr("stroke", "black")
.attr("stroke-width", '.15px')
.attr("transform", `translate(0,-25)`)

return svg.node()
}
Insert cell
cp_chart(hierarchy2bis)
Insert cell
function handleClick(event) {
let arr = []
console.log(event)
let clicked_node = event.target.__data__
console.log(clicked_node.data[0])
let parent = clicked_node.parent
console.log(parent.data[0])
}
Insert cell
## Imported data for chart study
Insert cell
# Test of existing circle Pack
Insert cell
dataroll2_sel
Insert cell
Insert cell
packet_chart = Pack(hierarchy2bis, {
title: (d, n) => `${n.ancestors().reverse().map(({data: d}) => d.name).join(".")}\n${n.value.toString()}`,
width: 1000,
height: 1000,
padding: 1
})
Insert cell
import {Pack} from "@d3/pack"
Insert cell
# Data
## Data 1
Insert cell
// Needs cleaning, or case sensitivity treatment
data_fooDB = {
let group_map = {};
fooDB.forEach( food =>{
var group = food.food_group
var subgroup = food.food_subgroup
if(! group_map[group])
group_map[group] = {}

if(!group_map[group][subgroup])
group_map[group][subgroup] = []

group_map[group][subgroup].push(food.name)
});
return group_map;
}
Insert cell
data = Array.from(Object.entries(data_fooDB))
Insert cell
fooDB = food_base.map( object =>
(({ id, name, name_scientific, description, food_group, food_subgroup, food_type, category}) =>
({ id, name, name_scientific, description, food_group, food_subgroup, food_type, category}))(object)
)
Insert cell
value = ([, value]) => value
Insert cell
d3.map(data, d => value(d))
Insert cell
food_base = FileAttachment("Food.csv").csv()
Insert cell
## Data 2
### Set up
Le jeu de données **food_h_parsed** contient énormément de descriptions, **cols_det** est un objet pour simplifier la logique de la sélection des données à extraire.
Ce jeu de données contient des descriptions sous la colonne *"name"*, pour concorder avec la partie AQR, l'objet **aqr_gObj** contient le nom du groupe alimentaire décrit dans la section de rapports nutritifs, et une petite sélection manuelle de noms qui tombent sous cette catégorie.
Une bonne préparation de ces données est malheureusement *time consuming*.
Insert cell
Insert cell
cols_det = new Object({
identification : ['name', 'serving_size'],
vitamins : ['vitamin_a', 'vitamin_a_rae','vitamin_b12', 'vitamin_b6',
'vitamin_c', 'vitamin_d', 'vitamin_e','vitamin_k'
],
main : ['total_fat',
'saturated_fat',
'sugars',
'calories',
'fiber',
'carbohydrate',
'protein',
'calcium',
], // How many on the screen first
more : ['irom', 'magnesium', 'sodium', 'water', 'caffeine', 'alcohol', 'cholesterol', 'lactose']
})
Insert cell
aqr_gObj = new Object({
"Conserve" : ['canned'],
"Fruits et légumes" : ['Fruit', 'tomatoes', 'cherries', 'vegetable', 'berries', 'berry', 'grapes', 'eggplant',
'broccoli', 'pepper', 'cucumber', 'mushroom', 'seaweed', 'celery', 'chicory', 'pear',
'carrots', 'onion', 'peas', 'asparagus', 'raddish', 'apricot', 'celeriac', 'celtuce'],
"Fruits à coque": ['Nuts', 'quinoa'],
"Légumineuses": ['lentils', 'beans', ],
"Produits céréaliers": ['biscuit', 'pasta', 'bread', 'wheat', 'tapioca', 'cous ncous', 'pretzel'],
"Produits laitiers": ['milk', 'cheese', 'whey'],
"Viande et volaille": ['meat', 'beef', 'ostrich', 'lamb', 'turkey', 'egg', 'chicken', 'veal', 'pork'],
"Poisson et fruits de mer ": ['fish', 'salami'],
"Charcuterie": ['bacon', 'ham', 'sausage'],
"Matières grasses ajoutées ": ['oil', 'fat'],
"Produits sucrés": ['candies', 'sugar', 'syrup', 'honey', 'ice cream'],
"Boissons": ['water', 'juice', 'nectar', 'drink'],
"Sel et epices": ['salt', 'spice', 'cornstarch', 'sauce', 'flour'],
"Fast food" : ['mcdo', 'keebler', 'kfc', 'denny', 'taco bell', 'fast food']
})
Insert cell
Nous nous assurons qu'aucune valeur n'est *undefined*, puis nous utilisons une fonction de rollup pour retourner en quelque sortes notre base de données, divisée en map de 2 valeurs; ***True*** pour les objets sous-contenant dans leur attribut *"name"* le nom provenant de la liste ci-dessus *(aqr_gObj)*
Insert cell
//Sanity check; all values are defined.
d3.map(food_h_parsed, d => Object.values(d).every(x => typeof(x) !== 'undefined')).filter(n => n == false)
Insert cell
rollup_fn = (base, str) =>
d3.rollup(base,
d => d,
d => d.name.toLowerCase().includes(str.toLowerCase())
)
Insert cell
Insert cell
Insert cell
food_h_adapt2[1]
Insert cell
Insert cell
Insert cell
### Group & Rollup
Et grâce à ce dernier objet,
Insert cell
bd2_group = d3.group(bd_adapt2, d => d.group, d => d.name)
Insert cell
bd2_rollup = d3.rollup(bd_adapt2, v => {
let map = new Map()
let apport_total = 0
for(let nutr of cols_det.main){
map.set(nutr, v[0][nutr])
if(nutr != 'calories' && nutr != "saturated_fat")
apport_total += v[0][nutr]
}
return new Map().set(map, apport_total.toFixed(2))
}, d => d.group,
d => d.name)
Insert cell
Je trouve le premier rollup intéressant, mais je questionne son utilité, nous obtenons juste en dessous, notre version tenant en paramètre d'entrée, le nutriment que l'on aimerai explorer.
Insert cell
nutr_selection
Insert cell
dataroll2_sel = bd2_rollup_sel(nutr_selection)
Insert cell
Insert cell
### Hiérarchies
[
Notre nouvelle Map contient d'abord le nom du groupe, ensuite celui de l'aliment, puis une map descriptive de chacun des nuriments de ce dernier, et enfin une addition des differents apports de cet aliments; ceci est une mesure entrant dans la cadre de la réalisation d'une visualisation, plus de recherches seraient requises pour trouver une fonction plus pertinante.
Celà dit, le total de l'apport contenu par un aliment représentera pour nous la valeur richesse nutritive d'une portion de 100 grammes d'un aliment donné, nous ne pouvons pas en induire si l'aliment est riche en un unique nutriment ou dans la diversité qu'il offre.
Cette valeur devra être modifiable par la séléction de filtres.
]
Insert cell
hierarchy2 = d3.hierarchy(bd2_rollup)
.sum(([,value]) => value)
.sort((a, b) => b.value - a.value)
Insert cell
hierarchy2bis = d3.hierarchy(dataroll2_sel)
.sum(([,value]) => value)
.sort((a, b) => b.value - a.value)
Insert cell
# If you're learning
> [D3 Intro](https://observablehq.com/@d3/learn-d3).

> Good *(and official)* explanation of [d3.hierarchy](https://observablehq.com/@d3/d3-group-d3-hierarchy). *stamped*
> Exploration of [hierarchies](https://observablehq.com/@d3/visiting-a-d3-hierarchy).
> Data manipulation; [groups, rollups & hierarchy](https://observablehq.com/@stopyransky/making-hierarchy-from-any-tabular-data) *here*

> [d3 path](https://observablehq.com/@d3/d3-path)

> Sunburst : [interaction](https://observablehq.com/@shuaihaofzny/sunburst-map-of-nyc-crime-data-in-2018)! And [animation](https://observablehq.com/@kerryrodden/sequences-sunburst).
> Official Observable on [Circle Packing](https://observablehq.com/@d3/pack) and [Circle Packing with rollup](https://observablehq.com/@d3/pack-rollup)
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