将Bostock的自动换行功能应用于文本

时间:2019-04-26 08:12:43

标签: javascript d3.js svg

我有一个用例,我想在视觉的不同部分附加一小段或中段的文字。作为默认行为,这看起来非常难看,因为svg文本只是被添加到了一起。因此,经过一番研究,我发现Mike Bostock创建了一种巧妙的方式来处理svg文本中的较长字符串,可以在here中看到。我试图使此功能适应我的特定视觉效果,但效果并不理想。这是代码段:

  

 var margins = {top:20, left:50, bottom:100, right:20};

var width = 1200;
var height = 500;

var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;

var svg = d3.select('body')
    .append('svg')
    .attr('width', totalWidth)
    .attr('height', totalHeight);

var graphGroup = svg.append('g')
    .attr('transform', "translate("+margins.left+","+margins.top+")");

var rawData = [
  {'date':'Dec-02-2018', 'regulator':'CBIRC', 'penalty':false, 'summary':'Finalized bank wealth management subsidiary rules allow equity investments'},
  {'date':'Nov-28-2018', 'regulator':'CSRC', 'penalty':false, 'summary':"Brokerage's retail-targeted, pooled asset management products required to follow mutual fund standards"},
  {'date':'Dec-14-2018', 'regulator':'CSRC', 'penalty':false, 'summary':'Regulators issue window guidance to stop FMCs from promoting short-term performance of pension funds'},
  {'date':'Dec-19-2018', 'regulator':'CSRC', 'penalty':false, 'summary':'CSRC issues information technology magement rules'},
  {'date':'Dec-25-2018', 'regulator':'AMAC', 'penalty':false, 'summary':'AMAC issues guidelines on bond-trading'},
  {'date':'Jan-11-2019', 'regulator':'SZSE', 'penalty':false, 'summary':'SZSE revises trading rules for certain ETFs'},
  {'date':'Jan-18-2019', 'regulator':'CSRC', 'penalty':false, 'summary':'CSRC issues guidelines on mutual fund investment info credit derivatives, while AMAC issues affiliated valuation guidelines'},
  {'date':'Jan-26-2019', 'regulator':'CSRC', 'penalty':false, 'summary':'Yi Huiman appointed as CSRC party secretary and chairman'},
  {'date':'Jan-28-2019', 'regulator':'CSRC', 'penalty':false, 'summary':'CSRC publishes draft rules for the new technology innovation board, which will be paired with a registration-based IPO system'},
  {'date':'Jan-22-2019', 'regulator':'CSRC', 'penalty':true, 'summary':'Several third-party fund distribution institutions punished by CSRC for incompliant distribution and reporting'},
  {'date':'Jan-31-2019', 'regulator':'PBoC', 'penalty':true, 'summary':'ICBC Credit Suisse punished by PBoC for mishandling customer information'}
];

var parseDate = d3.timeParse("%b-%d-%Y");

var formatTime = d3.timeFormat("%b %d, %Y");

var data = rawData.map(function(d) {
    return  {date:parseDate(d.date), regulator:d.regulator, penalty:d.penalty, summary:d.summary}
});

data.sort(function(x, y){
   return d3.ascending(x.date, y.date);
});

//var earliest = d3.min(data.map(d=>d.date));
//var latest = d3.max(data.map(d=>d.date));

var dateMin = d3.min(data, function(d){
    return d3.timeDay.offset(d.date, -10);
});

var dateMax = d3.max(data, function(d){
    return d3.timeDay.offset(d.date, +10);
});

var timeScale = d3.scaleTime()
    .domain([dateMin, dateMax])
    .range([0, width]);

var colorMap = {
  'CSRC':'#003366',
  'CBIRC':'#e4a733',
  'AMAC':'#95b3d7',
  'SZSE':'#b29866',
  'PBoC':'#366092'
};

var defs = svg.append('svg:defs');
var fillURL = "Fills/gray-1-crosshatch.svg";


defs.append("svg:pattern")
    .attr("id", "gray_hatch")
    .attr("width", 10)
    .attr("height", 10)
    .attr("patternUnits", "userSpaceOnUse")
    .append("svg:image")
    .attr("xlink:href", fillURL)
    .attr("width", 10)
    .attr("height", 10)
    .attr("x", 0)
    .attr("y", 0);

graphGroup.append('rect')
    .attr('width', width)
    .attr('height', 80)
    .attr('x', 0)
    .attr('y', height*.75)
    .style('fill', "url(#gray_hatch)");

graphGroup.append('rect')
    .attr('width', width)
    .attr('height', 20)
    .attr('x', 0)
    .attr('y', height*.75+30)
    .style('fill', "#a6a6a6");

graphGroup.append('rect')
    .attr('width',8)
    .attr('height',80)
    .attr('x',0)
    .attr('y',height*.75)
    .style('fill', "#a6a6a6");

graphGroup.append('rect')
    .attr('width',8)
    .attr('height',80)
    .attr('x',width)
    .attr('y',height*.75)
    .style('fill', "#a6a6a6");

graphGroup.selectAll('circle')
    .data(data)
    .enter()
    .append('circle')
    .attr('cx', function(d) {return timeScale(d.date)})
    .attr('cy', height*.75+40)
    .attr('r', 10)
    .style('fill', function(d) {return colorMap[d.regulator]});
    
graphGroup.selectAll('line')
    .data(data.filter(function(d) {return d.penalty==false}))
    .enter()
    .append('line')
    .attr('x1', function(d) {return timeScale(d.date)})
    .attr('x2', function(d) {return timeScale(d.date)})
    .attr('y1', function(d) {return height*.75+40})
    .attr('y2', function(d,i) {
      if (i%2) {
        return 50;
      } else {
        return height/2;
      }
      })
    .style('stroke', function(d) {return colorMap[d.regulator]})
    .style('stroke-width', '2px');

graphGroup.selectAll('.labelRects')
    .data(data.filter(function(d) {return d.penalty==false}))
    .attr('class', 'labelRects')
    .enter()
    .append('rect')
    .attr('width', 125)
    .attr('height', 10)
    .attr('x', function(d) {return timeScale(d.date)-125})
    .attr('y', function(d,i) {
      if (i%2) {
        return 50;
      } else {
        return height/2;
      }
      })
    .style('fill', function(d) { return colorMap[d.regulator]});

graphGroup.selectAll('text')
    .data(data.filter(function(d) {return d.penalty==false}))
    .enter()
    .append('text')
    .attr('x', function(d) {return timeScale(d.date)-125})
    .attr('y', function(d,i) {
      if (i%2) {
        return 50-5;
      } else {
        return height/2-5;
      }
      })
    .text(function(d) {return formatTime(d.date)})
    //.attr('text-anchor','middle')
    .attr('class', 'date');


    function wrap(text, width) {

      text.each(function() {
        var text = d3.select(this),
            words = text.text().split(/\s+/).reverse(),
            word,
            line = [],
            lineNumber = 0,
            lineHeight = 1.1, // ems
            y = text.attr("y"),
            dy = parseFloat(text.attr("dy")),
            tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
        while (word = words.pop()) {
          line.push(word);
          tspan.text(line.join(" "));
          if (tspan.node().getComputedTextLength() > width) {
            line.pop();
            tspan.text(line.join(" "));
            line = [word];
            tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
          }
        }
      });
    }

graphGroup.selectAll('.labelText')
    .data(data.filter(function(d) {return d.penalty==false}))
    .attr('class', 'labelText')
    .enter()
    .append('text')
    .attr('x', function(d) {return timeScale(d.date)-125})
    .attr('y', function(d,i) {
      if (i%2) {
        return 50+20;
      } else {
        return height/2+20;
      }
      })
    .text(function(d) {return d.summary})
    .style('font-size','12px');

d3.selectAll('.labelText')
  .call(wrap, 120);
text {
  font-family: Tw Cen MT;

}

.date {
    font-size: 18px;
    paint-order: stroke;
    stroke: #fff;
    stroke-width: 3px;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    font-weight: 800;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

对我来说,从概念上讲,我做了Bostock在示例中所做的一切(我认为),但是文本似乎并没有正确地通过函数,也没有发生换行-也没有错误。 / p>

问题

是否有可能使Bostock的tspan wrap函数适合一般情况?如果是这样,如果它没有选择文本并调用函数并设置所需的宽度,该怎么办?

进一步的说明:

  • 我的字体大小:12px
  • 我想要的宽度:120

奖励积分:

  • 所需文本对齐:正确(看来Bostock的文本居中)

1 个答案:

答案 0 :(得分:3)

这里的主要问题是您要在输入方法之前设置类:

graphGroup.selectAll('.labelText')
    .data(data.filter(function(d) {return d.penalty==false}))
    .attr('class', 'labelText')
    .enter()
    .append('text')
    //etc...

应该是:

graphGroup.selectAll('.labelText')
  .data(data.filter(function(d) {
    return d.penalty == false
  }))
  .enter()
  .append('text')
  .attr('class', 'labelText')
  //etc...

因此,您的d3.selectAll('.labelText')为空(即其size()为零)。

然后,我们必须做一些小的调整才能使用wrap函数:

  1. text-anchor设置为end,并在x位置删除填充;
  2. wrap函数中,获取文本的x位置...

    x = text.attr("x")
    

    并在tspan中使用它们:

    .attr("x", x)
    

这是您更新的代码段:

var margins = {
  top: 20,
  left: 50,
  bottom: 100,
  right: 20
};

var width = 1200;
var height = 500;

var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;

var svg = d3.select('body')
  .append('svg')
  .attr('width', totalWidth)
  .attr('height', totalHeight);

var graphGroup = svg.append('g')
  .attr('transform', "translate(" + margins.left + "," + margins.top + ")");

var rawData = [{
    'date': 'Dec-02-2018',
    'regulator': 'CBIRC',
    'penalty': false,
    'summary': 'Finalized bank wealth management subsidiary rules allow equity investments'
  },
  {
    'date': 'Nov-28-2018',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': "Brokerage's retail-targeted, pooled asset management products required to follow mutual fund standards"
  },
  {
    'date': 'Dec-14-2018',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': 'Regulators issue window guidance to stop FMCs from promoting short-term performance of pension funds'
  },
  {
    'date': 'Dec-19-2018',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': 'CSRC issues information technology magement rules'
  },
  {
    'date': 'Dec-25-2018',
    'regulator': 'AMAC',
    'penalty': false,
    'summary': 'AMAC issues guidelines on bond-trading'
  },
  {
    'date': 'Jan-11-2019',
    'regulator': 'SZSE',
    'penalty': false,
    'summary': 'SZSE revises trading rules for certain ETFs'
  },
  {
    'date': 'Jan-18-2019',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': 'CSRC issues guidelines on mutual fund investment info credit derivatives, while AMAC issues affiliated valuation guidelines'
  },
  {
    'date': 'Jan-26-2019',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': 'Yi Huiman appointed as CSRC party secretary and chairman'
  },
  {
    'date': 'Jan-28-2019',
    'regulator': 'CSRC',
    'penalty': false,
    'summary': 'CSRC publishes draft rules for the new technology innovation board, which will be paired with a registration-based IPO system'
  },
  {
    'date': 'Jan-22-2019',
    'regulator': 'CSRC',
    'penalty': true,
    'summary': 'Several third-party fund distribution institutions punished by CSRC for incompliant distribution and reporting'
  },
  {
    'date': 'Jan-31-2019',
    'regulator': 'PBoC',
    'penalty': true,
    'summary': 'ICBC Credit Suisse punished by PBoC for mishandling customer information'
  }
];

var parseDate = d3.timeParse("%b-%d-%Y");

var formatTime = d3.timeFormat("%b %d, %Y");

var data = rawData.map(function(d) {
  return {
    date: parseDate(d.date),
    regulator: d.regulator,
    penalty: d.penalty,
    summary: d.summary
  }
});

data.sort(function(x, y) {
  return d3.ascending(x.date, y.date);
});

//var earliest = d3.min(data.map(d=>d.date));
//var latest = d3.max(data.map(d=>d.date));

var dateMin = d3.min(data, function(d) {
  return d3.timeDay.offset(d.date, -10);
});

var dateMax = d3.max(data, function(d) {
  return d3.timeDay.offset(d.date, +10);
});

var timeScale = d3.scaleTime()
  .domain([dateMin, dateMax])
  .range([0, width]);

var colorMap = {
  'CSRC': '#003366',
  'CBIRC': '#e4a733',
  'AMAC': '#95b3d7',
  'SZSE': '#b29866',
  'PBoC': '#366092'
};

var defs = svg.append('svg:defs');
var fillURL = "Fills/gray-1-crosshatch.svg";


defs.append("svg:pattern")
  .attr("id", "gray_hatch")
  .attr("width", 10)
  .attr("height", 10)
  .attr("patternUnits", "userSpaceOnUse")
  .append("svg:image")
  .attr("xlink:href", fillURL)
  .attr("width", 10)
  .attr("height", 10)
  .attr("x", 0)
  .attr("y", 0);

graphGroup.append('rect')
  .attr('width', width)
  .attr('height', 80)
  .attr('x', 0)
  .attr('y', height * .75)
  .style('fill', "url(#gray_hatch)");

graphGroup.append('rect')
  .attr('width', width)
  .attr('height', 20)
  .attr('x', 0)
  .attr('y', height * .75 + 30)
  .style('fill', "#a6a6a6");

graphGroup.append('rect')
  .attr('width', 8)
  .attr('height', 80)
  .attr('x', 0)
  .attr('y', height * .75)
  .style('fill', "#a6a6a6");

graphGroup.append('rect')
  .attr('width', 8)
  .attr('height', 80)
  .attr('x', width)
  .attr('y', height * .75)
  .style('fill', "#a6a6a6");

graphGroup.selectAll('circle')
  .data(data)
  .enter()
  .append('circle')
  .attr('cx', function(d) {
    return timeScale(d.date)
  })
  .attr('cy', height * .75 + 40)
  .attr('r', 10)
  .style('fill', function(d) {
    return colorMap[d.regulator]
  });

graphGroup.selectAll('line')
  .data(data.filter(function(d) {
    return d.penalty == false
  }))
  .enter()
  .append('line')
  .attr('x1', function(d) {
    return timeScale(d.date)
  })
  .attr('x2', function(d) {
    return timeScale(d.date)
  })
  .attr('y1', function(d) {
    return height * .75 + 40
  })
  .attr('y2', function(d, i) {
    if (i % 2) {
      return 50;
    } else {
      return height / 2;
    }
  })
  .style('stroke', function(d) {
    return colorMap[d.regulator]
  })
  .style('stroke-width', '2px');

graphGroup.selectAll('.labelRects')
  .data(data.filter(function(d) {
    return d.penalty == false
  }))
  .attr('class', 'labelRects')
  .enter()
  .append('rect')
  .attr('width', 125)
  .attr('height', 10)
  .attr('x', function(d) {
    return timeScale(d.date) - 125
  })
  .attr('y', function(d, i) {
    if (i % 2) {
      return 50;
    } else {
      return height / 2;
    }
  })
  .style('fill', function(d) {
    return colorMap[d.regulator]
  });

graphGroup.selectAll('text')
  .data(data.filter(function(d) {
    return d.penalty == false
  }))
  .enter()
  .append('text')
  .attr('x', function(d) {
    return timeScale(d.date) - 125
  })
  .attr('y', function(d, i) {
    if (i % 2) {
      return 50 - 5;
    } else {
      return height / 2 - 5;
    }
  })
  .text(function(d) {
    return formatTime(d.date)
  })
  //.attr('text-anchor','middle')
  .attr('class', 'date');


function wrap(text, width) {

  text.each(function() {
    var text = d3.select(this),
      words = text.text().split(/\s+/).reverse(),
      word,
      line = [],
      lineNumber = 0,
      lineHeight = 1.1, // ems
      y = text.attr("y"),
      x = text.attr("x"),
      dy = parseFloat(text.attr("dy")),
      tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("y", y).attr("x", x).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}

graphGroup.selectAll('.labelText')
  .data(data.filter(function(d) {
    return d.penalty == false
  }))
  .enter()
  .append('text')
  .attr('class', 'labelText')
  .attr('x', function(d) {
    return timeScale(d.date) - 4
  })
  .attr('y', function(d, i) {
    if (i % 2) {
      return 50 + 20;
    } else {
      return height / 2 + 20;
    }
  })
  .attr("dy", 0)
  .attr("text-anchor", "end")
  .text(function(d) {
    return d.summary
  })
  .style('font-size', '12px');

d3.selectAll('.labelText')
  .call(wrap, 120);
text {
  font-family: Tw Cen MT;

}

.date {
    font-size: 18px;
    paint-order: stroke;
    stroke: #fff;
    stroke-width: 3px;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    font-weight: 800;
}
<script src="https://d3js.org/d3.v5.min.js"></script>