简体   繁体   English

D3 一般更新模式转换不适用于饼图

[英]D3 General update pattern transition not working on pie chart

I have a project where I am using a Pie/Doughnut chart to visualize my data.我有一个项目,我使用饼图/甜甜圈图来可视化我的数据。 I have added the general update pattern in order to create a smooth transition when my data changes/updates.我添加了一般更新模式,以便在我的数据更改/更新时创建平滑过渡。

In order to accomplish this I have followed an example which uses the general update pattern on a pie chart: Bl.ocks example .为了做到这一点,我遵循了一个在饼图上使用一般更新模式的示例: Bl.ocks example

The problem I am facing is that the chart doesn't update smoothly when updating the data.我面临的问题是更新数据时图表更新不顺畅。 The chart instantly swaps from one state into the next.图表立即从一个 state 交换到下一个。

In this and other examples they define a arcTween method where d3 interpolates between the previous angles and the angles from the newly updated data:在这个示例和其他示例中,他们定义了一个arcTween方法,其中 d3 在先前的角度和新更新数据的角度之间进行插值:

    arcTween(a) {
      let i = d3.interpolate(this._current, a);
      this._current = i(0);
      return t => {
        return this.arc(i(t));
      };
    }

I also have added the code where I join, enter and update my data to the pie chart.我还添加了加入、输入和更新我的数据到饼图的代码。 Here I first create the group element where the pie is being drawn in. I also define a transition using the 'arcTween' method to transition between the states.在这里,我首先创建了绘制饼图的组元素。我还使用“arcTween”方法定义了一个转换,以在状态之间转换。 And lastly I also define the 'this._current' when the pie is created:最后,我还在创建饼图时定义了“this._current”:

      this.g = this.svg
        .selectAll("doughnut")
        .data(data_ready)
        .enter()
        .append("g");

      this.g
        .transition()
        .duration(1500)
        .attrTween("d", this.arcTween);

      this.g
        .append("path")
        .attr("d", d => {
          return this.arc(d);
        })
        .attr("fill", "#206BF3")
        .attr("class", "slice")
        .attr("stroke", "#2D3546")
        .style("stroke-width", "2px")
        .each(d => {
          this._current = d;
        });

This is my full code.这是我的完整代码。 This is being written in Vue.js.这是用 Vue.js 编写的。 I have tried to get this to work inside a snippet.我试图让它在一个片段中工作。 But I couldn't get it to work.但我无法让它工作。

The images that are shown on top of the doughnut slices are locally stored:显示在甜甜圈切片顶部的图像在本地存储:

<template>
  <div class="p-5 flex flex-col h-full">
    <h2 class="mb-3">{{ title }}</h2>
    <div ref="my_dataviz" class="flex justify-center"></div>
    <div class="grid grid-cols-2 gap-7 m-7">
      <div v-for="item in data" :key="item.key" class="flex">
        <img
          :src="require('@/assets/img/doughnut/' + item.icon)"
          alt=""
          class="doughnutIcon mr-4"
        />
        <div class="flex flex-col">
          <h3>{{ item.key }}</h3>
          <p class="opacity-50">
            {{ formatNumberValue(item.value) }} {{ unit }}
          </p>
          <p class="opacity-50">{{ percentageOfTotal(item.value) }} %</p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { converter } from "@/shared";
import * as d3 from "d3";

export default {
  name: "DoughnutChartItem",
  props: {
    title: {
      type: String,
      required: true
    },
    data: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      required: true
    },
    width: {
      type: Number,
      required: true
    },
    unit: {
      type: String
    }
  },
  data() {
    return {
      totalAmount: 0,
      svg: undefined,
      arc: undefined,
      radius: undefined,
      g: undefined
    };
  },
  created() {
    let total = 0;
    this.data.forEach(item => {
      total += item.value;
    });
    this.totalAmount = total;
  },
  mounted() {
    // set the dimensions and margins of the graph
    var margin = 1;

    // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
    this.radius = Math.min(this.width, this.height) / 2 - margin;

    // append the svg object to the div called 'my_dataviz'
    this.svg = d3
      .select(this.$refs.my_dataviz)
      .append("svg")
      .attr("width", this.width)
      .attr("height", this.height)
      .append("g")
      .attr(
        "transform",
        "translate(" + this.width / 2 + "," + this.height / 2 + ")"
      );

    // Compute the position of each group on the pie:
    this.pie = d3.pie().value(function(d) {
      return d[1];
    });

    // declare an arc generator function
    this.arc = d3
      .arc()
      .outerRadius(100)
      .innerRadius(50);

    this.setSlicesOnDoughnut(this.data);

    this.addImagesToSlices();
  },
  methods: {
    animateSliceOnHover(radius, path, dir) {
      switch (dir) {
        case 0:
          path
            .transition()
            .duration(500)
            .ease(d3.easeBounce)
            .attr(
              "d",
              d3
                .arc()
                .innerRadius(100)
                .outerRadius(50)
            );
          path.style("fill", "#206BF3");
          break;

        case 1:
          path.transition().attr(
            "d",
            d3
              .arc()
              .innerRadius(50)
              .outerRadius(110)
          );
          path.style("fill", "white");
          break;
      }
    },
    percentageOfTotal(amount) {
      return Math.round((amount / this.totalAmount) * 100);
    },
    formatNumberValue(amount) {
      return converter.formatNumberValue(amount);
    },
    setSlicesOnDoughnut(data) {
      // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
      var data_ready = this.pie(
        data.map(function(d) {
          return [d["key"], d["value"], d["icon"], d["hover"]];
        })
      );

      this.g = this.svg
        .selectAll("doughnut")
        .data(data_ready)
        .enter()
        .append("g");

      this.g
        .transition()
        .duration(1500)
        .attrTween("d", this.arcTween);

      this.g
        .append("path")
        .attr("d", d => {
          return this.arc(d);
        })
        .attr("fill", "#206BF3")
        .attr("class", "slice")
        .attr("stroke", "#2D3546")
        .style("stroke-width", "2px")
        .each(d => {
          this._current = d;
        });

      // Add tooltip
      d3.selectAll(".slice")
        .on("mouseover", this.mouseover)
        .on("mousemove", this.mousemove)
        .on("mouseout", this.mouseout);
    },
    addImagesToSlices() {
      var image_width = 20;
      var image_height = 20;

      this.g.selectAll(".logo").remove();

      this.g
        .append("svg:image")
        .attr("transform", d => {
          var x = this.arc.centroid(d)[0] - image_width / 2;
          var y = this.arc.centroid(d)[1] - image_height / 2;
          return "translate(" + x + "," + y + ")";
        })
        .attr("class", "logo")
        .attr("class", function(d) {
          return `${d.data[0]}-logo`;
        })
        .attr("href", function(d) {
          return require("@/assets/img/doughnut/" + d.data[2]);
        })
        .attr("width", image_width)
        .attr("height", image_height);
    },
    mouseover(event, data) {
      //Swap doughnut icon to blue icon
      d3.selectAll("." + data.data[0] + "-logo").attr("href", d => {
        return require("@/assets/img/doughnut/" + d.data[3]);
      });

      this.animateSliceOnHover(this.radius, d3.select(event.currentTarget), 1);

      const tip = d3.select(".tooltip");

      tip
        .style("left", `${event.clientX + 15}px`)
        .style("top", `${event.clientY}px`)
        .transition()
        .style("opacity", 0.98);

      tip.select("h3").html(`${data.data[0]}`);
      tip
        .select("h4")
        .html(`${this.formatNumberValue(data.data[1])} ${this.unit}`);
    },
    mousemove(event) {
      // Move tooltip
      d3.select(".tooltip")
        .style("left", `${event.clientX + 15}px`)
        .style("top", `${event.clientY}px`);
    },
    mouseout(event, data) {
      //Swap doughnut icon to white icon
      d3.selectAll("." + data.data[0] + "-logo").attr("href", function(d) {
        return require("@/assets/img/doughnut/" + d.data[2]);
      });

      // Animate slice
      var thisPath = d3.select(event.currentTarget);
      this.animateSliceOnHover(this.radius, thisPath, 0);

      // if (!thisPath.classed("clicked")) {
      //   this.animateSliceOnHover(this.radius, thisPath, 0);
      // }

      // Hide tooltip
      d3.select(".tooltip")
        .transition()
        .style("opacity", 0);
    },
    arcTween(a) {
      let i = d3.interpolate(this._current, a);
      this._current = i(0);
      return t => {
        return this.arc(i(t));
      };
    }
  },
  watch: {
    data() {
      this.setSlicesOnDoughnut(this.data);

      this.addImagesToSlices();
    }
  }
};
</script>

I have shortened the code and included into a code snippet:我已经缩短了代码并包含在代码片段中:

 new Vue({ el: "#app", data() { return { index: 0, data: [ [ { key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 } ], [ { key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); },methods: { swapData() { if(this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } }, arcTween(a) { var i = d3.interpolate(this._current, a); this._current = i(0); return t => { return this.arc(i(t)); }; } })
 <div id="app"> <button @click="swapData">Swap</button> <div id="my_dataviz" class="flex justify-center"></div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <script src="https://d3js.org/d3.v6.js"></script>

The snippet that works, but the slices change position:该片段有效,但切片更改 position:

 new Vue({ el: "#app", data() { return { index: 0, data: [ [{ key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 }, { key: "four", value: 238 } ], [{ key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 }, { key: "four", value: 168 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); }, methods: { swapData() { if (this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, arcTween(a, j, n) { var i = d3.interpolate(n[j]._current, a); n[j]._current = i(0); return t => { return this.arc(i(t)); }; }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); //console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } } })
 <div id="app"> <button @click="swapData">Swap</button> <div id="my_dataviz" class="flex justify-center"></div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <script src="https://d3js.org/d3.v6.js"></script>

Two changes made your snippet work.两项更改使您的代码段起作用。

The first one was moving the arcTween method up in the methods object.第一个是在methods object 中向上移动arcTween方法。 Since I'm not a Vue user and as this makes no sense from a JS perspective (the object's properties order shouldn't matter), I'll leave that part for you to investigate.由于我不是 Vue 用户,并且从 JS 的角度来看这是没有意义的(对象的属性顺序无关紧要),我将把这部分留给你调查。

The second change is just related to the meaning of this .第二个变化只是与this的含义有关。 You're mixing this as the method object (as in this.arc ) and this as the DOM element (as in this._current ).您将this混合为method object(如在this.arc中)和this作为 DOM 元素(如在this._current中)。 Like I said in my comment , just use the third and second arguments combined to refer to the DOM element (in the vast majority of D3 methods).就像我在评论中所说的那样,只需使用第三个和第二个 arguments 组合来引用 DOM 元素(在绝大多数 D3 方法中)。

That said, this...也就是说,这...

arcTween(a) {
      var i = d3.interpolate(this._current, a);
      this._current = i(0);
      return t => {
        return this.arc(i(t));
      };
    }

...should be: ...应该:

arcTween(a, j, n) {
  var i = d3.interpolate(n[j]._current, a);
  n[j]._current = i(0);
  return t => {
    return this.arc(i(t));
  };
}

Here is your snippet with those two changes:这是您进行这两项更改的代码段:

 new Vue({ el: "#app", data() { return { index: 0, data: [ [{ key: "one", value: 123 }, { key: "two", value: 232 }, { key: "three", value: 186 } ], [{ key: "one", value: 145 }, { key: "two", value: 270 }, { key: "three", value: 159 } ], ] } }, mounted() { // set the dimensions and margins of the graph var margin = 1; // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin. this.radius = Math.min(this.width, this.height) / 2 - margin; this.width = 250; this.height = 250; // append the svg object to the div called 'my_dataviz' this.svg = d3.select("#my_dataviz").append("svg").attr("width", this.width).attr("height", this.height).append("g").attr( "transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")" ); // Compute the position of each group on the pie: this.pie = d3.pie().value(function(d) { return d[1]; }); // declare an arc generator function this.arc = d3.arc().outerRadius(100).innerRadius(50); this.setSlicesOnDoughnut(); }, methods: { swapData() { if (this.index === 0) this.index = 1; else this.index = 0; this.setSlicesOnDoughnut(); }, arcTween(a, j, n) { var i = d3.interpolate(n[j]._current, a); n[j]._current = i(0); return t => { return this.arc(i(t)); }; }, setSlicesOnDoughnut() { // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function. var data_ready = this.pie( this.data[this.index].map(function(d) { return [d["key"], d["value"]]; }) ); //console.log(data_ready); // join var arcs = this.svg.selectAll(".arc").data(data_ready); // update arcs.transition().duration(1500).attrTween("d", this.arcTween); // enter arcs.enter().append("path").attr("class", "arc").attr("fill", "#206BF3").attr("stroke", "#2D3546").style("stroke-width", "2px").attr("d", this.arc).each((d, i, n) => { n[i]._current = d; }); } } })
 <div id="app"> <button @click="swapData">Swap</button> <div id="my_dataviz" class="flex justify-center"></div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <script src="https://d3js.org/d3.v6.js"></script>

the code really works, you can see the fiddle https://jsfiddle.net/1Lk8zqud/1/该代码确实有效,您可以看到小提琴https://jsfiddle.net/1Lk8zqud/1/

If you really need to have the containers, then you need to have some considerations:如果你真的需要容器,那么你需要有一些考虑:

  1. You need to consider the containers with the full d3 update pattern:您需要考虑具有完整 d3 更新模式的容器:
  2. New containers -> set the attr d, append a path, append an image 3 - Existing containers -> no appends, just update the path and image 4 - remove the containers not needed新容器 -> 设置属性,append 路径,append 图像 3 - 现有容器 -> 没有附加,只需更新路径和图像 4 - 删除不需要的容器

In case of new containers, remember that they are added with the final path (no transitions), so its better to lower the old containers in order to dont mess up with the transitions.如果是新容器,请记住它们与最终路径一起添加(无过渡),因此最好降低旧容器以免弄乱过渡。 I didn't try it with images.我没有尝试使用图像。

So here is the new version所以这是新版本

const g = svg.selectAll('g.arcs')
    .data(data_ready, function(d) { return d.index; });

//  new data -> add arcs
    g.enter().append('g')
      .attr('class', 'arcs')
      .lower()
      .append('path')
        .attr("fill", "#206BF3")
        .attr("class", "slice")
        .attr("stroke", "#2D3546")
        .style("stroke-width", "2px")
        .transition()
        .duration(1500)
        .attrTween("d", function(a) {
            let i = d3.interpolate(this._current, a);
            this._current = i(0);
            return function(t) {
                return this.arc(i(t));
            };
       });
       
// old data -> transition data on the container g (for using with the images). Don't add arcs
   g.raise()
       .transition()
       .duration(1500)
       .attrTween("d", function(a) {
            let i = d3.interpolate(this._current, a);
            this._current = i(0);
            return function(t) {
                return this.arc(i(t));
            };
       });
       
       // transition the path inside the old containers g
       g.select('path').transition()
       .duration(1500)
       .attrTween("d", function(a) {
            let i = d3.interpolate(this._current, a);
            this._current = i(0);
            return function(t) {
                return this.arc(i(t));
            };
       })

  g.exit().lower().transition().duration(1500).remove();```


A working version of this code:
https://jsfiddle.net/rtLbyn52/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM