Tuesday 30 April 2019

Applying Bostock's wrap function on text

I have a use case where I would like to append a small/medium blurb of text to different parts of my visual. As is the default behavior, this looks very unsightly as svg text is just appended in one fell swoop. So after a bit of research I found that Mike Bostock has created a clever way to handle longer strings in svg text, which can be seen here. I have tried to adapt this function to my particular visual but it didn't quite pan out. Here is the snippet:

  

 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>

It makes conceptual sense to me, I did everything that Bostock did in the example (I think), however the text doesn't appear to be going through the function correctly and no wrapping takes place -- no errors either.

Question

Is it possible to adapt Bostock's tspan wrap function for the general case? If so, how do we do it if it's not selecting the text and calling the function and setting the desired width?

Further Clarifications:

  • My font size: 12px
  • My desired width: 120

Bonus Points:

  • Desired text align: right (Bostock's is centered text, it seems)


from Applying Bostock's wrap function on text

No comments:

Post a Comment