Unlisted
Edited
Nov 15, 2020
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
stackview = stacks(suggested_trees, { shift: 3, h: 20, w: 24 })
Insert cell
Insert cell
{
d3.select(ui)
.select("#presets")
.html("")
.node()
.appendChild(stackview);
yield md`<tt>inserted presets at ${Date.now()}</tt>`;
}
Insert cell
Insert cell
Insert cell
Insert cell
import {sequence, animate} with {objects, path, sequence_width as width, sequence_height} from "881c12067aa169ee"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
objects = {
// Levenshtein objects can be declared or not declared as attributes…
const att = [...attributes]
.map(d => ({ cluster: d[0] }))
.concat(suggested_preset.tree)
.concat(tree.tree);

const uniq = d3.scaleOrdinal().range(d3.range(100));

// attributes will be taken by priority: from the tree, then suggested tree, then default
return new Map(
att.map(d => [
String.fromCharCode(4365 + uniq(d.group || d.cluster || "?")),
d
])
);
}
Insert cell
Insert cell
Insert cell
function stacks(suggested_trees, options = { shift: 4 }) {
const w = options.w || 80,
h = options.h || 50;

const s = DOM.svg(
20 + suggested_trees.length * (w * 1.25),
h + options.shift * 8 + 20
);

d3.select(s).append("style").text(`
.stack { cursor: pointer; }
.preset g g:last-of-type rect {fill: #eee;}
.hover g g:last-of-type rect {fill: ${d3.rgb("lightblue").darker(-1) + ""}}
.leaf rect {fill: lightblue !important;}
`);

const pack = d3
.select(s)
.selectAll("g")
.data(suggested_trees)
.enter()
.append("g")
.classed("stack", true)
.style("shape-rendering", "crispEdges")
.attr("transform", (_, i) => `translate(${[20 + w * 1.25 * i, 3]})`)
.classed("preset", (_, i) => i === +mutable tree_option)
.on("click", (_, i) => {
delete s.value.temp;
mutable tree_option = i;
pack.classed("preset", (_, i) => i === +mutable tree_option);
pack.classed("hover", false);
})
.on("mouseover", (_, i, e) => {
d3.select(e[i]).classed("hover", true);
if (s.value.temp != i) {
s.value.temp = i;
s.dispatchEvent(new CustomEvent("input"));
}
})
.on("mouseout", () => {
pack.classed("hover", false);
delete s.value.temp;
// s.value.leaf = +tree_option;
s.dispatchEvent(new CustomEvent("input"));
});

pack.append("g").classed("layers", true);

pack
.append("text")
.attr("pointer-events", "none")
.attr("x", w / 2)
.attr("y", 5 + h / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-weight", "bold")
.style("font-size", `${w * (28 / 80)}px`)
.style("font-family", "sans-serif")
.text((d, i) => "#" + i);

s.value = { leaf: 0, stack: 0 };

return s;
}
Insert cell
{
const options = { shift: 3, h: 20, w: 24 },
w = options.w,
h = options.h,
s = stackview,
pack = d3.select(s).selectAll(".layers");

const layers = pack
.attr(
"transform",
(_, i) => `translate(${[0, paths[i].length * options.shift]})`
)
.html("")
.selectAll("g")
.data((_, i) =>
paths[i]
.slice()
.reverse()
.map((d, j) => ({ i, d }))
)
.enter()
.append("g")
.attr(
"transform",
(_, i) => `translate(${[2 + i * 0, 2 - i * options.shift]})`
);
// .attr("pointer-events", "none");
// setTimeout(function() {
layers
.on("mouseover", (_, i, e) => {
d3.selectAll(".leaf").classed("leaf", false);
d3.select(e[i]).classed("leaf", true);
})
.on("mouseout", () => {
d3.selectAll(".leaf").classed("leaf", false);
});
//}, 100);

layers
.append("rect")
.attr("width", w)
.attr("height", h)
.attr("stroke", "black")
.attr("stroke-width", "1")
.attr("fill", "white");
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
tidy = {
const source = selected.data,
tidy = [];
for (let i = 0; i < source.length; i++) {
const d = source[i];
d.index = i;
if (d.x1 === undefined) {
const p1 = projection([d.start_longitude, d.start_latitude]) || [
Math.random(),
Math.random()
],
p2 = projection([d.end_longitude, d.end_latitude]) || [
Math.random(),
Math.random()
];
d.x1 = p1[0];
d.y1 = p1[1];
d.x2 = p2[0];
d.y2 = p2[1];

// fix bounds (all d3 projections are in [960, 500]).
for (const o of ["x1", "x2"]) d[o] = Math.max(-10, Math.min(970, d[o]));
for (const o of ["y1", "y2"]) d[o] = Math.max(-10, Math.min(510, d[o]));
}
tidy.push(d);
}

// Compute ckmeans thresholds for quantitative clusters
tidy.thresholds = {};

od.raw.r.attributes
.filter(attr => attr.type === "quantitative")
.forEach(attr => {
const values = tidy
.map(d => d[attr.name])
.filter(d => Math.abs(d) < +Infinity), // remove Infinity
extent = d3.extent(values),
sample = d3.shuffle(values).slice(0, 1000); // ckmeans will not consume too many values

const thresholds = sample.length
? simple
.ckmeans(sample, Math.min(values.length, clusterlen))
.map(v => d3.max(v))
: [];
tidy.thresholds[attr.name] = [extent[0], ...thresholds, extent[1]];

tidy.forEach(d => {
d["cluster:" + attr.name] = d3.scan(
thresholds.map(t => d[attr.name] >= t)
);
});
});

// x/y extent for the map projection
const xExtent = d3.extent(tidy.flatMap(d => [d.x1, d.x2])),
yExtent = d3.extent(tidy.flatMap(d => [d.y1, d.y2]));

if ((!xExtent[0] && !xExtent[1]) || (!yExtent[0] && !yExtent[1])) {
mutable ERROR = md`# Error: No O/D coordinates found.`;
} else {
mutable ERROR = md``;
}

tidy.extent = [[xExtent[0], yExtent[0]], [xExtent[1], yExtent[1]]];

return tidy;
}
Insert cell
attributes = {
const m = new Map(
od.raw.r.attributes.map(d => [
d.name,
Object.assign({}, d, { extent: d3.extent(tidy, e => e[d.name] || NaN) })
])
);
m.set("selected", { name: "selected", type: "categorical", extent: [0, 1] });
return m;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
/* adds groupby and gridby functions to the json structure */
function addFunctions(json) {
return {
width: json.width || width,
height: json.height || json.width || width,
tree: json.tree.map(t => {
const margin = t.margin || 0;
const padding = t.padding || 0;

if (t.cluster) t.group = "cluster:" + t.cluster;

// todo try...catch
if (t.group.match(/=>/) || t.group.match(/function[(]/))
t.group = eval("(" + t.group + ")");

const mode = t.gridding || "grid",
gridby = (g, root, i) => {
const gridder = d3
.gridding()
.mode(mode)
.size([g.width, g.height])
.margin(margin)
.padding(padding);

ops(mode).forEach(op => {
const attr = t[op],
aggregation =
t[op + "_aggregation"] || "mean" /* "mean" or "sum" ? */;
if (attr) {
const stats = get_stats(root, aggregation + "_" + attr, i + 1),
extent = d3.extent(stats);
if (extent[0] !== undefined) {
const scale = d3.scaleLinear().domain(extent);
gridder[op](d => scale(d[aggregation + "_" + attr]));
}
}
});

return gridder;
},
groupby = t.group
? typeof t.group === "function"
? t.group
: d => d[t.group]
: d => 0;

return Object.assign({}, t, {
groupby,
gridby
});
})
};
}
Insert cell
function get_gridded(structure) {
structure = addFunctions(structure);

function recurse_group(l, g) {
const groups = [...d3.group(g, structure.tree[l].groupby)];

for (const h of groups) {
if (l === structure.tree.length - 1) {
h.nodes = h[1];
} else {
h[1] = recurse_group(l + 1, h[1]);
h.nodes = d3.merge(h[1].map(d => d.nodes));
}

// aggregates
h.count = h.nodes.length;
for (const [name, desc] of attributes) {
if (desc.type === "quantitative") {
h["sum_" + name] = d3.sum(h.nodes, d => d[name]);
h["mean_" + name] = h["sum_" + name] / h.count;
} else {
h["sum_" + name] = h["mean_" + name] = h.count;
}
h["count_" + name] = h.count;
h["distinct_" + name] =
h["count_" + name] === 1 ? 1 : d3.group(h.nodes, d => d[name]).size;
}
}

return groups;
}

function recurse_cells(l, g, root, i) {
const s = structure.tree[l],
sort = s.sort === "desc" ? d3.descending : d3.ascending;

// the key is g[0]
// the contents is g[1]
g[1] = [...g[1]].sort((a, b) => sort(a[0], b[0]));

g[1] = s.gridby(g, root, i)(g[1]);

// copy absolute positions (ax,ay) in each cell
g[1].forEach(d => {
d.ax = (g.ax || 0) + d.x;
d.ay = (g.ay || 0) + d.y;
});

// recurse
if (l + 1 in structure.tree) {
g[1] = g[1].map(g => recurse_cells(l + 1, g, root, i + 1));
}

return g;
}

const groups = recurse_group(0, tidy_filtered);
const g = [{}, groups];
const w = structure.width || width,
h = structure.height || w;
g.width = w;
g.height = h;
const r = recurse_cells(0, g, g, 0)[1];

r.width = w;
r.height = h;
return r;
}
Insert cell
paint = {
const last = tree.tree[tree.tree.length - 1],
attribute = last.cluster || last.group,
attribute_type =
last.cluster && tidy.thresholds[last.cluster]
? { type: "cluster" }
: attributes.get(attribute) || {};

return attribute === "selected"
? {
type: "ordinal",
color: d3.scaleOrdinal(["#666", "#c33"]).domain(["", "selected"])
}
: attribute_type.type === "categorical"
? { type: "ordinal", color: d3.scaleOrdinal(d3.schemeCategory10) }
: attribute_type.type === "quantitative"
? {
type: "linear",
color: d3
.scaleSequential(d3.interpolateCool)
.domain(attribute_type.extent),
domain: attribute_type.extent
}
: attribute_type.type === "cluster"
? {
type: "threshold",
color: d3
.scaleThreshold()
.domain(tidy.thresholds[attribute] || [])
.range(
d3
.range(clusterlen + 1)
.map(t => d3.interpolateWarm(t / clusterlen))
),
domain: [0, clusterlen]
}
: {
type: attribute_type.type + "???",
color: d3.scaleOrdinal(d3.schemePaired),
domain: attribute_type.extent || [0, 1]
};
}
Insert cell
function chartcanvas(structure, context) {
const last = structure.tree[structure.tree.length - 1];
last.gridding = "central";

const gridded = get_gridded(structure);

context = context || DOM.context2d(gridded.width, gridded.height);

d3.select(context.canvas).style("opacity", 0.5);

var minW = gridded.width,
minH = gridded.height,
maxN = 1,
nLayers = 0;

const root = ["root", gridded];
root.width = gridded.width;
root.height = gridded.height;

setTimeout(() => {
d3.select(context.canvas).style("opacity", 1);
context.clearRect(0, 0, root.width, root.height);
domains_recurse(root, 0);
draw(root, 0);
}, 10);
setTimeout(() => {
const panel = panelSVG("left"),
svg = d3.select(panel.svg);
svg.html("");
draw_grid(root, svg);
}, 100);

function domains_recurse(g, depth) {
if (depth < structure.tree.length - 1) {
if (unique_scale) {
for (const d of g[1]) {
minW = Math.min(d.width, minW);
minH = Math.min(d.height, minH);
}
}

for (const d of g[1]) {
domains_recurse(d, depth + 1);
}
} // leaves!
else {
maxN = Math.max(maxN, g.count);
nLayers += g[1].length;
}
}

function draw(g, depth) {
if (depth < structure.tree.length - 1) {
for (const d of g[1]) {
context.save();
context.translate(d.x, d.y);
draw(d, depth + 1);
context.translate(-d.x, -d.y);
context.restore();
}
} // leaves!
else {
// each leaf d is a multiple in a list of small multiples
// "une carte dans une collection de cartes"
// et fontionne en layers, qui correspondent à la dernière étape du partitionnement

// d[0] = key used to group
// d[1] = list of OD elements
// .width,.height, .cx etc: gridding properties
const w = unique_scale ? minW : g.width,
h = unique_scale ? minH : g.height;

const layers = g[1];

// minimap_scatterplot({ w, h, layers, context, last, maxN });
minimap_geo({ w, h, layers, context, last, maxN });
}
}
}
Insert cell
function chartsvg(structure) {
const last = structure.tree[structure.tree.length - 1];
last.gridding = "central";

const gridded = get_gridded(structure);

var minW = gridded.width,
minH = gridded.height,
maxN = 1,
nLayers = 0;

const root = ["root", gridded];
root.width = gridded.width;
root.height = gridded.height;

const s = d3.select(svg`<svg>`);
s.attr("width", root.width).attr("height", root.height);

domains_recurse(root, 0);

draw_grid(root, s.append("g"));

draw_svg(root, 0, s.append("g"));

return s;

function domains_recurse(g, depth) {
if (depth < structure.tree.length - 1) {
if (unique_scale) {
for (const d of g[1]) {
minW = Math.min(d.width, minW);
minH = Math.min(d.height, minH);
}
}

for (const d of g[1]) {
domains_recurse(d, depth + 1);
}
} // leaves!
else {
maxN = Math.max(maxN, g.count);
nLayers += g[1].length;
}
}

function draw_svg(g, depth, container) {
if (depth < structure.tree.length - 1) {
for (const d of g[1]) {
draw_svg(
d,
depth + 1,
container.append("g").attr("transform", `translate(${[d.x, d.y]})`)
);
}
} // leaves!
else {
const w = unique_scale ? minW : g.width,
h = unique_scale ? minH : g.height;
const layers = g[1];
minimap_geo_svg({ w, h, layers, container, last, maxN });
}
}
}
Insert cell
exportsvg = {
return DOM.download(
() => {
const { width, height } = panelContext("left");
tree.width = width;
tree.height = height;
const chart = chartsvg(tree).node();
return serialize(chart);
},
`Gridify ${od.title} ${tree.title} ${tree.tree
.map(d => d.cluster || d.group)
.filter(d => !!d)
.join(",")}.svg`,
"Download SVG"
);
}
Insert cell
function minimap_geo_svg({ w, h, layers, container, last, maxN }) {
// réglage de la projection pour cette carte
const path = d3.geoPath(
d3.geoIdentity().fitExtent([[w * 0.05, h * 0.05], [w * 0.95, h * 0.95]], {
type: "MultiPoint",
coordinates: unique_scale
? tidy.extent
: layers.map(l => l[1].map(d => [[d.x1, d.y1], [d.x2, d.y2]])).flat(2)
})
);

const show = {
link: tree.stroke_width > 0
};

// afficher chaque layer de la carte
if (show.link) {
for (const d of layers) {
container
.append("path")
.attr("transform", `translate(${[d.tx, d.ty]})`)
.style(
"stroke-width",
(tree.stroke_width / 20) * Math.sqrt(maxN / tidy.length)
)
.attr(
"d",
path({
type: "MultiLineString",
coordinates: d[1].map(d => [[d.x1, d.y1], [d.x2, d.y2]])
})
)
.style("stroke", paint.color(d[1][0][last.cluster || last.group]));
}
}

for (const d of layers) {
const g = container
.append("g")
.attr("transform", `translate(${[d.tx, d.ty]})`)
.style("opacity", tree.opacity);

if (tree.start_circle_width > 0) {
path.pointRadius(tree.start_circle_width / Math.sqrt(tidy.length));
g.append("path")
.attr(
"d",
path({
type: "MultiPoint",
coordinates: d[1].map(d => [d.x1, d.y1])
})
)
.style("fill", paint.color(d[1][0][last.cluster || last.group]))
.style("stroke-width", 0.3)
.style("stroke", "black");
}

if (tree.end_circle_width > 0) {
path.pointRadius(tree.end_circle_width / Math.sqrt(tidy.length));
g.append("path")
.attr(
"d",
path({
type: "MultiPoint",
coordinates: d[1].map(d => [d.x2, d.y2])
})
)
.style("fill", paint.color(d[1][0][last.cluster || last.group]))
.style("stroke-width", 0.3)
.style("stroke", "white");
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
function minimap_geo({ w, h, layers, context, last, maxN }) {

// réglage de la projection pour cette carte
const path = d3
.geoPath(
d3.geoIdentity().fitExtent([[w * 0.05, h * 0.05], [w * 0.95, h * 0.95]], {
type: "MultiPoint",
coordinates: unique_scale
? tidy.extent
: layers.map(l => l[1].map(d => [[d.x1, d.y1], [d.x2, d.y2]])).flat(2)
})
)
.context(context);

const show = {
link: tree.stroke_width > 0
};

context.globalAlpha = tree.opacity;

// afficher chaque layer de la carte
if (show.link) {
for (const d of layers) {
context.save();

context.translate(d.tx, d.ty);
context.lineWidth =
(tree.stroke_width / 20) * Math.sqrt(maxN / tidy.length);

// TODO (d[1][0][last.cluster || last.group] * tree.stroke_width_ratio) / Math.sqrt(tidy.length);
context.beginPath();
path({
type: "MultiLineString",
coordinates: d[1].map(d => [[d.x1, d.y1], [d.x2, d.y2]])
});

context.strokeStyle = paint.color(d[1][0][last.cluster || last.group]);
context.stroke();
context.translate(-d.tx, -d.ty);
context.restore();
}
}

for (const d of layers) {
context.save();

context.translate(d.tx, d.ty);

if (tree.start_circle_width > 0) {
path.pointRadius(tree.start_circle_width / Math.sqrt(tidy.length));
context.beginPath();
path({
type: "MultiPoint",
coordinates: d[1].map(d => [d.x1, d.y1])
});
context.fillStyle = paint.color(d[1][0][last.cluster || last.group]);
context.fill();
context.lineWidth = 0.3;
context.strokeStyle = "black";
context.stroke();
}

if (tree.end_circle_width > 0) {
path.pointRadius(tree.end_circle_width); // / Math.sqrt(tidy.length));
context.beginPath();
path({
type: "MultiPoint",
coordinates: d[1].map(d => [d.x2, d.y2])
});
context.fillStyle = paint.color(d[1][0][last.cluster || last.group]);
context.fill();
//context.lineWidth = 0.3;
//context.strokeStyle = "white";
//context.stroke();
}

context.translate(-d.tx, -d.ty);
context.restore();
}

context.globalAlpha = 1;
}
Insert cell
Insert cell
Insert cell
tree_test = ({
title: "test",
tree: [{ group: "group" }, { group: "group" }]
})
Insert cell
Insert cell
import {create_ui, ui_presets, ui_show, start_circle_width, end_circle_width, stroke_width, opacity, select, viewof ui_dataset} with {datasets} from "ce3ffd0a9409d328"
Insert cell
Insert cell
panels = {
ui;

let panel,
width,
height,
panels = {};

panel = d3
.select(ui)
.select(".left_panel")
.html("");
width = panel.node().offsetWidth - 2;
height = panel.node().offsetHeight - 2;
panel
.node()
.appendChild((panels.leftCanvas = DOM.context2d(width, height).canvas));
panel.node().appendChild((panels.leftSVG = DOM.svg(width, height)));
panel.node().appendChild((panels.leftHTML = DOM.element("div")));
panels.leftSVG.style = panels.leftCanvas.style = panels.leftHTML.style = `position:absolute; width:${width}px; height:${height}px;`;
panels.leftCanvas.w = panels.leftHTML.w = width;
panels.leftCanvas.h = panels.leftHTML.h = height;

d3.select(panels.leftHTML).style("pointer-events", "none");

panel = d3
.select(ui)
.select(".right_panel")
.html("");
width = panel.node().offsetWidth - 2;
height = panel.node().offsetHeight - 2;
panel
.node()
.appendChild((panels.rightCanvas = DOM.context2d(width, height).canvas));
panel.node().appendChild((panels.rightSVG = DOM.svg(width, height)));
panel.node().appendChild((panels.rightHTML = DOM.element("div")));
panels.rightSVG.style = panels.rightCanvas.style = panels.rightHTML.style = `position:absolute; width:${width}px; height:${height}px;`;
panels.rightCanvas.w = panels.rightHTML.w = width;
panels.rightCanvas.h = panels.rightHTML.h = height;

return panels;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {datasets} with {src, load_limit} from "ba3c3e015089f1f0"
Insert cell
Insert cell
rawdata = od.data
Insert cell
od = datasets[ui_dataset]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function set_selection_highlight() {
const l = tidy.reduce((a, b) => a + !!b.selected, 0),
h = l > 0 && l < tidy.length;
d3.select(ui)
.selectAll("span._selected")
.classed("highlight", h);
}
Insert cell
viewof suggested_preset = {
const _me = JSON.stringify(this && this.value);
// console.warn(_me);

const div = d3.select(DOM.element("div")).attr("style", "min-height: 18em");

/* suggested_tree */
const struct = JSON.parse(
JSON.stringify(
"temp" in reference_stack
? suggested_trees[reference_stack.temp]
: suggested_trees[+tree_option] || tree_test
)
),
visible = div.node();
visible.value = struct;

//ui_tree === "Sequence tree") {
const form = div.append("form");

const onchange = function(attr) {
return () => {
form
.selectAll(".widget")
.each((d, i, e) => {
if (attr === "grid") {
const v = form.node()[attr + i].value;
struct.tree[i].gridding = v;
// show/hide the relevant mapping widgets
const relevant_mappings = ["color"].concat(ops(v));
d3.select(e[i])
.selectAll(".visual_mappings>div")
.style("display", function() {
if (
relevant_mappings.indexOf(
this.className.replace(/_mapping/, "")
) > -1
)
return "block";
else return "none";
});
} else if (attr === "attr") {
const group = form.node()[attr + i].value;
if (tidy.thresholds[group]) {
delete struct.tree[i].group;
struct.tree[i].cluster = group;
} else {
delete struct.tree[i].cluster;
struct.tree[i].group = group;
}
} /* "color", "valueX", "valueY", "valueWidth", "valueHeight" */ else {
if (form.node()[attr + i]) {
const v = form.node()[attr + i].value;
if (v) struct.tree[i][attr] = v;
else delete struct.tree[i][attr];
}
}
})
.selectAll(".miniplot")
.call(update_miniplot);

set_selection_highlight();

visible.dispatchEvent(new CustomEvent("input"));
};
};

fill_form();

function fill_form() {
form.selectAll(".widget").remove();

const widgets = form
.selectAll(".widget")
.data(struct.tree)
.join("div")
.classed("widget", true);

widgets
.each((d, i) => {
d.index = d.index || 1 + (d3.max(struct.tree, d => d.index) || 0);
})
.style("border-left", d => "solid 5px " + tree_color(d.index));

widgets.call(drag, visible, function() {
fill_form();
visible.dispatchEvent(new CustomEvent("input"));
});

widgets
.append("div")
.append("button")
.classed("add", true)
.html("+") // ADD
.on("click", (d, i) => {
struct.tree = struct.tree
.slice(0, i)
.concat([{ gridding: "grid", group: "…" }])
.concat(struct.tree.slice(i, Infinity));
fill_form();
});

widgets
.filter((_, i) => i < widgets.size() - 1)
.append("button")
.classed("remove", true)
.html("x") // REMOVE
.on("click", (d, i) => {
struct.tree = struct.tree
.slice(0, i)
.concat(struct.tree.slice(i + 1, Infinity));
fill_form();
visible.dispatchEvent(new CustomEvent("input"));
});

widgets.append("div").call(
flatmenu,
(d, i) =>
[{ name: "…" }, ...attributes.values()].map(o => ({
value: o,
selected: d.group == o.name,
name: "attr" + i,
text: o.name
})), // data
d => d.value.name, // value
d => `${d.value.name} [${d.value.type}]`, // text
(d, i) => "attr" + i, // name
onchange("attr"),
"blocks", // "select" vs "radio" vs "blocks"
struct.tree.length - 1 // last
);

widgets.append("div").classed("miniplot", true);

const sort_div = widgets.append("div").classed("sort", true);
sort_div
.append("button")
.attr("type", "button")
.html("<") // Sort descending
.on("click", (d, i) => {
struct.tree[i].sort = "desc";
onchange("sort")();
});
sort_div
.append("button")
.attr("type", "button")
.html(">") // Sort ascending
.on("click", (d, i) => {
struct.tree[i].sort = "asc";
onchange("sort")();
});

if (interface_tests)
widgets
.append("div")
.classed("binning", true)
.attr("title", "Binning");

widgets.call(
flatmenu,
(d, i) => {
return (i === struct.tree.length - 1
? ["color"]
: available_griddings
).map(o => ({
value: o,
selected: d.gridding == o,
name: "grid" + i,
text: o
}));
},
d => d.value,
d => d.value,
(d, i) => "grid" + i,
onchange("grid"),
"blocks", // "select" vs "radio" vs "blocks"
struct.tree.length - 1 // last
);

if (true) {
const visual_mappings_div = widgets
.filter((_, i) => i < struct.tree.length - 1)
.append("div")
.classed("visual_mappings", true);

["color", "valueHeight", "valueWidth", "valueX", "valueY"].forEach(
mapping => {
const div = visual_mappings_div
.append("div")
.classed(mapping + "_mapping", true)
.style("display", mapping === "color" ? "block" : "none");
div
.append("div")
.attr("title", mapping)
.classed(mapping + "-icon", true)
.classed("value-icon", true);

div.call(
flatmenu,
(d, i) =>
[{ name: "…" }, ...attributes.values()].map(o => ({
value: o,
selected: d[mapping] == o.name,
name: mapping + i,
text: o.name
})), // data
d => d.value.name, // value
d => `${d.value.name} [${d.value.type}]`, // text
(d, i) => mapping + i, // name
onchange(mapping),
"blocks", // "select" vs "radio" vs "blocks"
struct.tree.length - 1 // last
);

var aggreg = div
.append("div")
.classed("rectangle aggregation", true)
.attr("title", "aggregation")
.call(
flatmenu,
(d, i) =>
[
{ name: "mean" },
{ name: "sum" },
{ name: "count" },
{ name: "distinct" }
].map(o => ({
value: o,
selected: d[mapping + "_aggregation"] == o.name,
name: mapping + "_aggregation" + i,
text: o.name
})), // data
d => d.value.name, // value
d => `${d.value.name} [${d.value.type}]`, // text
(d, i) => mapping + "_aggregation" + i, // name
onchange(mapping + "_aggregation"),
"blocks", // "select" vs "radio" vs "blocks"
struct.tree.length - 1 // last
);
}
);
}
}

form.on("change", onchange("change"));
onchange("change")();

return visible;
}
Insert cell
Insert cell
function update_miniplot(selection) {
selection.html("").each(function(d, i, e) {
const n = d.cluster || d.group,
attr = attributes.get(n) || {};

const scale =
attr.type === "quantitative"
? d => +d
: d3.scaleOrdinal().range(d3.range(200)),
values = tidy.map(d => scale(d[n]));

const s = miniplot_histogram(values /* , color*/);
e[i].appendChild(s);
});
}
Insert cell
function miniplot(values, color) {
/*
aurelientabard> c’est compliqué de brancher leur binning sur la valeur du “# of k-means clusters for quantitative dimensions” ? (ou quantile)
fil> les histograms de D3 c’est toute une histoire pour que les valeurs “tombent juste”
on pourrait remplacer par un truc de binning dont la longueur serait l’intervalle, et la hauteur l’effectif divisé par la longueur de l’intervalle
(histoire que la surface soit proportionnelle à l’effectif)
aurelientabard> naivement ça me semble bien
*/
}
Insert cell
function miniplot_histogram(values, color) {
return svg`${histogram(values, {
width: 200,
height: 50,
xText: "",
margin:{top: 0, right: 10, bottom: 0, left: 0},
axis: false
})}`;
}
Insert cell
function flatmenu(
selection,
data = [],
value = (d, i) => i,
text = (d, i) => `${i}`,
name = (d, i) => "grid" + i,
onchange = function() {},
type = "select",
last
) {
if (type === "radio") {
selection
.append("fieldset") // "select"
.style("display", "block")
.selectAll("input") // option
.data(data)
.join("input")
.attr("type", "radio")
.attr("name", d => d.name)
.attr("value", value)
.attr("checked", d => (d.selected ? "checked" : null))
.on("mouseover", (d, i, e) => {
const parent = d3.select(d3.event.target.parentElement);
d3.event.target.checked = true;
parent.select("output").text(d.text);
onchange();
});

return selection
.append("output")
.attr("id", name)
.classed("last", (d, i) => i === last)
.text((d, i) => {
const t = data(d, i).filter(d => d.selected);
if (t[0]) return t[0].text;
});
}

if (type === "blocks") {
var current;
const b = selection
.append("div")
.classed("rectangle", true)
.classed("last", (d, i) => i === last);
b.selectAll("span")
.data(data)
.join("span")
.attr("class", d => {
if (d.name.includes("grid")) {
return "menuitem grid-layout " + d.text;
} else {
return "menuitem attr _" + d.text;
}
})
.classed("selected", d => d.selected)
.on("click", (d, i, e) => {
const parent = d3.select(d3.event.target.parentElement);
parent.selectAll("span").classed("selected", false);
d3.select(d3.event.target).classed("selected", true);
parent.select("output").text((current = d.text));
onchange();
})
.on("mouseover", (d, i, e) => {
const parent = d3.select(d3.event.target.parentElement);
current = parent.select("output").text();
d3.select(d3.event.target).classed("hover", true);
parent.select("output").text(d.text);
onchange();
})
.on("mouseout", (d, i, e) => {
d3.select(d3.event.target).classed("hover", false);
const parent = d3.select(d3.event.target.parentElement);
parent.select("output").text(current);
onchange();
});

return b
.append("output")
.attr("id", name)
.text((d, i) => {
const t = data(d, i).filter(d => d.selected);
if (t[0]) return t[0].text;
});
}

if (type === "select")
return selection
.append("select")
.style("display", "block")
.attr("name", name)
.selectAll("option")
.data(data)
.join("option")
.attr("value", value)
.attr("selected", d => (d.selected ? "selected" : null))
.text(text);
}
Insert cell
Insert cell
css = html`<style>
#presets {float:right; }
.right_panel { margin-top: 2.5em }

div.rectangle {
border: none;
display: inline-block;
max-width: 70%;
}
div.rectangle span {
background: #eee;
width: 9px;
height: 1em;
display: inline-block;
border: 1px solid #aaa;
margin-left: -1px;
cursor: pointer;
}
div.rectangle span.grid-layout {
width: 17px;
border: 1.5px solid transparent;
margin-left: 0px;
border: 1px solid white;
background-size: contain; /* for the background image */
}
div.rectangle span.grid-layout.hover {
border: 1.5px solid lightblue;
}
div.rectangle span.grid-layout.selected {
border: 1.5px solid steelblue;
}
div.rectangle span.attr.highlight {
background: yellow;
}
div.rectangle span.attr.hover {
background: lightblue;
}
div.rectangle span.attr.selected {
background: steelblue;
}
div.rectangle output { margin-left: 3px; }

.grid rect.highlight { stroke: yellow; stroke-width:2px }


.aggregation {
width: 60px;
position: absolute;
right: 2px;
}

.widget {
/* position: absolute;
overflow: hidden;*/
margin-top: 1em;
margin-bottom: 1em;
padding: 5px;
font-size: small;
background-color: #f8f8f8;
margin-right: 1px;
}
.widget button {
text-decoration: none;
background-color: #f1f1f1;
color: black;
float: left
}
.widget button:hover {
background-color: #ddd;
color: black;
}

button.remove {
float: right;
position: relative;
top: -0.3em;
}
button.add {
position: relative;
vertical-align: top;
left: 2px;
top: -2em;
}

.miniplot {
display: inline-block;
}

.sort {
display: inline-block;
}

.visual_mapping {
display: inline-block;
}

div.binning {
height:20px;
width:200px;
margin: 5px 0px;
background-size: contain;
vertical-align: top;
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/binning.png);
}


.value-icon {
width:15px;
height:15px;
margin-right:5px;
background-size: contain;
display:inline-block;
vertical-align: top;
}
.color-icon {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/color-icon.png);
}
.valueHeight-icon {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/height-icon.svg);
}
.valueWidth-icon {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/width-icon.svg);
}
.valueX-icon {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/vertical.svg); /* x-icon.svg */
}
.valueY-icon {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/horizontal.svg); /* y-icon.svg */
}

div.rectangle span.grid {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/grid.svg);
}
div.rectangle span.central {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/central.svg);
}
div.rectangle span.coordinate {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/coordinate.svg);
}
div.rectangle span.brick {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/brick.svg);
}
div.rectangle span.treemap {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/treemap.svg);
}
div.rectangle span.horizontal {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/horizontal.svg);
}
div.rectangle span.vertical {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/vertical.svg);
}
div.rectangle span.radial {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/radial.svg);
}
div.rectangle span.overlap {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/overlap.svg);
}
div.rectangle span.color {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/color.svg);
}
div.rectangle span.size {
background-image: url(https://projet.liris.cnrs.fr/mi2/oddata/icons/size.svg);
}

</style>`
Insert cell
function init_tree(g, sequence) {
const st = g
.html("")
.append("g")
.attr("id", "sequence_tree");

}
Insert cell
function draw_tree(g, tree) {
return false;

const st = g.select("#sequence_tree");

st.append("line")
.attr("transform", (d, i) => `translate(120, ${40})`)
.attr("y2", 70 * tree.length)
.attr("stroke", "black");

const ng = st
.selectAll("g")
.data([{ group: "root" }, ...tree.tree])
.join("g")
.attr("transform", (d, i) => `translate(120, ${40 + 70 * i})`);

ng.append("circle")
.attr("r", 30)
.attr("fill", (d, i) => tree_color(d.index))
.attr("stroke-width", (d, i) => (i === tree.tree.length ? "4" : "none"))
.attr("stroke", (d, i) => (i === tree.tree.length ? "brown" : "none"));

ng.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.text(d => d.group);

setTimeout(() => {
ng.append("g")
.attr("transform", "translate(35,0)")
.call(make_legend);
}, 100);

function make_legend(g) {
g.each((d, i, e) => {
if (i === 0) {
d3.select(e[i])
.append("circle")
.attr("r", 10)
.attr("fill", "red"); // root
} else if (i < g.size() - 1) {
d3.select(e[i])
.append("circle")
.attr("r", 10)
.attr("fill", "lightblue"); // intermediaries
} else {
const legends = legend
.legendColor()
.shapeWidth(50)
.orient("vertical")
.scale(paint.color);
if (paint.type === "cluster")
legends.labels(legend.legendHelpers.thresholdLabels);

d3.select(e[i])
.attr("transform", "translate(50,-35)")
.call(legends);
}
});
}
}
Insert cell
tree_color = {
const scale = d3
.scaleOrdinal(["#ccc", ...d3.schemeCategory10])
.domain(d3.range(100));
return scale;
}
Insert cell
Insert cell
Insert cell
function draw_grid(root, svg) {
const gridVisible = +(tree.ui_show.indexOf("Grid") > -1);

let g = svg.append("g").attr("class", "grid");

g.attr("transform", d => `translate(0,0)`)
.datum(root)
.attr("opacity", gridVisible || 0.01);

for (let i = 0; i < tree.tree.length - 1; i++) {
const fontsize = 30 / Math.sqrt(i + 3);

const t = tree.tree[i],
aggregation = t["color_aggregation"] || "mean" /* "mean" or "sum" ? */;
var fill =
t.color && t.color !== "…"
? g => gridColor(g[aggregation + "_" + t.color])
: null;
if (fill) {
const stats = get_stats(root, aggregation + "_" + t.color, i + 1),
extent = d3.extent(stats);
if (extent[0] !== undefined) gridColor.domain(extent);
else fill = null;
}

g = g
.selectAll("g")
.data(d => d[1])
.join("g")
.attr("transform", d => `translate(${[d.x, d.y]})`);

g.append("rect")
.attr("width", g => g.width)
.attr("height", g => g.height)
.attr("stroke", tree_color(t.index))
.attr("fill", g => (fill ? fill(g) : "white"))
.attr(
"fill-opacity",
g => (fill ? 1 : 0.01) // capture click
)
.on("click", function(d, e) {
const me = this;
select_subset(d, me, d3.event.shiftKey);
})
.on("mouseover", function(d, e) {
const me = this;
// if shift is pressed we consider that we are in select mode
if (d3.event.shiftKey) {
select_subset(d, me, d3.event.shiftKey);
}
});

g.append("defs")
.append("clipPath")
.attr("id", (g, j) => `clipText${i}-${j}`)
.append("rect")
.attr("width", g => g.width - 1)
.attr("height", g => g.height);

g.append("text")
.style("font-size", g => fontsize * Math.sqrt(g.width / 150))
.text(d => d[0])
.attr("fill", tree_color(t.index))
.attr("dx", 2)
.attr("dy", g => 1 + 18 * i * Math.sqrt(g.width / 150))
.attr("dominant-baseline", "hanging")
.attr("clip-path", (g, j) => `url(#clipText${i}-${j})`);
}
}
Insert cell
SVGANIMATE = {
yield "grid";
width;

const sequence = [tree]; //animation_sequence(true);

const panel = panelSVG("right");
d3.select(panel.svg).html("");
}
Insert cell
import {histogram} from "bd9701cc29a65196"
Insert cell
LEFT = {
const { context, width, height } = panelContext("left");

tree.width = width;
tree.height = height;

chartcanvas(tree, context);

}
Insert cell
projection = d3.geoMercator()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@5", "d3-hsv@0", "d3-array@2", "d3-gridding@0.1", "d3-hexbin@0")
Insert cell
import {table} from "@tmcw/tables/2" // or "365b531bc3fccde9" // forked of @tmcw
Insert cell
Insert cell
import {checkbox, radio, slider, textarea} from "@jashkenas/inputs"
Insert cell
Insert cell
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