Published
Edited
12 forks
Importers
55 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart.bind(this)(tl, { key: d => d.data.civilization })
Insert cell
tl = timeline()
.flat(true) // so we can animate individual items
.padding(tlOptions.padding)
.compact(tlOptions.compact)
.accessor(toInterval)(data)
Insert cell
timeline()
.flat(true) // so we can animate individual items
.padding(tlOptions.padding)
.compact(tlOptions.compact)
.accessor(toInterval)(data)
Insert cell
Insert cell
Insert cell
Insert cell
timeline()
.flat(options.flatten)
.padding(options.padding)
.compact(options.compact)
.accessor(accessor)(data)
Insert cell
accessor = v => [v.start, v.end]
Insert cell
timeline = function() {
const o = {
padding: 0,
accessor: identity,
flat: false,
compact: -1
};

const treeOptions = {
update: (value, T) => {
T.value._m.set(value.id, value);
},
remove: (value, T) => {
if (value._m) return true; // this is a node value, it should be destroyed
T.value._m.delete(value.id);
return T.value._m.size === 0;
},
compare: (a, b) => o.compact * (a.max - b.max),
create: value => {
return node({
max: value.max,
_m: new Map([[value.id, value]])
});
}
};

const Row = (id, item) => {
const r = [item];
r.id = id;
r.max = item[1];
return r;
};
Row.add = (row, item) => (row.push(item), (row.max = item[1]));
Row.check = (row, [min]) => row.length === 0 || row.max + o.padding <= min;

const timeline = function(items) {
let counter = 0;
let T = new Tree(treeOptions);

items.forEach(item => {
const interval = o.accessor(item);
interval.data = item;

let finished = T.forEach(holder =>
each(holder._m.values(), function(row) {
if (Row.check(row, interval)) {
T.remove(row);
Row.add(row, interval);
T.insert(row);
return false;
}
})
);

if (finished !== false) T.insert(Row(counter++, interval));
});

let rows = [];
T.forEach(v => [].push.apply(rows, Array.from(v._m.values())));
rows.sort((a, b) => a.id - b.id);

if (o.flat) {
const flat = flatten(rows);
flat.rows = rows.length;
return flat;
}
return rows;
// return { T, rows: rows.sort((a, b) => a.id - b.id).map(v => v.items) };
};

timeline.padding = function(v) {
if (v === undefined || v === null) return o.padding;
o.padding = v;
return timeline;
};

timeline.accessor = function(v) {
if (v === undefined || v === null) return o.accessor;
o.accessor = v;
return timeline;
};

timeline.flat = function(fl) {
if (fl === undefined || fl === null) return o.flat;
o.flat = fl;
return timeline;
};

timeline.compact = function(fl) {
if (fl === undefined || fl === null) return o.compact;
o.compact = fl ? -1 : 1;
return timeline;
};

return timeline;
}
Insert cell
Insert cell
identity = v => v
Insert cell
flatten = rows =>
[].concat.apply([], rows.map(r => r.map(v => ((v.row = r.id), v))))
Insert cell
function each(iterable, callback) {
const iterator = iterable;
let curr = iterator.next();
while (!curr.done) {
if (callback(curr.value) === false) return false;
curr = iterator.next();
}
}
Insert cell
Insert cell
function chart(timeline, options = {}) {
let { key, text, create, update } = {
key: identity,
create: createItem,
update: updateItem,
...options
};
if (!text) {
text = key;
}
const svg = this ? d3.select(this) : d3.create("svg");
const t = svg.transition().duration(chartOptions.easing);
const height = timeline.rows * offset + margin.bottom + margin.top;
svg.transition(t).attr("viewBox", [0, 0, width, height]);

svg
.selectAll('g.items')
.data([timeline])
.join('g')
.attr('class', "items")
.attr("transform", `translate(0,${margin.top})`)
.selectAll("g")
.data(identity, key)
.join(g => g.call(create, text), g => {
g.transition(t)
.delay(
(d, i) =>
d.row *
((chartOptions.easing * Math.min(2, timeline.rows)) / timeline.rows)
)
.call(update);
});

svg
.selectAll('g.axis')
.data(['x'])
.join('g')
.attr('class', 'axis')
.transition(t)
.call(xAxis, height);

return svg.node();
}
Insert cell
createItem = (g, text) =>
g
.append('g')
.attr('transform', d => `translate(${d[0]}, ${offset * d.row + 1})`)
.call(g => {
g.append("rect").call(rect);
g.append("text")
.text(text)
.attr("text-anchor", "middle")
.attr('y', 10)
.attr('x', d => (d[1] - d[0]) / 2)
.style('font-size', '10px')
.style('opacity', 0)
.style('pointer-events', 'none');
g.on('mouseenter', function() {
d3.select(this)
.select('text')
.style('opacity', 1);
}).on('mouseleave', function() {
d3.select(this)
.select('text')
.style('opacity', 0);
});
})
Insert cell
updateItem = g => {
g.attr('transform', d => `translate(${d[0]}, ${offset * d.row + 1})`).call(
g => {
g.select('rect').call(rect);
g.selectAll("text").attr('x', d => (d[1] - d[0]) / 2);
}
);
}
Insert cell
colorize = d => color(d.data.region)
Insert cell
rect = rect =>
rect
.attr('fill', colorize)
.attr('rx', chartOptions.round)
.attr("width", d => d[1] - d[0])
.attr("height", chartOptions.height)
Insert cell
toInterval = function({ start, end }) {
[start, end] = [start, end].map(x);
const width = Math.abs(end - start),
double = chartOptions.round * 2;

if (tlOptions.pretty && width < double) {
const pad = (double - width) / 2;
return [start - pad, end + pad];
}
return [start, end];
}
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

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