繁体   English   中英

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

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

我有一个项目,我使用饼图/甜甜圈图来可视化我的数据。 我添加了一般更新模式,以便在我的数据更改/更新时创建平滑过渡。

为了做到这一点,我遵循了一个在饼图上使用一般更新模式的示例: Bl.ocks example

我面临的问题是更新数据时图表更新不顺畅。 图表立即从一个 state 交换到下一个。

在这个示例和其他示例中,他们定义了一个arcTween方法,其中 d3 在先前的角度和新更新数据的角度之间进行插值:

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

我还添加了加入、输入和更新我的数据到饼图的代码。 在这里,我首先创建了绘制饼图的组元素。我还使用“arcTween”方法定义了一个转换,以在状态之间转换。 最后,我还在创建饼图时定义了“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;
        });

这是我的完整代码。 这是用 Vue.js 编写的。 我试图让它在一个片段中工作。 但我无法让它工作。

显示在甜甜圈切片顶部的图像在本地存储:

<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>

我已经缩短了代码并包含在代码片段中:

 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>

该片段有效,但切片更改 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>

两项更改使您的代码段起作用。

第一个是在methods object 中向上移动arcTween方法。 由于我不是 Vue 用户,并且从 JS 的角度来看这是没有意义的(对象的属性顺序无关紧要),我将把这部分留给你调查。

第二个变化只是与this的含义有关。 您将this混合为method object(如在this.arc中)和this作为 DOM 元素(如在this._current中)。 就像我在评论中所说的那样,只需使用第三个和第二个 arguments 组合来引用 DOM 元素(在绝大多数 D3 方法中)。

也就是说,这...

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

...应该:

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

这是您进行这两项更改的代码段:

 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>

该代码确实有效,您可以看到小提琴https://jsfiddle.net/1Lk8zqud/1/

如果你真的需要容器,那么你需要有一些考虑:

  1. 您需要考虑具有完整 d3 更新模式的容器:
  2. 新容器 -> 设置属性,append 路径,append 图像 3 - 现有容器 -> 没有附加,只需更新路径和图像 4 - 删除不需要的容器

如果是新容器,请记住它们与最终路径一起添加(无过渡),因此最好降低旧容器以免弄乱过渡。 我没有尝试使用图像。

所以这是新版本

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