这里是D3的全新产品……我正在尝试通过合并和缩放构建单轴时间轴。我有一个概念验证工作,没有分箱:
const data = [
{
assessment_date: "2018-04-19T00:31:03.153000Z",
score: 4,
type: "formative",
is_proficient: false,
label: "a",
id: 1
}, {
assessment_date: "2017-11-20T09:51:36.035983Z",
score: 3,
type: "summative",
is_proficient: false,
label: "b",
id: 2,
}, {
assessment_date: "2018-02-15T09:51:36.035983Z",
score: 3,
type: "formative",
is_proficient: true,
label: "c",
id: 3,
}, {
assessment_date: "2018-02-20T09:51:36.035983Z",
score: 3,
type: "summative",
is_proficient: true,
label: "d",
id: 4,
}, {
assessment_date: "2018-03-19T17:48:44.820000Z",
score: 4,
type: "summative",
is_proficient: false,
label: "e",
id: 5
}
];
const byDate = o => o.assessment_date;
const sortedData = data.map(o => Object.assign({}, o, {
"assessment_date": new Date(o.assessment_date)
})).sort((a,b) => a.assessment_date - b.assessment_date);
const NODE_RADIUS = 6;
const WIDTH = 600;
const HEIGHT = 30;
const xScale = d3.time.scale()
.domain(d3.extent(sortedData.map(byDate)))
.range([0, WIDTH])
.nice();
const xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
const zoom = d3.behavior.zoom()
.x(xAxis.scale())
.on("zoom", function() {
axisSelector.call(xAxis);
nodesSelector.attr('cx', o => {
return xScale(o.assessment_date)
});
});
const svg = d3.select("#timeline")
.append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.attr("padding-top", "10px")
.attr("transform", "translate(0," + (HEIGHT) + ")")
.call(zoom);
const axisSelector = svg.append('g')
.attr("class", "x axis")
.call(xAxis);
const nodesSelector = svg.selectAll(".node")
.data(sortedData)
.enter()
.append("circle")
.attr('id', o => `node${o.id}`)
.attr('class', o => {
let cx = ['node'];
(o.type === 'formative') ? cx.push('formative') : cx.push('summative');
(o.is_proficient) ? cx.push('proficient') : cx.push('not-proficient');
return cx.join(' ');
})
.attr("r", 8)
.attr("cx", o => xScale(o.assessment_date))
nodesSelector.on("click", function(node) {
console.log('boop!')
});
#timeline {
overflow: hidden;
}
#timeline svg {
padding: 15px 30px;
overflow: hidden;
}
.axis text {
font-family: sans-serif;
font-size: 10px;
}
.axis path,
.axis line {
stroke: 3px;
fill: none;
stroke: black;
stroke-linecap: round;
}
.node {
stroke-width: 3px;
stroke: white;
}
.node.proficient {
fill: green;
stroke: green;
}
.node.not-proficient {
fill: orange;
stroke: orange;
}
.node.summative {
stroke: none;
}
.node.formative {
fill: white;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="timeline"></div>
在生产中,我将处理大量数据,并且需要将节点分组到一个组中(同时在组上方显示一个数字,指示一个组中有多少个节点)。
我的第一次尝试是在这里
const data = [
{
assessment_date: "2018-04-19T00:31:03.153000Z",
id: 1
}, {
assessment_date: "2017-11-20T09:51:36.035983Z",
id: 2,
}, {
assessment_date: "2018-02-15T09:51:36.035983Z",
id: 3,
}, {
assessment_date: "2018-02-20T09:51:36.035983Z",
id: 4,
}, {
assessment_date: "2018-03-19T17:48:44.820000Z",
id: 5
}
];
const byDate = datum => datum.assessment_date;
const sortedData = data.map(datum => Object.assign({}, datum, {
"assessment_date": new Date(datum.assessment_date)
})).sort((a,b) => a.assessment_date - b.assessment_date);
const NODE_RADIUS = 6;
const WIDTH = 600;
const HEIGHT = 30;
const xScale = d3.time.scale()
.domain(d3.extent(sortedData.map(byDate)))
.range([0, WIDTH])
// .nice();
const xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
const histogram = d3.layout.histogram()
.value(datum => datum.assessment_date)
.range(xAxis.scale().domain())
const zoom = d3.behavior
.zoom()
.x(xScale)
.on("zoom", function() {
axisSelector.call(xAxis);
update(histogram(sortedData));
});
const svg = d3.select("#timeline")
.append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.attr("padding-top", "10px")
// .attr("transform", "translate(0," + (HEIGHT) + ")")
.call(zoom);
const axisSelector = svg.append('g')
.attr("class", "x axis")
.call(xAxis);
function update(data) {
const node = svg.selectAll(".node").data(data);
const nodeLabel = svg.selectAll(".node-label").data(data);
node.enter()
.append("circle")
.attr("class", "node")
.attr("r", NODE_RADIUS)
.attr("style", datum => !datum.length && 'display: none')
// ^ this seems inelegant. why are some bins empty?
.attr("cx", datum => xScale(datum.x))
node.enter()
.append("text")
.attr("class", "node-label")
.text(datum => datum.length > 1 ? `${datum.length}` : '')
.attr("x", datum => xScale(datum.x) - NODE_RADIUS/2)
.attr("y", "-10px")
node.attr("cx", datum => xScale(datum.x));
nodeLabel.attr("x", datum => xScale(datum.x) - NODE_RADIUS/2);
return node;
}
const nodeSelector = update(histogram(sortedData));
#timeline {
overflow: hidden;
}
#timeline svg {
padding: 20px 30px;
overflow: hidden;
}
.axis text {
font-family: sans-serif;
font-size: 10px;
}
.axis path,
.axis line {
stroke: 3px;
fill: none;
stroke: black;
stroke-linecap: round;
}
.node {
stroke-width: 3px;
stroke: white;
}
.node-label {
font-family: sans-serif;
font-size: 11px;
}
.node.proficient {
fill: green;
stroke: green;
}
.node.not-proficient {
fill: orange;
stroke: orange;
}
.node.summative {
stroke: none;
}
.node.formative {
fill: white;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="timeline"></div>
似乎可以将附近的节点很好地合并在一起,但是并不能对缩放进行分组/取消分组。有什么想法或例子吗?我一直在搜寻bl.ocks和google几个小时。
直方图是否带有针对我要执行的操作的正确原语?这是一个很好的示例,说明了在不清楚的情况下我要做什么:http://www.iftekhar.me/ibm/ibm-project-timeline/…导航到Final Iteration
部分的底部。
最后,由于我们尚未升级依赖项,因此我正在使用D3 v3.x 。
奖金问题:为什么某些直方图箱为空?
答案 0 :(得分:3)
这是一个d3v5
(d3v3
波纹管)解决方案,当两个圆的距离小于2个半径时(当它们彼此接触时)合并两个圆,并为所得的圆提供合并的平均日期圈子。
let data = [
{ assessment_date: "2017-11-20T09:51:36.035983Z", id: 2 },
{ assessment_date: "2018-04-19T00:31:03.153000Z", id: 1 },
{ assessment_date: "2018-02-15T09:51:36.035983Z", id: 3 },
{ assessment_date: "2018-02-20T09:51:36.035983Z", id: 4 },
{ assessment_date: "2018-03-19T17:48:44.820000Z", id: 5 }
];
data = data
.map(d => { d.date = new Date(d.assessment_date); return d; })
.sort(d => d.assessment_date);
const NODE_RADIUS = 6;
const WIDTH = 600;
const HEIGHT = 30;
const svg = d3.select("#timeline").append("svg")
.attr("width", WIDTH).attr("height", HEIGHT)
.attr("padding-top", "10px");
let xScale = d3.scaleTime()
.domain(d3.extent(data.map(d => d.date)))
.range([0, WIDTH])
.nice();
const xAxis = d3.axisBottom(xScale);
const axisSelector = svg.append("g").attr("class", "x axis").call(xAxis);
svg.call(
d3.zoom()
.on("zoom", function() {
newScale = d3.event.transform.rescaleX(xScale);
axisSelector.call(xAxis.scale(newScale));
updateCircles(newScale);
})
);
function updateCircles(newScale) {
const mergedData = merge(
data.map(d => { return { date: d.date, count: 1 }; }),
newScale
);
var circles = svg.selectAll("circle").data(mergedData);
circles.enter().append("circle")
.attr("r", NODE_RADIUS)
.merge(circles)
.attr("cx", d => newScale(d.date));
circles.exit().remove();
var counts = svg.selectAll("text.count").data(mergedData);
counts.enter().append("text")
.attr("class", "count")
.merge(counts)
.attr("transform", d => "translate(" + (newScale(d.date) - 3) + ",-10)")
.text(d => d.count);
counts.exit().remove();
}
function merge(data, scale) {
let newData = [data[0]];
let i;
for (i = 1; i < data.length; i++) {
const previous = newData[newData.length - 1];
const distance = scale(data[i].date) - scale(previous.date);
if (Math.abs(distance) < 2 * NODE_RADIUS) {
const averageDate = new Date(
(data[i].date.getTime() * data[i].count + previous.date.getTime() * previous.count)
/ (data[i].count + previous.count)
);
const count = previous.count;
newData.pop();
newData.push({ date: averageDate, count: data[i].count + count });
}
else
newData.push(data[i]);
}
return newData;
}
updateCircles(xScale);
#timeline {
overflow: hidden;
}
#timeline svg { padding: 20px 30px; overflow: hidden; }
.axis text {
font-family: sans-serif;
font-size: 10px;
}
.axis path,
.axis line {
stroke: 3px;
fill: none;
stroke: black;
stroke-linecap: round;
}
.node {
stroke-width: 3px;
stroke: white;
}
.node-label {
font-family: sans-serif;
font-size: 11px;
}
.node.proficient {
fill: green;
stroke: green;
}
.node.not-proficient {
fill: orange;
stroke: orange;
}
.node.summative {
stroke: none;
}
.node.formative { fill: white; }
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="timeline"></div>
与原始代码相比,唯一的区别是使用以下算法合并圆:
function merge(data, scale) {
let newData = [data[0]];
let i;
for (i = 1; i < data.length; i++) {
const previous = newData[newData.length - 1];
const distance = scale(data[i].date) - scale(previous.date);
if (Math.abs(distance) < 2 * NODE_RADIUS) {
const averageDate = new Date(
(data[i].date.getTime() * data[i].count + previous.date.getTime() * previous.count)
/ (data[i].count + previous.count)
);
const count = previous.count;
newData.pop();
newData.push({ date: averageDate, count: data[i].count + count });
}
else
newData.push(data[i]);
}
return newData;
}
会在每次缩放事件时产生新的数据版本,并显示每个节点的相关计数。
和等效的d3v3
:
let data = [
{ assessment_date: "2017-11-20T09:51:36.035983Z", id: 2 },
{ assessment_date: "2018-04-19T00:31:03.153000Z", id: 1 },
{ assessment_date: "2018-02-15T09:51:36.035983Z", id: 3 },
{ assessment_date: "2018-02-20T09:51:36.035983Z", id: 4 },
{ assessment_date: "2018-03-19T17:48:44.820000Z", id: 5 }
];
data = data
.map(d => { d.date = new Date(d.assessment_date); return d; })
.sort(d => d.date);
const NODE_RADIUS = 6;
const WIDTH = 600;
const HEIGHT = 30;
const svg = d3.select("#timeline").append("svg")
.attr("width", WIDTH).attr("height", HEIGHT)
.attr("padding-top", "10px");
let xScale = d3.time.scale()
.domain(d3.extent(data.map(d => d.date)))
.range([0, WIDTH])
.nice();
const xAxis = d3.svg.axis().scale(xScale).orient('bottom');
const axisSelector = svg.append("g").attr("class", "x axis").call(xAxis);
svg.call(
d3.behavior.zoom()
.x(xScale)
.on("zoom", function() {
axisSelector.call(xAxis);
updateCircles(xScale);
})
);
function updateCircles(newScale) {
const mergedData = merge(
data.map(d => { return { date: d.date, count: 1 }; }),
newScale
);
var circles = svg.selectAll("circle").data(mergedData);
circles.attr("cx", d => newScale(d.date));
circles.enter().append("circle")
.attr("r", NODE_RADIUS)
.attr("cx", d => newScale(d.date));
circles.exit().remove();
var counts = svg.selectAll("text.count").data(mergedData);
counts.attr("transform", d => "translate(" + (newScale(d.date) - 3) + ",-10)")
.text(d => d.count);
counts.enter().append("text")
.attr("class", "count")
.attr("transform", d => "translate(" + (newScale(d.date) - 3) + ",-10)")
.text(d => d.count);
counts.exit().remove();
}
function merge(data, scale) {
let newData = [data[0]];
let i;
for (i = 1; i < data.length; i++) {
const previous = newData[newData.length - 1];
const distance = scale(data[i].date) - scale(previous.date);
if (Math.abs(distance) < 2 * NODE_RADIUS) {
const averageDate = new Date(
(data[i].date.getTime() * data[i].count + previous.date.getTime() * previous.count)
/ (data[i].count + previous.count)
);
const count = previous.count;
newData.pop();
newData.push({ date: averageDate, count: data[i].count + count });
}
else
newData.push(data[i]);
}
return newData;
}
updateCircles(xScale);
#timeline {
overflow: hidden;
}
#timeline svg { padding: 20px 30px; overflow: hidden; }
.axis text {
font-family: sans-serif;
font-size: 10px;
}
.axis path,
.axis line {
stroke: 3px;
fill: none;
stroke: black;
stroke-linecap: round;
}
.node {
stroke-width: 3px;
stroke: white;
}
.node-label {
font-family: sans-serif;
font-size: 11px;
}
.node.proficient {
fill: green;
stroke: green;
}
.node.not-proficient {
fill: orange;
stroke: orange;
}
.node.summative {
stroke: none;
}
.node.formative { fill: white; }
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="timeline"></div>