Published
Edited
May 16, 2022
Insert cell
md`# D3.js TimeLine`
Insert cell
//require('d3-scale-chromatic');
Insert cell
d3tip = require('d3-tip');
Insert cell
//vegalite = require("@observablehq/vega-lite@0.1");
Insert cell
d3 = require('d3@5');
Insert cell
_ = require('lodash');
Insert cell
Insert cell
data = {
const text = await FileAttachment("appear_dct_CustNo_35.csv").text();
const parseDate = d3.utcParse("%Y-%m-%d");
return d3.csvParse(text, ({custNo, date, type, count}) => ({
custNo: +custNo,
date: parseDate(date),
dateStr: date,
type: type,
count: +count
}));
};
Insert cell
Insert cell
typetoName = ({
"104": "104 人力銀行",
"1111": "1111 人力銀行",
"yes123": "Yes123 人力銀行",
"518": "518 熊班"
});
Insert cell
filterData = data.filter(d => Object.keys(typetoName).includes(d.type) && d.count > 0).sort((a, b) => a.date - b.date);
Insert cell
Insert cell
filterDataGroupbyDate = _.groupBy(filterData, 'date');
Insert cell
json2array = function json2array(json){
var result = [];
var keys = Object.keys(json);
keys.forEach(function(key){
result.push(json[key]);
});
return result;
};
Insert cell
dateStats = json2array(filterDataGroupbyDate).map(d => d.length);
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
eventTypes = d3.nest()
.key(d => typetoName[d.type])
.rollup(event => ({
success: d3.sum(event, e => e.count),
scale: d3.scaleSqrt()
.domain([0, d3.max(event, e => e.count)])
.range([0, rowHeight / 2]),
count: event.length
}))
.object(filterData);
Insert cell
Insert cell
eventTypeEntries = d3.entries(eventTypes);
Insert cell
Insert cell
x = d3.scaleTime()
.domain(d3.extent(filterData.map(d=>d.date)))
.range([120, innerWidth - 200]);
Insert cell
rightBarX = d3.scaleLinear()
.domain([0, d3.max(eventTypeEntries, d => d.value.success)])
.range([x.range()[1] + 16, innerWidth]);
//the right half width
Insert cell
[0, dateStats.length]
Insert cell
dateStats.map((d, i) => i);
Insert cell
d3.max(dateStats.map((d, i) => d));
Insert cell
topBarX = d3.scaleLinear()
.domain([0, dateStats.length])
.range([120, innerWidth - 200]);
Insert cell
numTypes = d3.keys(eventTypes).length
Insert cell
y = d3.scaleOrdinal()
.domain(eventTypeEntries.sort((a, b) => {
return b.value.success - a.value.success
}).map(t => t.key))
.range(d3.range(numTypes).map(d => (d * (rowHeight + rowPadding)) + rowHeight));
Insert cell
topBarY = d3.scaleLinear()
.domain([0, d3.max(dateStats.map((d , i) => d))])
.range([0, margin.top]);
Insert cell
// exp = d3.scaleSqrt()
// .domain([0, 1])
// .range([0, rowHeight / 2]);
Insert cell
// pow = d3.scalePow()
// .exponent(10)
// .domain([0, d3.max(filterData, d => d.count)])
// .range([0, 1])
Insert cell
// r = function(d) {
// return exp(pow(d));
// }
Insert cell
multiFormat = {
var formatMillisecond = d3.timeFormat(".%L"),
formatSecond = d3.timeFormat(":%S"),
formatMinute = d3.utcFormat("%H:%M:%S"),
formatHour = d3.timeFormat("%I %p"),
formatDay = d3.timeFormat("%a %d"),
formatWeek = d3.timeFormat("%b %d"),
formatMonth = d3.timeFormat("%b"),
formatYear = d3.timeFormat("%Y"),
formatYm = d3.timeFormat("%Y-%m"),
formatYmd = d3.timeFormat("%Y-%m");

function multiFormat(date) {
// return (d3.timeSecond(date) < date ? formatMillisecond
// : d3.timeMinute(date) < date ? formatSecond
// : d3.timeHour(date) < date ? formatMinute
// : d3.timeDay(date) < date ? formatHour
// : d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek)
// : d3.timeYear(date) < date ? formatMonth
// : formatYear)(date);
return formatYm(date);
}
return multiFormat;
}
Insert cell
xAxis = g => g.attr("transform", `translate(0, ${numTypes * (rowHeight + rowPadding) + rowHeight})`)
.call(d3.axisBottom(x).ticks(innerWidth / 100).tickFormat(multiFormat))
.call(g => g.select(".domain").remove())
Insert cell
colorOfType = ({
"104 人力銀行": "#FF9100",
"1111 人力銀行": "#009CD8",
"Yes123 人力銀行": "#E40177",
"518 熊班": "#FA1E50"
})
Insert cell
viewof rowHeight = html`
<input type=range value=16 min=12 max=64 />
`
Insert cell
D3 畫圖
Insert cell
{
const svg = d3.select(DOM.svg(innerWidth + margin.left + margin.right, (numTypes * (rowHeight + rowPadding)) + margin.top + margin.bottom+50));
let chart = svg.append("g").attr("transform", `translate(${margin.left}, ${margin.top})`);

const tooltipCircle = d3tip()
.attr('class', 'd3-tip')
.html(d =>`<div style='float: right'>
類型: ${typetoName[d.type]} <br/>
時間: ${d.dateStr} <br/>
</div>`);
svg.call(tooltipCircle);
const tooltipTopbar = d3tip()
.attr('class', 'd3-tip')
.html(d =>`<div style='float: right'>
同時刊登家數: ${d} <br/>
</div>`);
svg.call(tooltipTopbar);

const tooltipRightbar = d3tip()
.attr('class', 'd3-tip')
.html(d =>`<div style='float: right'>
類型: ${d.key} <br/>
累計刊登: ${d.value.count} <br/>
</div>`);
svg.call(tooltipRightbar);

let topBars = chart.append("g")
.selectAll("rect")
.data(dateStats)
.enter().append("rect")
.attr("x", (d,i) => topBarX(i))
.attr("y", d => -topBarY(d))
.attr("height", d => topBarY(d))
.attr("width", d => topBarX(d + 1) - topBarX(d))
.attr("fill", "#53A851")
.attr("opacity", 0.5)
.on('mouseover', tooltipTopbar.show)
.on('mouseout', tooltipTopbar.hide);
chart.append("g")
.call(xAxis);
let typeRows = chart.append("g")
.selectAll("g")
.data(eventTypeEntries)
.enter().append("g");
typeRows.append("text")
.text(d => `${d.key}`)
.attr("class", "label")
.style("font-size", "13px")
.attr("x", -margin.left)
.attr("y", d => y(d.key) + textOffset);
typeRows.append("line")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.attr("stroke", "rgba(0,0,0,0.05)")
.attr("y1", d => y(d.key))
.attr("y2", d => y(d.key));
typeRows.append("rect")
.attr("x", rightBarX.range()[0])
.attr("y", d => y(d.key) - rowHeight / 2)
.attr("height", rowHeight)
.attr("width", d => rightBarX(d.value.success) - rightBarX.range()[0])
.attr("fill", d => colorOfType[d.key])
.attr("opacity", 0.5)
.on('mouseover', tooltipRightbar.show)
.on('mouseout', tooltipRightbar.hide);
typeRows.append("text")
.text(d => d.value.success)
.attr("class", "label")
.style("font-size", "13px")
.attr("text-anchor", "end")
.attr("x", rightBarX.range()[1] - 4)
.attr("y", d => y(d.key) + textOffset);
chart.append("g")
.selectAll("circle")
.data(filterData)
.enter().append("circle")
.attr("cx", d => x(d.date))
.attr("cy", d => y(typetoName[d.type]))
.attr("r", d => eventTypes[typetoName[d.type]].scale(d.count)/2)
.attr("fill", d=>colorOfType[typetoName[d.type]])
.attr("opacity", 0.25)
.on('mouseover', tooltipCircle.show)
.on('mouseout', tooltipCircle.hide);
return svg.node();
}
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