(d3) 单轴可缩放时间线的动态分箱?

Bre*_*dan 5 javascript charts graph d3.js

全新的 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!')
});
Run Code Online (Sandbox Code Playgroud)
#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;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="timeline"></div>
Run Code Online (Sandbox Code Playgroud)

在生产中,我将处理大量数据,并且需要将节点放在一个组中(同时在组上方显示一个数字,指示一个组有多少个节点)。

我的第一次尝试在这里:

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));
Run Code Online (Sandbox Code Playgroud)
#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;
}
Run Code Online (Sandbox Code Playgroud)
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="timeline"></div>
Run Code Online (Sandbox Code Playgroud)

它似乎可以很好地将附近的节点组合在一起,但它不会在缩放时分组/取消分组。任何想法或例子?我一直在搜索 bl.ocks 和 google 几个小时。

在此处输入图片说明

带有 bin 的直方图甚至是我想要的行为的正确原语吗?这是一个很好的例子,说明我要做什么,以防不清楚:http : //www.iftekhar.me/ibm/ibm-project-timeline/ ...导航到底部Final Iteration部分。

最后,我使用的是 D3 v3.x,因为我们还没有升级我们的依赖项。

奖金问题:为什么一些直方图箱是空的?

Xav*_*hot 4

这是一个d3v5d3v3如下)解决方案,当两个圆的距离小于 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);
Run Code Online (Sandbox Code Playgroud)
#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; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="timeline"></div>
Run Code Online (Sandbox Code Playgroud)

与原始代码相比,唯一真正的区别在于使用以下算法来合并圆圈:

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;
}
Run Code Online (Sandbox Code Playgroud)

它在每个缩放事件时生成要显示的新数据版本以及每个节点的关联计数。


以及d3v3等效的:

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;
}
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
#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; }
Run Code Online (Sandbox Code Playgroud)

  • 非常感谢您的帮助和解释! (2认同)