Изменение диаграммы пончиков на «диаграмму пончиков прогресса»

#javascript #d3.js

#javascript #d3.js

Вопрос:

Я пытаюсь разработать визуальную диаграмму пончиков d3, которая просто принимает определенный пользователем процент — где угодно от 0 до 100%. Исходя из этого значения, я хочу, чтобы у пончика были сегменты, равные пропорции. Например, если 50%, то будет добавлена ровно половина сегментов диаграммы пончика. Аналогично, если 100%, то будет нарисована вся диаграмма пончиков; все сегменты будут добавлены. Я не мог понять, как добиться этого элегантным способом, но я нашел грубый обходной путь, который вы можете увидеть ниже во фрагменте.

 var data =
[{'value':0,'interval':6.25},
{'value':6.25,'interval':6.25},
{'value':12.5,'interval':6.25},
{'value':18.75,'interval':6.25},
{'value':25,'interval':6.25},
{'value':31.25,'interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25},
{'value':'none','interval':6.25}];


var width = 960,
    height = 500,
    radius = Math.min(width, height) / 2;

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle   x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate("  xOff "," yOff  ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate("   width / 2   ","   height / 2   ")");

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {
        if (d.data.value=='none') {
          return 'none'
        }
        return color(d.data.value); });

  g.append("text")
      .attr("transform", function(d) { return "translate("   arc.centroid(d)   ")"; })
      .attr("dy", ".35em")
      //.text(function(d) { return d.data.age; });

svg.append('text')
    .text('37.5%') // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);


function type(d) {
  d.interval =  d.interval;
  return d;
}  
 <!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>  

По сути, происходит то, что доля записей со значениями в записях с надписью «нет» составляет 37,5% — это мое магическое число (6 значений и 10 нулей, таким образом, 6/16 = 37,5%). Излишне говорить, что это вообще не масштабируется.

Вопрос

Есть ли какие-либо встроенные средства или другие менее трудоемкие решения на моем конкретном этапе? Я просто хочу иметь возможность передавать число от 0 до 100 в функцию, а затем извлекать этот процент сегментов пончика. В моем конкретном случае я выбрал 6.25, потому что он казался наиболее эстетичным.

Возможно, пользовательская заливка прозрачностью для имитации эффекта разнесенных сегментов? Кажется слишком хакерским…

Примечание: Версии являются необязательными. То есть я не против решений d3.v5, я просто использовал d3.v3, поскольку я еще не использовал donuts в версии 5.

Комментарии:

1. Вам нужны только целые сегменты? Или вам нужно показывать фрагменты сегментов в случае, если процент не равен круглому числу (сегментов)?

2. @AndrewReid В любом случае, я думаю, для меня было бы нормально. Фрагменты сегментов были бы идеальными, но я согласен с небольшой ошибкой размера, если это слишком хлопотно. Возможно, установив относительно небольшую длину сегмента, можно убедиться, что расхождение не будет слишком большим.

Ответ №1:

Самый простой способ сделать это — добавить еще одну дугу поверх остальных сегментов. Эта дуга может быть произвольной длины, поэтому она покрывает все сегменты, которые не нужно показывать. Это может быть сделано с:

   var percentage = .35;
  g.append("path")
    .attr("d", d3.svg.arc()
      .endAngle(Math.PI*2)
      .startAngle(percentage * Math.PI*2)
      .outerRadius(radius - 10)
      .innerRadius(radius - 70)
     )
     .attr("fill","white")
  

Мы начинаем с конечного угла, Math.PI * 2, который равен одному полному повороту, что соответствует 100% завершению. Затем мы движемся назад с меньшим начальным углом, покрывая все между 100% и тем процентом, который у нас есть.

В зависимости от того, как вы хотите ее оформить, вы могли бы даже сделать ее слегка прозрачной, чтобы показать, какая часть является неполной.

Вот пример:

 var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

var width = 400,
    height = 400,
    radius = Math.min(width, height) / 2;

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle   x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate("  xOff "," yOff  ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate("   width / 2   ","   height / 2   ")");

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate("   arc.centroid(d)   ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;
  g.append("path")
    .attr("d", d3.svg.arc()
      .endAngle(Math.PI*2)
      .startAngle(percentage * Math.PI*2)
      .outerRadius(radius - 10)
      .innerRadius(radius - 70)
     )
     .attr("fill","white")


svg.append('text')
    .text(percentage * 100   "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);


function type(d) {
  d.interval =  d.interval;
  return d;
}  
 <!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>  

Этот подход также позволяет легко анимировать:

 var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

var width = 400,
    height = 400,
    radius = Math.min(width, height) / 2;

var color = d3.scale.linear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle   x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate("  xOff "," yOff  ")";
}

var arc = d3.svg.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.layout.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate("   width / 2   ","   height / 2   ")");

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate("   arc.centroid(d)   ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;

  var coverArc = g.append("path")
     .attr("fill","white")


var label = svg.append('text')
    .text(percentage * 100   "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);
    
function transition() {

  coverArc
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(0, percentage);
       return function(t) { 
         that.attr("d", d3.svg.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100)   "%");
        };
     })
     .duration(1000)
     // other way:
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(percentage, 0);
       return function(t) { 
         that.attr("d", d3.svg.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100)   "%");
        };
     })
     .duration(1000)
     .each("end",transition);

}

transition();




function type(d) {
  d.interval =  d.interval;
  return d;
}  
 <!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>  

В ответе используется d3v3, для d3v4 есть некоторые незначительные изменения, касающиеся, в частности, arc, d3.svg.arc теперь является d3.arc, d3.layout.круг теперь d3.pie и d3.scale.линейный теперь d3.scaleLinear, а для анимации, используемой во 2-м фрагменте, переход.теперь каждый является transition.on:

 var data = d3.range(16).map(function(d) {
  return { value: d*6.25, interval: 6.25 };
})

var width = 400,
    height = 400,
    radius = Math.min(width, height) / 2;

var color = d3.scaleLinear()
        .range(["#0005fd","#00fe80"]).domain([0,35]);

var explode = function(x,index) {
  var offset = (index==5) ? 80:0;
  var angle = (x.startAngle   x.endAngle)/2;
  var xOff = Math.sin(angle)*offset;
  var yOff = -Math.cos(angle)*offset;
  return "translate("  xOff "," yOff  ")";
}

var arc = d3.arc()
    .outerRadius(radius - 10)
    .innerRadius(radius - 70);

var pie = d3.pie()
    .padAngle(.05)
    .sort(null)
    .value(function(d) { return d.interval; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate("   width / 2   ","   height / 2   ")");

  var g = svg.selectAll(".arc")
      .data(pie(data))
    .enter().append("g")
      .attr("class", "arc");

  g.append("path")
      .attr("d", arc)
      //.attr('transform', explode)
      .style('stroke','none')
      .style("fill", function(d) {return color(d.data.value); });
      
      
  g.append("text")
      .attr("transform", function(d) { return "translate("   arc.centroid(d)   ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.data.age; });
      
  // Extra arc:
  var percentage = .35;

  var coverArc = g.append("path")
     .attr("fill","white")


var label = svg.append('text')
    .text(percentage * 100   "%") // magic number
    .attr('font-family','Play')
    .attr('font-size','140px')
    .attr('text-anchor','middle')
    .attr('x',0)
    .attr('y',40);
    
function transition() {

  coverArc
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(0, percentage);
       return function(t) { 
         that.attr("d", d3.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100)   "%");
        };
     })
     .duration(1000)
     // other way:
    .transition()
    .tween("d", function(d) {
       var that = d3.select(this),
       i = d3.interpolateNumber(percentage, 0);
       return function(t) { 
         that.attr("d", d3.arc()
           .endAngle(Math.PI*2)
           .startAngle(i(t) * Math.PI*2)
           .outerRadius(radius - 10)
           .innerRadius(radius - 70)
           )
          label.text(Math.round(i(t) * 100)   "%");
        };
     })
     .duration(1000)
     .on("end",transition);

}

transition();




function type(d) {
  d.interval =  d.interval;
  return d;
}  
 <!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>
<script src="http://d3js.org/d3.v5.min.js"></script>