提问人:The Old County 提问时间:11/4/2023 更新时间:11/4/2023 访问量:33
甘特图 - 限制画笔的大小,并在开始时自动选择时间线的一小部分
Gannt chart - limiting the size of the brush and auto selecting a fraction of the timeline at the start
问:
我已经构建了一个 d3.js gannt 图表 - 但我正在努力让画笔成为固定宽度/更小的范围 - 加载时 - 仅显示时间线的一小部分,而不是整个时间线。
我希望图表像这样加载 - 而不是全局/全角。
https://codesandbox.io/s/objective-villani-qngrlj
我尝试操作 x1.range() - 或添加范围 - 或尝试修改刷过的函数 - 但我要么找到了非常古老的示例,要么它破坏了我拥有的代码库。
https://observablehq.com/@d3/click-to-recenter-brush?collection=@d3/d3-brush
代码库目前
import React from 'react'
import * as d3 from 'd3'
//import './GanttChart1.scss'
class GanttChart extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
this.state = {
data: this.props.data ? this.props.data : [],
theme: this.props.theme
? this.props.theme
: ['#bde0fe', '#2698f9', '#71bcfd', '#f1f8fe'],
}
}
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.data !== this.state.data) {
//console.log("PROPS HAVE CHANGED FOR CHART");
this.setState({data: nextProps.data})
this.buildChart()
}
}
componentDidMount() {
this.buildChart()
}
buildChart() {
var $this = this.myRef.current
d3.select($this).selectAll('svg').remove()
//var data = this.props.data;
//data for chart
var data = [
{
label: 'person a',
avatar:
'https://cdn.britannica.com/61/137461-050-BB6C5D80/Brad-Pitt-2008.jpg',
times: [
{
text: 'Test 1',
starting_time: 1355752800000,
ending_time: 1355759900000,
},
{
text: 'Test 2',
starting_time: 1355767900000,
ending_time: 1355774400000,
},
],
},
{
label: 'person b',
avatar:
'https://media.onvoitout.fr/2021/10/Compress_20211028_135754_4577.jpg',
times: [
{
text: 'Test 3',
starting_time: 1355767900000,
ending_time: 1355774400000,
},
],
},
{
label: 'person c',
avatar:
'https://ichef.bbci.co.uk/news/624/mcs/media/images/77876000/jpg/_77876037_cara2.jpg',
times: [
{
text: 'Test 4',
starting_time: 1355761910000,
ending_time: 1355763910000,
},
],
},
{
label: 'person d',
avatar:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQLThUPE00PBf-es9Y3teevd5qws4O1-5vQSZEYvF6I4IyZVi9tfi4Wt_sBKgbXbEhvycs&usqp=CAU',
times: [
{
text: 'Test 5',
starting_time: 1355861000000,
ending_time: 1355865900000,
},
],
},
{
label: 'person e',
avatar:
'https://www.wellandgood.com/wp-content/uploads/2022/09/celebrity-beauty-brands-425x285.jpg',
times: [
{
text: 'Test 11',
starting_time: 1355861900000,
ending_time: 1355864520000,
},
{
text: 'Test 12',
starting_time: 1355777900000,
ending_time: 1355784400000,
},
],
},
{
label: 'person e',
avatar:
'https://www.shutterstock.com/image-photo/cannes-france-may-21-leonardo-260nw-1433831474.jpg',
times: [
{
text: 'Test 11',
starting_time: 1355752800000,
ending_time: 1355759900000,
},
{
text: 'Test 12',
starting_time: 1355967900000,
ending_time: 1355994400000,
},
],
},
]
var tooltip = d3
.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0)
.style('display', 'none')
//setting data formate for chart
var lanes = []
var times = []
var avatars = []
//var data = this.state.data;
data.forEach((value, index) => {
lanes.push(value.label)
avatars.push(value.avatar)
value.times.forEach((v, i) => {
v['lane'] = index
})
times.push(value.times)
})
var laneLength = lanes.length
var items = [].concat.apply([], times)
items.forEach((v, i) => {
v['id'] = i
})
var timeBegin = d3.min(items, function (d) {
return d['starting_time']
})
var timeEnd = d3.max(items, function (d) {
return d['ending_time']
})
var width = parseInt(this.props.width, 10),
height = parseInt(this.props.height, 10)
var color = d3.scaleOrdinal().range(this.state.theme)
var parseTime = d3.timeParse('%Y%m%d')
var m = [20, 85, 10, 190], //top right bottom left
w = width - m[1] - m[3],
h = height - m[0] - m[2],
miniHeight = laneLength * 12 + 50,
mainHeight = h - miniHeight - 50
//scales
var x = d3.scaleTime().range([0, w]).domain([timeBegin, timeEnd])
var x1 = d3.scaleLinear().range([0, w])
var y1 = d3.scaleLinear().range([0, mainHeight]).domain([0, laneLength])
var y2 = d3.scaleLinear().range([0, miniHeight]).domain([0, laneLength])
var scaleFactor = (1 / (timeEnd - timeBegin)) * w
var chart = d3
.select($this)
.append('svg')
.attr('width', w + m[1] + m[3])
.attr('height', h + m[0] + m[2])
.attr('class', 'chart')
chart
.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', w)
.attr('height', mainHeight)
var main = chart
.append('g')
.attr('transform', 'translate(' + m[3] + ',' + m[0] + ')')
.attr('width', w)
.attr('height', mainHeight)
.attr('class', 'main')
var mini = chart
.append('g')
.attr(
'transform',
'translate(' + m[3] + ',' + (mainHeight + m[0] + 13) + ')',
)
.attr('width', w)
.attr('height', miniHeight)
.attr('class', 'mini')
//background colors
function colores_background(n) {
var colores_g = ['#f8dd2f', '#e9168a', '#448875', '#2b2d39', '#c3bd75']
return colores_g[n % colores_g.length]
}
//foreground colors
function colores_foreground(n) {
var colores_g = ['#553814', '#311854', '#f7b363', '#c12f39', '#89191d']
return colores_g[n % colores_g.length]
}
//main lanes and texts
main
.append('g')
.selectAll('.laneLines')
.data(items)
.enter()
.append('line')
.attr('x1', 0)
.attr('y1', function (d) {
return y1(d.lane)
})
.attr('x2', w)
.attr('y2', function (d) {
return y1(d.lane)
})
.attr('stroke', 'lightgray')
var defs = main.append('svg:defs')
main
.append('g')
.selectAll('.laneText')
.data(lanes)
.enter()
.append('text')
.text(function (d) {
return d
})
.attr('x', -m[1] + 10)
.attr('y', function (d, i) {
return y1(i + 0.5)
})
.attr('dy', '.5ex')
.attr('text-anchor', 'end')
.attr('class', 'laneText')
//mini lanes and texts
mini
.append('g')
.selectAll('.laneLines')
.data(items)
.enter()
.append('line')
.attr('x1', 0)
.attr('y1', function (d) {
return y2(d.lane)
})
.attr('x2', w)
.attr('y2', function (d) {
return y2(d.lane)
})
.attr('stroke', 'lightgray')
mini
.append('g')
.selectAll('.laneText')
.data(lanes)
.enter()
.append('text')
.text(function (d) {
return d
})
.attr('x', -m[1] + 40)
.attr('y', function (d, i) {
return y2(i + 0.5)
})
.attr('dy', '.5ex')
.attr('text-anchor', 'end')
.attr('class', 'laneText')
var itemRects = main.append('g').attr('clip-path', 'url(#clip)')
//mini item rects
mini
.append('g')
.selectAll('miniItems')
.data(items)
.enter()
.append('rect')
.attr('class', function (d) {
return 'miniItem'
})
.attr('x', function (d) {
return x(d.starting_time)
})
.attr('y', function (d) {
return y2(d.lane + 0.5) - 5
})
.attr('fill', function (d, i) {
return colores_background(d.lane)
})
.attr('width', function (d) {
return (d.ending_time - d.starting_time) * scaleFactor
})
.attr('height', 10)
//this is for avatars user images
avatars.forEach((value, index) => {
defs
.append('svg:pattern')
.attr('id', '--' + index)
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('image-rendering', 'optimizeQuality')
.attr('preserveAspectRatio', 'xMidYMid meet')
.attr('xlink:href', value)
.attr('x', 0)
.attr('y', 0)
.attr('width', 50)
.attr('height', 50)
defs
.append('svg:pattern')
.attr('id', '--m' + index)
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('image-rendering', 'optimizeQuality')
.attr('preserveAspectRatio', 'xMidYMid meet')
.attr('xlink:href', value)
.attr('x', 0)
.attr('y', 0)
.attr('width', 20)
.attr('height', 20)
//draw the x axis on time
main
.append('g')
.attr('transform', function (d, i) {
return 'translate(' + (-m[1] + 5) + ',' + (y1(index + 0.5) - 50) + ')'
})
.append('circle')
.attr('class', 'user-avatar')
//.style("stroke", "gray")
.style('fill', 'url(#--' + index + ')')
.attr('r', 25)
.attr('cx', 40)
.attr('cy', 50)
mini
.append('g')
.attr('transform', function (d, i) {
return (
'translate(' + (-m[1] + 40) + ',' + (y2(index + 0.5) - 20) + ')'
)
})
.append('circle')
.attr('class', 'user-avatar')
//.style("stroke", "gray")
.style('fill', 'url(#--m' + index + ')')
.attr('r', 10)
.attr('cx', 20)
.attr('cy', 20)
})
//draw x axis
function toDays(d) {
d = d || 0
return d / 24 / 60 / 60 / 1000
}
function toUTC(d) {
if (!d || !d.getFullYear) return 0
return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
}
//find best range
var days = daysBetween(new Date(timeBegin), new Date(timeEnd))
function daysBetween(d1, d2) {
return toDays(toUTC(d2) - toUTC(d1))
}
var tFormat1 = '%Y-%m'
var tTick1 = 'timeMonths'
if (days < 40) {
tFormat1 = '%Y-%m-%d'
tTick1 = 'timeWeek'
}
if (days <= 7) {
tFormat1 = '%b %Y'
tTick1 = 'timeDay'
}
if (days <= 1) {
tFormat1 = '%H%M'
tTick1 = 'timeHour'
}
// draw the x axis on date and years
var xMonthAxis = d3
.axisTop(x)
.tickArguments(d3[tTick1], 1)
.tickFormat(d3.timeFormat(tFormat1))
.tickSize(15)
//for years on top
mini
.append('g')
.attr('transform', 'translate(0,0.5)')
.attr('class', 'axis month')
.call(xMonthAxis)
.selectAll('text')
.attr('dx', 25)
.attr('dy', 15)
var gX = chart
.append('g')
.attr('class', 'axis axis-x')
.attr('transform', 'translate(' + m[3] + ',' + 450 + ')')
.call(d3.axisBottom(x))
//draw x axis
//call brush function
var brush = d3
.brushX()
.extent([
[0, 0],
[w, miniHeight],
])
.on('brush', brushed)
mini
.append('g')
.attr('class', 'x brush')
.call(brush)
.call(brush.move, x1.range())
.selectAll('rect')
.attr('y', 1)
.attr('height', miniHeight - 1)
//create brush function redraw scatterplot with selection
function brushed(event) {
var selection = event.selection
var timeSelection = selection.map(x.invert, x)
var rects
var labels
var minExtent = timeSelection[0]
var maxExtent = timeSelection[1]
var visItems = items.filter(function (d) {
return d.starting_time < maxExtent && d.ending_time > minExtent
})
x1.domain([minExtent, maxExtent])
//update main item rects
rects = itemRects
.selectAll('rect')
.data(visItems, function (d) {
return d.id
})
.attr('x', function (d) {
return x1(d.starting_time)
})
.attr('width', function (d) {
return x1(d.ending_time) - x1(d.starting_time)
})
rects
.enter()
.append('rect')
.attr('class', function (d) {
return 'miniItem'
})
.attr('x', function (d) {
return x1(d.starting_time)
})
.attr('y', function (d) {
return y1(d.lane) + 5
})
.attr('fill', function (d, i) {
return colores_background(d.lane)
})
.attr('width', function (d) {
return x1(d.ending_time) - x1(d.starting_time)
})
.attr('height', function (d) {
return 0.8 * y1(1)
})
.on('mouseover', function (d, i) {
tooltip
.transition()
.duration(200)
.style('opacity', 0.9)
.style('display', 'block')
tooltip
.html(`${d.target.__data__.text}`)
.style('left', d.pageX + 5 + 'px')
.style('top', d.pageY + 5 + 'px')
})
.on('mouseout', function (d) {
tooltip
.transition()
.duration(500)
.style('opacity', 0)
.style('display', 'none')
})
rects.exit().remove()
/*
//update the item labels
labels = itemRects.selectAll("text")
.data(visItems, function(d) {
return d.id;
})
.attr("x", function(d) {
return x1(Math.max(d.starting_time, minExtent) + 2);
});
labels.enter().append("text")
.text(function(d) {
return d.text;
})
.attr("x", function(d) {
return x1(Math.max(d.starting_time, minExtent));
})
.attr("y", function(d) {
return y1(d.lane + .5);
})
.attr("fill", function(d, i) {
return colores_foreground(d.lane);
})
.attr("text-anchor", "start");
labels.exit().remove();
*/
}
}
render() {
return <div ref={this.myRef} className="GanttChart" />
}
}
export default GanttChart
答:
1赞
Mark
11/4/2023
#1
相关代码行如下:
mini
.append('g')
.attr('class', 'x brush')
.call(brush)
.call(brush.move, x1.range()) // <-- this sets the initial brush width
.selectAll('rect')
.attr('y', 1)
.attr('height', miniHeight - 1)
只需将其设置为较小的值即可:
mini
.append('g')
.attr('class', 'x brush')
.call(brush)
.call(brush.move, [0,100]) // <-- smaller initial brush width
.selectAll('rect')
.attr('y', 1)
.attr('height', miniHeight - 1)
更新了沙盒。
评论
0赞
The Old County
11/4/2023
好的,太好了 - 所以它是 0-100px - 我不确定如果我只是这样更改宽度,它是否会捕捉到放大的数据集 - 我们不需要在画笔函数中使用一些东西来放大较小的数据带吗?
1赞
Mark
11/4/2023
正在调用 brushed 函数并执行缩放。.call(brush.move...
0赞
The Old County
11/5/2023
这太棒了,谢谢马克。我尝试了各种各样的事情。
评论