甘特图 - 限制画笔的大小并在开始时自动选择时间线的一部分

问题描述 投票:0回答:1

我已经构建了一个 d3.js 甘特图 - 但我正在努力让画笔成为固定宽度/较小的范围 - 加载时 - 仅显示时间线的一小部分,而不是整个时间线。

我希望图表像这样加载 - 而不是全局/全宽。

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
javascript d3.js
1个回答
0
投票

相关代码行在这里:

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)

更新了沙盒

© www.soinside.com 2019 - 2024. All rights reserved.