首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何在JS时序图中对标签进行分组

如何在JS时序图中对标签进行分组
EN

Stack Overflow用户
提问于 2020-11-25 10:25:43
回答 1查看 232关注 0票数 1

我真的需要一些帮助。我正在尝试制作我自己的时间线图(和谷歌时间线一样)。它已经工作了,但我不知道如何使一个函数按类型对标签进行分组。我硬编码了这个函数,它适用于3组/类型。我需要的是一个通用的功能,即使有100个标签,也可以对所有标签进行分组。

第二个问题是,在第二组和第三组中,如果是a.endTime >= b.startTime,则不在同一行。我会非常感谢你的帮助

这是我的代码:

代码语言:javascript
复制
var w = 800;
var h = 400;

var svg = d3
  .select(".svg")
  .append("svg")
  .attr("width", w)
  .attr("height", h)
  .attr("class", "svg");

//main array
var items = [{
    task: "conceptualize",
    type: "development",
    startTime: "2013-1-28", //year/month/day
    endTime: "2013-2-1",
    number: 2,
  },

  {
    task: "sketch",
    type: "development",
    startTime: "2013-2-6",
    endTime: "2013-2-9",
    number: 2,
  },

  {
    task: "color profiles",
    type: "development",
    startTime: "2013-2-6",
    endTime: "2013-2-9",
    number: 2,
  },

  {
    task: "HTML",
    type: "coding",
    startTime: "2013-2-2",
    endTime: "2013-2-6",
    number: 1,
  },

  {
    task: "write the JS",
    type: "coding",
    startTime: "2013-2-1",
    endTime: "2013-2-6",
    number: 1,
  },
  {
    task: "eat",
    type: "celebration",
    startTime: "2013-2-8",
    endTime: "2013-2-13",
    number: 0,
  },

  {
    task: "crying",
    type: "celebration",
    startTime: "2013-2-13",
    endTime: "2013-2-16",
    number: 0,
  },
];

//array sorting
items.sort((a, b) => {
  return (
    a.number - b.number || Date.parse(b.startTime) - Date.parse(a.startTime)
  );
});

//here create new array and add index for every label
var taskArray = [];
var stack = [];
items.map((e) => {
  var lane = stack.findIndex(
    (s) => s.endTime <= e.startTime && s.startTime < e.startTime
  );

  var yIndex = lane === -1 ? stack.length : lane;
  taskArray.push({
    ...e,
    yIndex,
  });
  stack[yIndex] = e;
});

//here hardcoded grouping based on index and type
var nRows = d3.max(taskArray, (d) =>
  d.type === "celebration" ? d.yIndex + 1 : null
);

//here hardcoded grouping based on index and type
var nRows2 = d3.max(taskArray, (d) =>
  d.type === "development" ? (d.yIndex = d.yIndex + 1 + nRows) : null
);

var dateFormat = d3.time.format("%Y-%m-%d");

var timeScale = d3.time
  .scale()
  .domain([
    d3.min(taskArray, function(d) {
      return dateFormat.parse(d.startTime);
    }),
    d3.max(taskArray, function(d) {
      return dateFormat.parse(d.endTime);
    }),
  ])
  .range([0, w - 150]);

var categories = new Array();

for (var i = 0; i < taskArray.length; i++) {
  categories.push(taskArray[i].type);
}

var catsUnfiltered = categories; //for vert labels

categories = checkUnique(categories);

makeGant(taskArray, w, h);

function makeGant(tasks, pageWidth, pageHeight) {
  var barHeight = 20;
  var gap = barHeight + 4;
  var topPadding = 75;
  var sidePadding = 75;

  var colorScale = d3.scale
    .linear()
    .domain([0, categories.length])
    .range(["#00B9FA", "#F95002"])
    .interpolate(d3.interpolateHcl);

  makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
  drawRects(
    tasks,
    gap,
    topPadding,
    sidePadding,
    barHeight,
    colorScale,
    pageWidth,
    pageHeight
  );
  vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}

function drawRects(
  theArray,
  theGap,
  theTopPad,
  theSidePad,
  theBarHeight,
  theColorScale,
  w,
  h
) {
  svg
    .append("g")
    .selectAll("rect")
    .data(theArray)
    .enter()
    .append("rect")
    .attr("x", 0)
    .attr("y", function(d, i) {
      return d.yIndex * theGap + theTopPad - 2;
    })
    .attr("width", function(d) {
      return w - theSidePad / 2;
    })
    .attr("height", theGap)
    .attr("stroke", "none")
    .attr("fill", function(d) {
      for (var i = 0; i < categories.length; i++) {
        if (d.type == categories[i]) {
          return d3.rgb(theColorScale(i));
        }
      }
    })
    .attr("opacity", 0.2);

  var rectangles = svg.append("g").selectAll("rect").data(theArray).enter();

  rectangles
    .append("rect")
    .attr("rx", 3)
    .attr("ry", 3)
    .attr("x", function(d) {
      return timeScale(dateFormat.parse(d.startTime)) + theSidePad;
    })
    //here draw milestones depend on index
    .attr("y", function(d, i) {
      return d.yIndex * theGap + theTopPad;
    })
    .attr("width", function(d) {
      return (
        timeScale(dateFormat.parse(d.endTime)) -
        timeScale(dateFormat.parse(d.startTime))
      );
    })
    .attr("height", theBarHeight)
    .attr("stroke", "none")
    .attr("fill", function(d) {
      for (var i = 0; i < categories.length; i++) {
        if (d.type == categories[i]) {
          return d3.rgb(theColorScale(i));
        }
      }
    });

  rectangles
    .append("text")
    .text(function(d) {
      return d.task;
    })
    .attr("x", function(d) {
      return (
        (timeScale(dateFormat.parse(d.endTime)) -
          timeScale(dateFormat.parse(d.startTime))) /
        2 +
        timeScale(dateFormat.parse(d.startTime)) +
        theSidePad
      );
    })
    .attr("y", function(d, i) {
      return d.yIndex * theGap + 14 + theTopPad;
    })
    .attr("font-size", 11)
    .attr("text-anchor", "middle")
    .attr("text-height", theBarHeight)
    .attr("fill", "#fff");
}

function makeGrid(theSidePad, theTopPad, w, h) {
  var xAxis = d3.svg
    .axis()
    .scale(timeScale)
    .orient("bottom")
    .ticks(d3.time.days, 1)
    .tickSize(-h + theTopPad + 20, 0, 0)
    .tickFormat(d3.time.format("%d %b"));

  var grid = svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
    .call(xAxis)
    .selectAll("text")
    .style("text-anchor", "middle")
    .attr("fill", "#000")
    .attr("stroke", "none")
    .attr("font-size", 10)
    .attr("dy", "1em");
}

function vertLabels(
  theGap,
  theTopPad,
  theSidePad,
  theBarHeight,
  theColorScale
) {
  var numOccurances = new Array();
  var prevGap = 0;

  for (var i = 0; i < categories.length; i++) {
    numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)];
  }

  var axisText = svg
    .append("g") //without doing this, impossible to put grid lines behind text
    .selectAll("text")
    .data(numOccurances)
    .enter()
    .append("text")
    .text(function(d) {
      return d[0];
    })
    .attr("x", 10)
    .attr("y", function(d, i) {
      if (i > 0) {
        for (var j = 0; j < i; j++) {
          prevGap += numOccurances[i - 1][1];
          // console.log(prevGap);
          return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;
        }
      } else {
        return (d[1] * theGap) / 2 + theTopPad;
      }
    })
    .attr("font-size", 11)
    .attr("text-anchor", "start")
    .attr("text-height", 14)
    .attr("fill", function(d) {
      for (var i = 0; i < categories.length; i++) {
        if (d[0] == categories[i]) {
          //  console.log("true!");
          return d3.rgb(theColorScale(i)).darker();
        }
      }
    });
}

//from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
function checkUnique(arr) {
  var hash = {},
    result = [];
  for (var i = 0, l = arr.length; i < l; ++i) {
    if (!hash.hasOwnProperty(arr[i])) {
      //it works with objects! in FF, at least
      hash[arr[i]] = true;
      result.push(arr[i]);
    }
  }
  return result;
}

//from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
function getCounts(arr) {
  var i = arr.length, // var to loop over
    obj = {}; // obj to store results
  while (i) obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
  return obj;
}

// get specific from everything
function getCount(word, arr) {
  return getCounts(arr)[word] || 0;
}
代码语言:javascript
复制
body {
  background: #fff;
  font-family: 'Open-Sans', sans-serif;
}

#container {
  margin: 0 auto;
  position: relative;
  width: 800px;
  overflow: visible;
}

.svg {
  width: 800px;
  height: 400px;
  overflow: visible;
  position: absolute;
}

.grid .tick {
  stroke: lightgrey;
  opacity: 0.3;
  shape-rendering: crispEdges;
}

.grid path {
  stroke-width: 0;
}
代码语言:javascript
复制
<div id="container">
  <div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-11-25 13:28:47

您可以极大地简化代码,同时使其具有动态。除了我留下的注释之外,还有一些提示

categories;

  • Don't

  • 只是尽快解析日期对象,而不是每次需要时都这样做;

  • 使用d3.nest对类别进行分组,以获得用于着色的索引,只需使用第二个函数参数--每个类别一个g元素,g中的一个rect.background,以及符合该类别的任务。这样,您可以非常容易地删除类别,这样可以更容易地计算位置,并且只需要设置填充颜色一次。

如果你有什么问题请告诉我

代码语言:javascript
复制
var w = 800;
var h = 400;

var svg = d3
  .select(".svg")
  .append("svg")
  .attr("width", w)
  .attr("height", h)
  .attr("class", "svg");

//main array
var items = [{
    task: "conceptualize",
    type: "development",
    startTime: "2013-1-28", //year/month/day
    endTime: "2013-2-1",
    number: 2,
  },

  {
    task: "sketch",
    type: "development",
    startTime: "2013-2-6",
    endTime: "2013-2-9",
    number: 2,
  },

  {
    task: "color profiles",
    type: "development",
    startTime: "2013-2-6",
    endTime: "2013-2-9",
    number: 2,
  },

  {
    task: "HTML",
    type: "coding",
    startTime: "2013-2-2",
    endTime: "2013-2-6",
    number: 1,
  },

  {
    task: "write the JS",
    type: "coding",
    startTime: "2013-2-1",
    endTime: "2013-2-6",
    number: 1,
  },
  {
    task: "eat",
    type: "celebration",
    startTime: "2013-2-8",
    endTime: "2013-2-13",
    number: 0,
  },

  {
    task: "crying",
    type: "celebration",
    startTime: "2013-2-13",
    endTime: "2013-2-16",
    number: 0,
  },
];

var dateFormat = d3.time.format("%Y-%m-%d");

items.forEach((e) => {
  e.startTime = dateFormat.parse(e.startTime);
  e.endTime = dateFormat.parse(e.endTime);
});

// Use d3.nest to group the items based on the category
// https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest
// You can use .sortKeys to sort by key, for example
var categories = d3.nest()
  .key(d => d.type)
  .sortValues((a, b) => a.startTime - b.startTime)
  .rollup(function(events) {
    // Here, we see if we can apply some useful logic
    // In this case, we search for overlapping events so they can be placed
    // in different lanes
    events.forEach(function(e, i) {
      // Look only at the preceding events
      // Remember that the events have been sorted already
      const overlappingEvents = events.slice(0, i)
        .filter(other => other.endTime > e.startTime);
      
      if(overlappingEvents.length > 0) {
        e.level = d3.max(overlappingEvents, e => e.level) + 1;
      } else {
        e.level = 0;
      }
    });
    return events;
  })
  .entries(items);

// Set for each category the required number of lanes
let offset = 0;
categories.forEach(c => {
  c.lanes = d3.max(c.values, d => d.level) + 1;
  c.offset = offset;
  offset += c.lanes;
});

var timeScale = d3.time
  .scale()
  .domain([
    d3.min(items, d => d.startTime),
    d3.max(items, d => d.endTime)
  ])
  .range([0, w - 150]);

makeGant(categories, w, h);

function makeGant(categories, pageWidth, pageHeight) {
  var barHeight = 20;
  var gap = barHeight + 4;
  var topPadding = 75;
  var sidePadding = 75;

  var colorScale = d3.scale
    .linear()
    .domain([0, categories.length])
    .range(["#00B9FA", "#F95002"])
    .interpolate(d3.interpolateHcl);

  makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
  drawRects(
    categories,
    gap,
    topPadding,
    sidePadding,
    barHeight,
    colorScale,
    pageWidth,
    pageHeight
  );
  vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}

function drawRects(
  categories,
  theGap,
  theTopPad,
  theSidePad,
  theBarHeight,
  theColorScale,
  w,
  h
) {
  const categoryGroups = svg
    .append("g")
    .selectAll("g")
    .data(categories)
    .enter()
    .append("g")
    .attr("transform", function(d, i) {
      // Take the preceding categories and sum their required number of levels
      return `translate(0, ${d.offset * theGap + theTopPad - 2})`;
    })
    // All children inherit this attribute
    .attr("fill", function(d, i) {
      return d3.rgb(theColorScale(i));
    })

  // Just draw one rectangle for all lanes of this category
  categoryGroups
    .append("rect")
    .attr("class", "background")
    .attr("width", function(d) {
      return w - theSidePad / 2;
    })
    .attr("height", function(d) {
      return theGap * d.lanes;
    })
    .attr("stroke", "none")
    .attr("opacity", 0.2);

  //
  var rectangles = categoryGroups
    .selectAll(".task")
    .data(function(d) { return d.values; })
    .enter();

  rectangles
    .append("rect")
    .attr("class", "task")
    .attr("rx", 3)
    .attr("ry", 3)
    .attr("x", function(d) {
      return timeScale(d.startTime) + theSidePad;
    })
    //here draw milestones depend on index
    .attr("y", function(d, i) {
      return d.level * theGap;
    })
    .attr("width", function(d) {
      return timeScale(d.endTime) - timeScale(d.startTime);
    })
    .attr("height", theBarHeight)
    .attr("stroke", "none");

  rectangles
    .append("text")
    .text(function(d) {
      return d.task;
    })
    .attr("x", function(d) {
      return (timeScale(d.endTime) - timeScale(d.startTime)) / 2 +
        timeScale(d.startTime) + theSidePad;
    })
    .attr("y", function(d, i) {
      return d.level * theGap + 14;
    })
    .attr("font-size", 11)
    .attr("text-anchor", "middle")
    .attr("text-height", theBarHeight)
    .attr("fill", "#fff");
}

function makeGrid(theSidePad, theTopPad, w, h) {
  var xAxis = d3.svg
    .axis()
    .scale(timeScale)
    .orient("bottom")
    .ticks(d3.time.days, 1)
    .tickSize(-h + theTopPad + 20, 0, 0)
    .tickFormat(d3.time.format("%d %b"));

  var grid = svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
    .call(xAxis)
    .selectAll("text")
    .style("text-anchor", "middle")
    .attr("fill", "#000")
    .attr("stroke", "none")
    .attr("font-size", 10)
    .attr("dy", "1em");
}

function vertLabels(
  theGap,
  theTopPad,
  theSidePad,
  theBarHeight,
  theColorScale
) {
  var axisText = svg
    .append("g") //without doing this, impossible to put grid lines behind text
    .selectAll("text")
    .data(categories) // one label per category
    .enter()
    .append("text")
    .text(function(d) {
      return d.key;
    })
    .attr("x", 10)
    .attr("y", function(d, i) {
      return (d.offset + d.lanes / 2) * theGap + theTopPad;
    })
    .attr("font-size", 11)
    .attr("text-anchor", "start")
    .attr("text-height", 14)
    .attr("fill", function(d, i) {
      return d3.rgb(theColorScale(i)).darker();
    });
}
代码语言:javascript
复制
body {
  background: #fff;
  font-family: 'Open-Sans', sans-serif;
}

#container {
  margin: 0 auto;
  position: relative;
  width: 800px;
  overflow: visible;
}

.svg {
  width: 800px;
  height: 400px;
  overflow: visible;
  position: absolute;
}

.grid .tick {
  stroke: lightgrey;
  opacity: 0.3;
  shape-rendering: crispEdges;
}

.grid path {
  stroke-width: 0;
}
代码语言:javascript
复制
<div id="container">
  <div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.js"></script>

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/65002755

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档