You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Chart.js实现分组饼图的技术求助

解决方案:用Chart.js实现分组嵌套饼图

Chart.js原生支持多数据集的环形图,通过调整cutout参数可实现外层大类、内层小类的分组效果,核心实现要点如下:

  1. 数据结构梳理:外层数据集对应大类的汇总值,内层数据集对应每个大类下的小类细分值,确保数据维度匹配
  2. 环形层级配置:外层设置较小的cutout(如'30%'),内层设置较大的cutout(如'70%'),形成嵌套视觉效果
  3. 颜色关联设计:同一大类的外层和内层细分项使用同色系,外层用主色、内层用浅色调,方便用户识别关联关系
  4. 数据处理优化:读取CSV时按大类分组,避免零散数据存储导致的逻辑混乱

以下是修改后的完整实现代码:

修改后的HTML

<div class="chartMenu">
  <p>分组饼图演示</p>
</div>
<div class="chartCard">
  <div class="chartBox">
    <canvas id="myChart"></canvas>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

修改后的CSS(保持原样式不变)

* {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
}
.chartMenu {
  width: 100vw;
  height: 40px;
  background: #1a1a1a;
  color: rgba(255, 26, 104, 1);
}
.chartMenu p {
  padding: 10px;
  font-size: 20px;
}
.chartCard {
  width: 100vw;
  height: calc(100vh - 40px);
  background: rgba(255, 26, 104, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.chartBox {
  width: 700px;
  padding: 20px;
  border-radius: 20px;
  border: solid 3px rgba(255, 26, 104, 1);
  background: white;
}

修改后的JavaScript

// 按大类分组存储数据
const groupedData = {};
// 外层大类标签和数据
const outerLabels = [];
const outerData = [];
// 内层小类标签和数据
const innerLabels = [];
const innerData = [];
// 颜色映射:大类对应主色,小类对应同色系浅色调
const colorMap = {
  '大类1': '#619E98',
  '大类2': '#4164BE',
  '大类3': '#7D35CA',
  // 可根据实际CSV数据扩展更多颜色
};

showGroupedDoughnutChart();

async function showGroupedDoughnutChart() {
  await processCSVData();
  const ctx = document.getElementById("myChart").getContext("2d");

  new Chart(ctx, {
    type: "doughnut",
    data: {
      labels: [...outerLabels, ...innerLabels],
      datasets: [
        // 外层:大类汇总数据
        {
          label: "文档类型汇总",
          backgroundColor: outerLabels.map(label => colorMap[label]),
          borderWidth: 1,
          borderRadius: 5,
          cutout: '30%', // 外层环形,预留中间空间给内层
          data: outerData,
          order: 1 // 控制绘制顺序,让外层显示在内层上方
        },
        // 内层:小类细分数据
        {
          label: "文档版本细分",
          backgroundColor: innerLabels.map(label => {
            const parentType = label.split('-')[0]; // 假设小类标签格式为"大类-小类"
            const baseColor = colorMap[parentType];
            // 将主色调浅,区分外层与内层
            return lightenColor(baseColor, 20);
          }),
          borderWidth: 1,
          borderRadius: 5,
          cutout: '70%', // 内层环形,占用中间区域
          data: innerData,
          order: 0
        }
      ]
    },
    options: {
      plugins: {
        legend: {
          display: true,
          position: "right",
        },
        title: {
          display: true,
          text: "按类别划分的标签分布",
          color: "black",
        },
        tooltip: {
          callbacks: {
            label: function(context) {
              const label = context.label || '';
              const value = context.raw || 0;
              return `${label}: ${value}`;
            }
          }
        }
      },
      layout: {
        padding: 10,
      },
      maintainAspectRatio: true,
      responsive: true,
    },
  });
}

async function processCSVData() {
  const response = await fetch("stats_doc_type_Rev.csv");
  const data = await response.text();
  // 过滤空行,避免解析错误
  const rows = data.split("\n").slice(1).filter(row => row.trim() !== '');

  rows.forEach(row => {
    const columns = row.split(",").map(col => col.trim());
    const docType = columns[0];
    const docRevision = columns[1];
    const revCount = parseInt(columns[2]);
    const totalCount = parseInt(columns[3]);

    // 初始化大类分组
    if (!groupedData[docType]) {
      groupedData[docType] = {
        total: totalCount,
        revisions: []
      };
      outerLabels.push(docType);
      outerData.push(totalCount);
    }

    // 添加小类数据
    const innerLabel = `${docType}-版本${docRevision}`;
    innerLabels.push(innerLabel);
    innerData.push(revCount);
    groupedData[docType].revisions.push({
      label: innerLabel,
      count: revCount
    });
  });
}

// 辅助函数:将十六进制颜色调浅
function lightenColor(color, percent) {
  color = color.replace(/^#/, '');
  const num = parseInt(color, 16);
  const amt = Math.round(2.55 * percent);
  const R = Math.min(255, Math.max(0, (num >> 16) + amt));
  const G = Math.min(255, Math.max(0, (num >> 8 & 0x00FF) + amt));
  const B = Math.min(255, Math.max(0, (num & 0x0000FF) + amt));
  return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}

关键修改说明

  • 数据分组管理:不再使用零散数组存储数据,而是按文档类型分组,清晰区分大类汇总与小类细分数据
  • 环形层级控制:通过cutout参数定义内外层环形的大小,order参数确保外层在视觉上层显示
  • 颜色体系优化:小类使用对应大类的浅色调,保证视觉上的关联识别性
  • 数据健壮性处理:读取CSV时过滤空行,避免无效数据导致的图表渲染错误
  • 交互体验提升:优化tooltip显示逻辑,确保标签与数值对应准确

原提问及代码

我需要用Chart.js实现分组饼图(效果为外层大类、内层对应小类的嵌套结构),虽然知道D3.js可以实现,但没时间学习,只能用Chart.js。我自己尝试写了代码但没成功,不确定是问题本身复杂还是思路有误,希望得到技术帮助。

原HTML代码

<div class="chartMenu">
  <p></p>
</div>
<div class="chartCard">
  <div class="chartBox">
    <canvas id="myChart"></canvas>
  </div>
</div>

<script
  type="text/javascript"
  src="https://cdn.jsdelivr.net/npm/chart.js"
></script>

原CSS代码

* {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
}
.chartMenu {
  width: 100vw;
  height: 40px;
  background: #1a1a1a;
  color: rgba(255, 26, 104, 1);
}
.chartMenu p {
  padding: 10px;
  font-size: 20px;
}
.chartCard {
  width: 100vw;
  height: calc(100vh - 40px);
  background: rgba(255, 26, 104, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.chartBox {
  width: 700px;
  padding: 20px;
  border-radius: 20px;
  border: solid 3px rgba(255, 26, 104, 1);
  background: white;
}

原JavaScript代码

<script>
  const documentType = [];
  const documentRevision = [];
  const docTypeRev = [];
  const docTypeTotal = [];
  const percentage = [];

  showDoughnutChart();
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // START OF DOUGHNUT CHART
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  async function showDoughnutChart() {
    await getDataDoughnutGraph();
    const doughnutChart = document
      .getElementById("myChart")
      .getContext("2d");

    let chart3a = new Chart(doughnutChart, {
      type: "doughnut",
      data: {
        labels: documentType,
        datasets: [
          {
            label: "Some metric",
            backgroundColor: [
              "#619E98",
              "#4164BE",
              "#7D35CA",
              "#B2A44D",
              "#946B76",
              "#6C9D62",
              "#C6B139",
              "#2688D9",
              "#D62963",
              "#CCBF33",
              "#adff2f",
              "#B6495D",
            ],
            borderWidth: 1,
            borderRadius: 5,
            offset: [0, 0, 0, 0, 0, 0, 0],
            cutout: "50%",
            data: docTypeRev,
          },

          {
            label: "Some metric",
            backgroundColor: [
              "#619E98",
              "#4164BE",
              "#7D35CA",
              "#B2A44D",
              "#946B76",
              "#6C9D62",
              "#C6B139",
              "#2688D9",
              "#D62963",
              "#CCBF33",
              "#adff2f",
              "#B6495D",
            ],
            borderWidth: 1,
            borderRadius: 5,
            offset: [0, 0, 0, 0, 0, 0, 0],
            cutout: "50%",
            data: [1, 1, 1],
          },
        ],
      },
      options: {
        plugins: {
          legend: {
            display: true,
            position: "right",
          },
          title: {
            display: true,
            text: "Tag Distrubution by Class",
            color: "black",
          },
        },
        layout: {
          padding: 10,
        },
        maintainAspectRatio: true,
        responsive: true,
      },
    });
  }

  async function getDataDoughnutGraph() {
    const responseLine = await fetch("stats_doc_type_Rev.csv");

    const chart3el = document.getElementById("myChart").getContext("2d");
    const data = await responseLine.text();

    const table = data.split("\n").slice(1);
    table.forEach((row) => {
      //split by line
      const columns = row.split(",");
      // split by column
      const documentTypeEX = columns[0];
      const documentRevisionEX = columns[1];
      const docTypeRevEX = columns[2];
      const docTypeTotalEX = columns[3];
      const percentage_EX = columns[4];

      //pushing into global variables
      documentType.push(documentTypeEX);
      documentRevision.push(documentRevisionEX);
      docTypeRev.push(docTypeRevEX);
      docTypeTotal.push(docTypeTotalEX);
      percentage.push(percentage_EX);

      // console.log(percentage_EX);
    });
  }
</script>

内容的提问来源于stack exchange,提问作者safalstha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 00:57:22