DraggableTable = input => {
input = input || penguins;
const data = Array.from(input);
const min_width = 30;
const padding_right = 10;
const max_height = 400;
const formatter = x => (isFinite(x) ? d3.format(".1f")(x) : x);
const wrapper = d3
.create("div")
.style("width", width + "px")
.style("max-height", max_height + "px")
.style("overflow-y", "auto");
const table = wrapper
.append("table")
.style("table-layout", "fixed")
.style("border-collapse", "collapse");
const el = wrapper.node();
el.value = data.map(d => Object.assign({}, d));
const cols = Object.keys(data[0]);
const col_width = Math.floor(width / cols.length);
const colgroup = table
.append("colgroup")
.append("col")
.attr("span", cols.length)
.style("width", col_width + "px")
.style("border-right", "40px");
let scales = new Map();
const header = table.append('tr');
// Append a table header and compute linear scales for each column
cols.forEach(col => {
header.append("th").text(col);
const max = d3.max(data, dd => +dd[col]);
const min = d3.min([0, d3.min(data, dd => +dd[col])]); // minimum of 0 or negative value
const col_scale = isNaN(max)
? x => x
: d3
.scaleLinear()
.domain([min, max])
.range([min_width, col_width - padding_right]);
scales.set(col, col_scale);
});
// Append rows
data.forEach((row, row_num) => {
const tr = table.append("tr");
// Append cells in each row
Object.keys(row).forEach((cell, cell_num) => {
const value = row[cell];
const scale = scales.get(cell);
const td = tr.append("td").style("width", col_width + "px");
const div = td
.append("div")
.style("display", "inline-block")
.style("border", "1px solid white")
.style("height", "20px")
.style("cursor", "pointer")
.style("background-color", isFinite(value) ? "lightblue" : "none");
div.append("text").text(formatter(value));
// Only assign drag events for numeric columns
if (scale.domain === undefined) return;
// Drag event based on: https://observablehq.com/@d3/circle-dragging-iii
// Name spacing these functions was hard....
div.style("width", scale(value) + "px").call(
d3
.drag()
.on("start", function(event) {
d3.select(this)
.raise()
.style("border", "1px solid black");
})
.on("drag", function(event) {
let x = event.x;
if (x > col_width - padding_right || x < min_width) return;
d3.select(this).style("width", x + "px");
const new_value = scale.invert(x);
d3.select(this)
.select("text")
.text(formatter(new_value));
// Set the value of the table to the updated data
el.value[row_num][cell] = new_value;
el.dispatchEvent(new Event("input", { bubbles: true }));
})
.on("end", function(event) {
d3.select(this).style("border", "1px solid white");
})
);
});
});
return el;
}