D3 学习:城市饼图

169 阅读2分钟

1631084600(1).jpg 上表我们可以分三步来实现。

  1. 实现主体饼图
  2. 添加城市 legend
  3. 添加环中心的文字说明和动画特效

第一步 实现饼图

先定义画布参数和数据

const svgWidth = 960,
  svgHeight = 500,
  radius = 220
const list = [
  {
    cityName: "北京",
    num: 1179,
  },
  {
    cityName: "上海",
    num: 1162,
  },
  {
    cityName: "成都",
    num: 1087,
  },
  {
    cityName: "武汉",
    num: 1067,
  },
  {
    cityName: "杭州",
    num: 874,
  },
  {
    cityName: "天津",
    num: 768,
  },
  {
    cityName: "深圳",
    num: 756,
  },
  {
    cityName: "长沙",
    num: 725,
  },
  {
    cityName: "西安",
    num: 716,
  },
  {
    cityName: "南京",
    num: 662,
  },
  {
    cityName: "重庆",
    num: 632,
  },
  {
    cityName: "沈阳",
    num: 626,
  },
  {
    cityName: "广州",
    num: 588,
  },
  {
    cityName: "郑州",
    num: 575,
  },
  {
    cityName: "青岛",
    num: 511,
  },
  {
    cityName: "苏州",
    num: 500,
  },
  {
    cityName: "合肥",
    num: 489,
  },
  {
    cityName: "哈尔滨",
    num: 472,
  },
  {
    cityName: "济南",
    num: 357,
  },
  {
    cityName: "贵阳",
    num: 353,
  },
  {
    cityName: "长春",
    num: 344,
  },
  {
    cityName: "无锡",
    num: 343,
  },
  {
    cityName: "大连",
    num: 342,
  },
  {
    cityName: "南昌",
    num: 337,
  },
  {
    cityName: "宁波",
    num: 311,
  },
  {
    cityName: "太原",
    num: 300,
  },
  {
    cityName: "昆明",
    num: 290,
  },
  {
    cityName: "常州",
    num: 254,
  },
  {
    cityName: "厦门",
    num: 229,
  },
  {
    cityName: "石家庄",
    num: 221,
  },
  {
    cityName: "其他",
    num: 14358,
  },
];

然后定义画布

// 设置画布大小
  let svg = d3
    .select("#pie")
    .append("svg")
    .attr("width", svgWidth)
    .attr("height", svgHeight)
    .append("g")
    .attr("transform", `translate(${svgWidth / 2}, ${svgHeight / 2})`);
    
  // 定义一个 pie
  let pie = d3
    .pie()
    .value((d) => d.num)
    .padAngle(0.02); // 设置间隔

  // 设置颜色
  let color = d3
    .scaleOrdinal()
    .domain(list.map((d) => d.num))
    .range(
      d3
        .quantize((t) => d3.interpolateSpectral(t * 0.8 + 0.1), list.length)
        .reverse()
    );
    
  svg
    .append("g")
    .selectAll("path")
    .data(pie(list))
    .enter()
    .append("path")
    .style("fill", (d, i) => {
      return color(i);
    })
    .attr("d", arc);

可以看到一个圆环就做好了。

1631955130(1).png

第二步 添加城市 legend

定义 label 的高度和一个 legend 插入 svg 里

const labelHeight = 18;
const legend = svg
    .append("g")
    .attr(
      "transform",
      `translate(-${svgWidth / 2 - 30},-${svgHeight / 2 - 30})`
    );

使用方形来做色块标识城市

legend
    .selectAll(null)
    .data(pie(list))
    .enter()
    .append("rect")
    .attr("x", (d) => labelHeight * parseInt(d.index / 14) * 4.8)
    .attr(
      "y",
      (d) => labelHeight * (d.index - parseInt(d.index / 14) * 14) * 1.8
    )
    .attr("width", labelHeight)
    .attr("height", labelHeight)
    .attr("fill", (d, i) => {
      return color(i);
    })
    .attr("stroke", "grey")
    .style("stroke-width", "1px");

添加城市名称

legend
    .selectAll(null)
    .data(pie(list))
    .enter()
    .append("text")
    .text((d) => d.data.cityName)
    .attr("x", (d) => labelHeight * parseInt(d.index / 14) * 4.8 + 25)
    .attr(
      "y",
      (d) =>
        labelHeight * (d.index - parseInt(d.index / 14) * 14) * 1.8 +
        labelHeight
    )
    .style("font-family", "sans-serif")
    .style("font-size", `${labelHeight}px`);

得到以下结果

1631957068(1).png

第三步 添加环中心的文字说明和动画特效

先获取总的门店数

let total = 0;
list.map((item) => (total += item.num));

然后定义放大的环

let arcHover = d3
        .arc()
        .innerRadius(this.radius - 60)
        .outerRadius(this.radius + 10);

添加监听函数

svg
    .append("g")
    .selectAll("path")
    .data(pie(list))
    .enter()
    .append("path")
    .style("fill", (d, i) => {
      return color(i);
    })
    .attr("d", arc)
    .on("mouseenter", function (e, d) {
      svg
        .data([d])
        .append("text")
        .attr("class", "text")
        .text((d) => {
          return d.data.cityName;
        });
      svg
        .data([d])
        .append("text")
        .attr("class", "info")
        .attr("dy", "2.5em")
        .text((d) => {
          return `${d.data.num} 家`;
        })
        .attr("fill", "#909399");
      svg
        .data([d])
        .append("text")
        .attr("class", "info")
        .attr("dy", "4em")
        .text((d) => {
          return `${((d.data.num / total) * 100).toFixed(2)} %`;
        })
        .attr("fill", "#909399");

      d3.select(this)
        .transition()
        .duration(200)
        .ease(d3.easeBounceOut)
        .attr("d", arcHover);
    })
    .on("mouseleave", function (e, d) {
      svg.selectAll(".text").remove();
      svg.selectAll(".info").remove();
      d3.select(this)
        .transition()
        .duration(200)
        .ease(d3.easeBounceOut)
        .attr("d", arc);
    });

最后添加上样式,注意,如果 style 添加了 scoped,样式是不会生效。

.text {
  text-anchor: middle;
  font-size: 28px;
  font-weight: bold;
  letter-spacing: 3px;
}
.info {
  text-anchor: middle;
  font-size: 20px;
  font-weight: bold;
  letter-spacing: 3px;
  color: #909399;
}

最终效果

image.png

完整代码

<script setup>
import { onMounted } from "vue";
import * as d3 from "d3";
const list = [
  {
    cityName: "北京",
    num: 1179,
  },
  {
    cityName: "上海",
    num: 1162,
  },
  {
    cityName: "成都",
    num: 1087,
  },
  {
    cityName: "武汉",
    num: 1067,
  },
  {
    cityName: "杭州",
    num: 874,
  },
  {
    cityName: "天津",
    num: 768,
  },
  {
    cityName: "深圳",
    num: 756,
  },
  {
    cityName: "长沙",
    num: 725,
  },
  {
    cityName: "西安",
    num: 716,
  },
  {
    cityName: "南京",
    num: 662,
  },
  {
    cityName: "重庆",
    num: 632,
  },
  {
    cityName: "沈阳",
    num: 626,
  },
  {
    cityName: "广州",
    num: 588,
  },
  {
    cityName: "郑州",
    num: 575,
  },
  {
    cityName: "青岛",
    num: 511,
  },
  {
    cityName: "苏州",
    num: 500,
  },
  {
    cityName: "合肥",
    num: 489,
  },
  {
    cityName: "哈尔滨",
    num: 472,
  },
  {
    cityName: "济南",
    num: 357,
  },
  {
    cityName: "贵阳",
    num: 353,
  },
  {
    cityName: "长春",
    num: 344,
  },
  {
    cityName: "无锡",
    num: 343,
  },
  {
    cityName: "大连",
    num: 342,
  },
  {
    cityName: "南昌",
    num: 337,
  },
  {
    cityName: "宁波",
    num: 311,
  },
  {
    cityName: "太原",
    num: 300,
  },
  {
    cityName: "昆明",
    num: 290,
  },
  {
    cityName: "常州",
    num: 254,
  },
  {
    cityName: "厦门",
    num: 229,
  },
  {
    cityName: "石家庄",
    num: 221,
  },
  {
    cityName: "其他",
    num: 14358,
  },
];
const svgWidth = 960,
  svgHeight = 500,
  radius = 220,
  labelHeight = 18;

let total = 0;

onMounted(() => {
  init();
});

const init = () => {
  list.map((item) => (total += item.num));
  setSvg();
};

const setSvg = () => {
  // 设置画布大小
  let svg = d3
    .select("#pie")
    .append("svg")
    .attr("width", svgWidth)
    .attr("height", svgHeight)
    .append("g")
    .attr("transform", `translate(${svgWidth / 2}, ${svgHeight / 2})`);

  // 用一段弧形来定义一个圆
  let arc = d3
    .arc()
    .innerRadius(radius - 60)
    .outerRadius(radius);

  let arcHover = d3
    .arc()
    .innerRadius(radius - 60)
    .outerRadius(radius + 10);

  // 定义一个 pie
  let pie = d3
    .pie()
    .value((d) => d.num)
    .padAngle(0.02); // 设置间隔

  // 设置颜色
  let color = d3
    .scaleOrdinal()
    .domain(list.map((d) => d.num))
    .range(
      d3
        .quantize((t) => d3.interpolateSpectral(t * 0.8 + 0.1), list.length)
        .reverse()
    );

  // 定义一个legend
  const legend = svg
    .append("g")
    .attr(
      "transform",
      `translate(-${svgWidth / 2 - 30},-${svgHeight / 2 - 30})`
    );

  legend
    .selectAll(null)
    .data(pie(list))
    .enter()
    .append("rect")
    .attr("x", (d) => labelHeight * parseInt(d.index / 14) * 4.8)
    .attr(
      "y",
      (d) => labelHeight * (d.index - parseInt(d.index / 14) * 14) * 1.8
    )
    .attr("width", labelHeight)
    .attr("height", labelHeight)
    .attr("fill", (d, i) => {
      return color(i);
    })
    .attr("stroke", "grey")
    .style("stroke-width", "1px");

  legend
    .selectAll(null)
    .data(pie(list))
    .enter()
    .append("text")
    .text((d) => d.data.cityName)
    .attr("x", (d) => labelHeight * parseInt(d.index / 14) * 4.8 + 25)
    .attr(
      "y",
      (d) =>
        labelHeight * (d.index - parseInt(d.index / 14) * 14) * 1.8 +
        labelHeight
    )
    .style("font-family", "sans-serif")
    .style("font-size", `${labelHeight}px`);

  svg
    .append("g")
    .selectAll("path")
    .data(pie(list))
    .enter()
    .append("path")
    .style("fill", (d, i) => {
      return color(i);
    })
    .attr("d", arc)
    .on("mouseenter", function (e, d) {
      svg
        .data([d])
        .append("text")
        .attr("class", "text")
        .text((d) => {
          return d.data.cityName;
        });
      svg
        .data([d])
        .append("text")
        .attr("class", "info")
        .attr("dy", "2.5em")
        .text((d) => {
          return `${d.data.num} 家`;
        })
        .attr("fill", "#909399");
      svg
        .data([d])
        .append("text")
        .attr("class", "info")
        .attr("dy", "4em")
        .text((d) => {
          return `${((d.data.num / total) * 100).toFixed(2)} %`;
        })
        .attr("fill", "#909399");

      d3.select(this)
        .transition()
        .duration(200)
        .ease(d3.easeBounceOut)
        .attr("d", arcHover);
    })
    .on("mouseleave", function (e, d) {
      svg.selectAll(".text").remove();
      svg.selectAll(".info").remove();
      d3.select(this)
        .transition()
        .duration(200)
        .ease(d3.easeBounceOut)
        .attr("d", arc);
    });
};
</script>

<template>
  <div id="pie"></div>
</template>

<style>
.text {
  text-anchor: middle;
  font-size: 28px;
  font-weight: bold;
  letter-spacing: 3px;
}
.info {
  text-anchor: middle;
  font-size: 20px;
  font-weight: bold;
  letter-spacing: 3px;
  color: #909399;
}
</style>