cez*_*zar 6 javascript svg data-visualization d3.js
我们有项目,分配给不同的团队。现在我必须创建项目时间表。
出于这个问题的目的,我在 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'
}
];
Run Code Online (Sandbox Code Playgroud)
时间显示在x轴上,从start_date
到 的每个项目都有一个水平条end_date
。
在左侧,在y轴上,我想显示团队(参见 jsfiddle 左侧的标签)并为每个团队创建一个网格线,将项目组分开。由于每个团队的项目数量不同,网格线应放置在不同的距离处。
我尝试在偶然情况下使用阈值量表:
const yScale = d3.scaleThreshold()
.domain(data.map(d => d.values.length))
.range(data.map(d => d.key));
const yAxis = d3.axisLeft(yScale);
Run Code Online (Sandbox Code Playgroud)
但是当我调用它时:
svg.append('g')
.attr('class', 'y-axis')
.call(yAxis);
Run Code Online (Sandbox Code Playgroud)
它抛出一个错误。
为此目的使用刻度和轴是否合适?如果是,我应该如何解决这个问题?
如果使用比例尺和轴是错误的方法,D3.js 是否为此提供了其他方法?
是的,您可以使用比例来处理该问题,如果数据始终是分组的,您可以尝试保存每个分组值的偏移量。我们可以使用比例或仅使用数据来完成。
创建一个规模将是这样的:
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));
Run Code Online (Sandbox Code Playgroud)
有了这个,我们可以使用比例获得偏移量。我们正在使用,scaleOrdinal
因为我们想要一个 1 对 1 的映射。从文档:
与连续尺度不同,序数尺度具有离散域和范围。例如,序数比例可能将一组命名类别映射到一组颜色,或确定柱形图中列的水平位置。
如果我们检查我们的新 yScale,我们可以看到以下内容:
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
Run Code Online (Sandbox Code Playgroud)
我们也可以尝试将偏移量添加到数据中并实现相同的效果:
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
};
})
})
Run Code Online (Sandbox Code Playgroud)
有了这个,我们只需简单地创建组并使用偏移量转换它们:
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
Run Code Online (Sandbox Code Playgroud)
现在让我们渲染每个项目:
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);
Run Code Online (Sandbox Code Playgroud)
这应该呈现我们所有的矩形相对于我们的组。现在让我们处理标签:
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')
Run Code Online (Sandbox Code Playgroud)
最后渲染团队的分隔符:
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);
Run Code Online (Sandbox Code Playgroud)
完整代码:
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);
*/
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
895 次 |
最近记录: |