本文介绍了在 D3.js 中为不同宽度的波段创建比例的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们有分配给不同团队的项目.现在我必须创建项目时间表.

We have projects, which are assigned to different teams. Now I have to create project timelines.

出于这个问题的目的,我在 jsfiddle.net 中创建了一个虚拟对象.https://jsfiddle.net/cezar77/6u1waqso/2

For the purposes of this question I have created a dummy in jsfiddle.net.https://jsfiddle.net/cezar77/6u1waqso/2

虚拟"数据如下所示:

const projects = [
    {
        'name': 'foo',
        'team': 'operations',
        'start_date': '2018-01-01',
        'end_date': '2019-12-31'
    },
    {
        'name': 'bar',
        'team': 'operations',
        'start_date': '2017-01-01',
        'end_date': '2018-12-31'
    },
    {
        'name': 'abc',
        'team': 'operations',
        'start_date': '2018-01-01',
        'end_date': '2018-08-31'
    },
    {
        'name': 'xyz',
        'team': 'devops',
        'start_date': '2018-04-01',
        'end_date': '2020-12-31'
    },
    {
        'name': 'wtf',
        'team': 'devops',
        'start_date': '2018-01-01',
        'end_date': '2019-09-30'
    },
    {
        'name': 'qwerty',
        'team': 'frontend',
        'start_date': '2017-01-01',
        'end_date': '2019-01-31'
    },
    {
        'name': 'azerty',
        'team': 'marketing',
        'start_date': '2016-01-01',
        'end_date': '2019-08-31'
    },
    {
        'name': 'qwertz',
        'team': 'backend',
        'start_date': '2018-05-01',
        'end_date': '2019-12-31'
    },
    {
        'name': 'mysql',
        'team': 'database',
        'start_date': '2015-01-01',
        'end_date': '2017-09-15'
    },
    {
        'name': 'postgresql',
        'team': 'database',
        'start_date': '2016-01-01',
        'end_date': '2018-12-31'
    }
];

时间显示在 x 轴上,从 start_dateend_date 的每个项目都有一个水平条.

The time is displayed on the x axis and there is a horizontal bar for every project stretching from the start_date to the end_date.

在左侧,在 y 轴上,我想显示团队(参见 jsfiddle 左侧的标签)并为每个团队创建一个网格线,将项目组.由于每个团队的项目数量不同,网格线应放置在不同的距离.

On the left side, on the y axis, I'd like to display the teams (see the labels on the left side in the jsfiddle) and create a gridline for each team, separating the groups of projects. Because each team has a different number of projects, the gridlines should be placed at different distances.

我尝试在偶然情况下使用阈值量表:

I tried to use a threshold scale on the off chance:

const yScale = d3.scaleThreshold()
  .domain(data.map(d => d.values.length))
  .range(data.map(d => d.key));

const yAxis = d3.axisLeft(yScale);

但是当我调用它时:

svg.append('g')
  .attr('class', 'y-axis')
  .call(yAxis);

它抛出一个错误.

为此目的使用刻度和轴是否合适?如果是,我应该如何解决这个问题?

Is it appropriate to use a scale and axis for this purpose? If yes, how should I approach the problem?

如果使用刻度和轴是错误的方法,D3.js 是否为此提供了其他方法?

If using a scale and axis is a wrong approach, are there any other methods provided by D3.js for this purpose?

推荐答案

是的,您可以使用比例来处理这个问题,如果数据总是分组的,您可以尝试保存每个分组值的偏移量.我们可以通过规模或仅使用数据来实现.

Yeah you can use a scale to handle that, if the data is always grouped you can try saving the offset of each grouped value. We can do it with the scale or just using the data.

创建一个规模将是这样的:

Creating a scale would be something like this:

const yScale = d3.scaleOrdinal()
  .range(data.reduce((acc, val, index, arr) => {
    if (index > 0) {
      acc.push(arr[index - 1].values.length + acc[acc.length - 1]);
    } else {
      acc.push(0);
    }
    return acc;
  }, []))
  .domain(data.map(d => d.key));

有了这个,我们可以使用比例获得偏移量.我们使用 scaleOrdinal 因为我们想要一个 1 对 1 的映射.来自文档:

With this we can get the offset using a scale. We are using scaleOrdinal since we want a 1-to-1 mapping. From the docs:

与连续尺度不同,序数尺度具有离散域和范围.例如,序数比例可能将一组命名类别映射到一组颜色,或者确定柱形图中列的水平位置.

如果我们检查新的 yScale,我们可以看到以下内容:

If we check our new yScale we can see the following:

console.log(yScale.range());       // Array(6) [ 0, 4, 5, 8, 9, 11 ]
console.log(yScale.domain());      // Array(6) [ "database", "marketing", "operations", "frontend", "devops", "backend" ]
console.log(yScale("database"));   // 0
console.log(yScale("marketing"));  // 4

我们也可以尝试将偏移量添加到数据中并实现相同的效果:

We could also try just adding the offset into the data and achieve the same:

const teams = svg.selectAll('g.group__team')
  .data(d => {
    let offset = 0;
    return data.map((d, i) => {
      if(i > 0) offset+= data[i - 1].values.length;
      return {
        ...d,
        offset
      };
    })
  })

我们只需简单地创建组并使用偏移量翻译它们:

With that we just simply create groups and translate them using the offset:

const teams = svg.selectAll('g.group__team')
  .data(d => {
    let offset = 0;
    return data.map((d, i) => {
      if (i > 0) offset += data[i - 1].values.length;
      return {
        ...d,
        offset
      };
    })
  })
  .join('g')
  .attr('class', d => 'group__team ' + d.key)
  .attr('transform', d => `translate(${[0, yScale(d.key) * barHeight]})`) // using scale
  .attr('transform', d => `translate(${[0, d.offset * barHeight]})`)      // using our data

现在让我们渲染每个项目:

Now lets render each project:

teams.selectAll('rect.group__project')
  .data(d => d.values)
  .join('rect')
  .attr('class', d => 'group__project ' + d.team)
  .attr('x', d => margin.left + xScale(d3.isoParse(d.start_date)))
  .attr('y', (d, i) => margin.top + i * barHeight)
  .attr('width', d => xScale(d3.isoParse(d.end_date)) - xScale(d3.isoParse(d.start_date)))
  .attr('height', barHeight);

这应该呈现我们所有相对于我们组的矩形.现在让我们处理标签:

This should render all our rects relative to our group. Now lets deal with the labels:

teams.selectAll('text.group__name')
  .data(d => [d])
  .join('text')
  .attr('class', 'group__name')
  .attr('x', 5)
  .attr('y', (d, i) => margin.top + (d.values.length * barHeight) / 2) // Get half of the sum of the project bars in the team
  .attr('dy', '6px')

最后渲染团队分隔符:

teams.selectAll('line.group__delimiter')
  .data(d => [d])
  .join('line')
  .attr('class', 'line group__delimiter')
  .attr('x1', margin.left)
  .attr('y1', (d, i) => margin.top + (d.values.length * barHeight))
  .attr('x2', viewport.width)
  .attr('y2', (d, i) => margin.top + (d.values.length * barHeight))
  .attr('stroke', '#222')
  .attr('stroke-width', 1)
  .attr('stroke-dasharray', 10);

JSfiddle 工作代码

完整代码:

const projects = [{
    'name': 'foo',
    'team': 'operations',
    'start_date': '2018-01-01',
    'end_date': '2019-12-31'
  },
  {
    'name': 'bar',
    'team': 'operations',
    'start_date': '2017-01-01',
    'end_date': '2018-12-31'
  },
  {
    'name': 'abc',
    'team': 'operations',
    'start_date': '2018-01-01',
    'end_date': '2018-08-31'
  },
  {
    'name': 'xyz',
    'team': 'devops',
    'start_date': '2018-04-01',
    'end_date': '2020-12-31'
  },
  {
    'name': 'wtf',
    'team': 'devops',
    'start_date': '2018-01-01',
    'end_date': '2019-09-30'
  },
  {
    'name': 'qwerty',
    'team': 'frontend',
    'start_date': '2017-01-01',
    'end_date': '2019-01-31'
  },
  {
    'name': 'azerty',
    'team': 'marketing',
    'start_date': '2016-01-01',
    'end_date': '2019-08-31'
  },
  {
    'name': 'qwertz',
    'team': 'backend',
    'start_date': '2018-05-01',
    'end_date': '2019-12-31'
  },
  {
    'name': 'mysql',
    'team': 'database',
    'start_date': '2015-01-01',
    'end_date': '2017-09-15'
  },
  {
    'name': 'postgresql',
    'team': 'database',
    'start_date': '2016-01-01',
    'end_date': '2018-12-31'
  },
  {
    'name': 'mysql',
    'team': 'database',
    'start_date': '2018-05-01',
    'end_date': '2019-12-31'
  },
  {
    'name': 'mysql',
    'team': 'database',
    'start_date': '2018-05-01',
    'end_date': '2019-12-31'
  },
]

// Process data
projects.sort((a, b) => d3.ascending(a.start_date, b.start_date));

const data = d3.nest()
  .key(d => d.team)
  .entries(projects);

const flatData = d3.merge(data.map(d => d.values));

// Configure dimensions
const
  barHeight = 16,
  margin = {
    top: 50,
    left: 100,
    right: 20,
    bottom: 50
  },
  chart = {
    width: 1000,
    height: projects.length * barHeight
  },
  viewport = {
    width: chart.width + margin.left + margin.right,
    height: chart.height + margin.top + margin.bottom
  },
  tickBleed = 5,
  labelPadding = 10;

// Configure scales and axes
const xMin = d3.min(
  flatData,
  d => d3.isoParse(d.start_date)
);
const xMax = d3.max(
  flatData,
  d => d3.isoParse(d.end_date)
);

const xScale = d3.scaleTime()
  .range([0, chart.width])
  .domain([xMin, xMax]);

const xAxis = d3.axisBottom(xScale)
  .ticks(20)
  .tickSize(chart.height + tickBleed)
  .tickPadding(labelPadding);

const yScale = d3.scaleOrdinal()
  .range(data.reduce((acc, val, index, arr) => {
    if (index > 0) {
      acc.push(arr[index - 1].values.length + acc[acc.length - 1]);
    } else {
      acc.push(0);
    }
    return acc;
  }, []))
  .domain(data.map(d => d.key));

console.log(yScale.range());
console.log(yScale.domain());
console.log(yScale("database"));
console.log(yScale("marketing"));

const yAxis = d3.axisLeft(yScale);

// Draw SVG
const svg = d3.select('body')
  .append('svg')
  .attr('width', viewport.width)
  .attr('height', viewport.height);

svg.append('g')
  .attr('class', 'x-axis')
  .call(xAxis);

d3.select('.x-axis')
  .attr(
    'transform',
    `translate(${[margin.left, margin.top]})`
  );

d3.select('.x-axis .domain')
  .attr(
    'transform',
    `translate(${[0, chart.height]})`
  );

const chartArea = svg.append('rect')
  .attr('x', margin.left)
  .attr('y', margin.top)
  .attr('width', chart.width)
  .attr('height', chart.height)
  .style('fill', 'red')
  .style('opacity', 0.1)
  .style('stroke', 'black')
  .style('stroke-width', 1);

const teams = svg.selectAll('g.group__team')
  .data(d => {
    let offset = 0;
    return data.map((d, i) => {
      if (i > 0) offset += data[i - 1].values.length;
      return {
        ...d,
        offset
      };
    })
  })
  .join('g')
  .attr('class', d => 'group__team ' + d.key)
  .attr('transform', d => `translate(${[0, yScale(d.key) * barHeight]})`)
  .attr('transform', d => `translate(${[0, d.offset * barHeight]})`)
  .on('mouseenter', d => {
    svg.selectAll('.group__team')
      .filter(team => d.key != team.key)
      .attr('opacity', 0.2);
  })
  .on('mouseleave', d => {
    svg.selectAll('.group__team')
      .attr('opacity', 1);
  })

teams.selectAll('rect.group__project')
  .data(d => d.values)
  .join('rect')
  .attr('class', d => 'group__project ' + d.team)
  .attr('x', d => margin.left + xScale(d3.isoParse(d.start_date)))
  .attr('y', (d, i) => margin.top + i * barHeight)
  .attr('width', d => xScale(d3.isoParse(d.end_date)) - xScale(d3.isoParse(d.start_date)))
  .attr('height', barHeight);


teams.selectAll('text.group__name')
  .data(d => [d])
  .join('text')
  .attr('class', 'group__name')
  .attr('x', 5)
  .attr('y', (d, i) => margin.top + (d.values.length * barHeight) / 2)
  .attr('dy', '6px')
  .text(d => d.key);

teams.selectAll('line.group__delimiter')
  .data(d => [d])
  .join('line')
  .attr('class', 'line group__delimiter')
  .attr('x1', margin.left)
  .attr('y1', (d, i) => margin.top + (d.values.length * barHeight))
  .attr('x2', viewport.width)
  .attr('y2', (d, i) => margin.top + (d.values.length * barHeight))
  .attr('stroke', '#222')
  .attr('stroke-width', 1)
  .attr('stroke-dasharray', 10)



/**
svg.append('g')
    .attr('class', 'y-axis')
  .call(yAxis);
*/

这篇关于在 D3.js 中为不同宽度的波段创建比例的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-05 20:54
查看更多