Pivot 透视表
行 × 列 二维聚合表格 —— sum / avg / count / min / max 五种聚合、自带行列合计、可选热力着色、单元格 drill-down。
基础用法
把扁平的对象数组按 rowField(行)/ colField(列)分组,对 valueField 字段做聚合。组件会自动计算行合计、列合计、总计。
背景
| region/channel | 官网 | 门店 | App | 合计 |
|---|---|---|---|---|
| 华北 | ¥12,400 | ¥8,200 | ¥4,900 | ¥25,500 |
| 华东 | ¥22,800 | ¥14,200 | ¥9,800 | ¥46,800 |
| 华南 | ¥18,200 | ¥6,800 | ¥5,400 | ¥30,400 |
| 西部 | ¥7,400 | ¥3,200 | ¥2,100 | ¥12,700 |
| 合计 | ¥60,800 | ¥32,400 | ¥22,200 | ¥115,400 |
<script setup lang="ts">
import { CfPivot } from '@chufix-design/vue';
interface Order {
region: string;
channel: string;
amount: number;
}
const data: Order[] = [
{ region: '华北', channel: '官网', amount: 12400 },
{ region: '华北', channel: '门店', amount: 8200 },
{ region: '华北', channel: 'App', amount: 4900 },
{ region: '华东', channel: '官网', amount: 22800 },
{ region: '华东', channel: '门店', amount: 14200 },
{ region: '华东', channel: 'App', amount: 9800 },
{ region: '华南', channel: '官网', amount: 18200 },
{ region: '华南', channel: '门店', amount: 6800 },
{ region: '华南', channel: 'App', amount: 5400 },
{ region: '西部', channel: '官网', amount: 7400 },
{ region: '西部', channel: '门店', amount: 3200 },
{ region: '西部', channel: 'App', amount: 2100 },
];
</script>
<template>
<CfPivot
:data="data"
row-field="region"
col-field="channel"
value-field="amount"
aggregator="sum"
:format="(v: number) => '¥' + v.toLocaleString()"
/>
</template> <script setup>
import { CfPivot } from '@chufix-design/vue';
const data= [
{ region: '华北', channel: '官网', amount: 12400 },
{ region: '华北', channel: '门店', amount: 8200 },
{ region: '华北', channel: 'App', amount: 4900 },
{ region: '华东', channel: '官网', amount: 22800 },
{ region: '华东', channel: '门店', amount: 14200 },
{ region: '华东', channel: 'App', amount: 9800 },
{ region: '华南', channel: '官网', amount: 18200 },
{ region: '华南', channel: '门店', amount: 6800 },
{ region: '华南', channel: 'App', amount: 5400 },
{ region: '西部', channel: '官网', amount: 7400 },
{ region: '西部', channel: '门店', amount: 3200 },
{ region: '西部', channel: 'App', amount: 2100 },
];
</script>
<template>
<CfPivot
:data="data"
row-field="region"
col-field="channel"
value-field="amount"
aggregator="sum"
:format="(v) => '¥' + v.toLocaleString()"
/>
</template> 热力图模式
heatmap 打开后,每个单元格按数值大小渲染为一层透明 accent 颜色(数值越大颜色越深)。配合不同 aggregator 切换可以快速观察分布形态。
背景
aggregator
周 × 小时 出行热力(模拟数据)
| weekday/hour | 00 | 04 | 08 | 12 | 16 | 20 |
|---|---|---|---|---|---|---|
| 周一 | 70 | 107 | 296 | 319 | 278 | 62 |
| 周二 | 89 | 67 | 277 | 295 | 308 | 65 |
| 周三 | 93 | 93 | 275 | 324 | 324 | 98 |
| 周四 | 60 | 75 | 323 | 275 | 280 | 50 |
| 周五 | 82 | 53 | 288 | 318 | 286 | 57 |
| 周六 | 129 | 164 | 144 | 332 | 154 | 320 |
| 周日 | 139 | 152 | 161 | 328 | 160 | 292 |
<script setup lang="ts">
import { ref } from 'vue';
import { CfPivot, CfSelect } from '@chufix-design/vue';
import type { PivotAggregator } from '@chufix-design/vue';
interface Trip {
weekday: string;
hour: string;
count: number;
}
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const HOURS = ['00', '04', '08', '12', '16', '20'];
const data: Trip[] = [];
for (const w of WEEKDAYS) {
for (const h of HOURS) {
let base = 50;
if (h === '08' || h === '12' || h === '16') base += 220;
if (w === '周六' || w === '周日') base = h === '12' || h === '20' ? 280 : 120;
data.push({ weekday: w, hour: h, count: Math.round(base + Math.random() * 60) });
}
}
const agg = ref<PivotAggregator>('sum');
const aggOptions = [
{ value: 'sum', label: 'sum' },
{ value: 'avg', label: 'avg' },
{ value: 'count', label: 'count' },
{ value: 'max', label: 'max' },
];
</script>
<template>
<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: var(--fg-3);">aggregator</span>
<CfSelect v-model="agg" :options="aggOptions" size="sm" style="max-width: 120px;" />
</div>
<CfPivot
:data="data"
row-field="weekday"
col-field="hour"
value-field="count"
:aggregator="agg"
heatmap
:show-totals="false"
caption="周 × 小时 出行热力(模拟数据)"
/>
</template> <script setup>
import { ref } from 'vue';
import { CfPivot, CfSelect } from '@chufix-design/vue';
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const HOURS = ['00', '04', '08', '12', '16', '20'];
const data= [];
for (const w of WEEKDAYS) {
for (const h of HOURS) {
let base = 50;
if (h === '08' || h === '12' || h === '16') base += 220;
if (w === '周六' || w === '周日') base = h === '12' || h === '20' ? 280 : 120;
data.push({ weekday: w, hour, count: Math.round(base + Math.random() * 60) });
}
}
const agg = ref<PivotAggregator>('sum');
const aggOptions = [
{ value: 'sum', label: 'sum' },
{ value: 'avg', label: 'avg' },
{ value: 'count', label: 'count' },
{ value: 'max', label: 'max' },
];
</script>
<template>
<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: var(--fg-3);">aggregator</span>
<CfSelect v-model="agg" :options="aggOptions" size="sm" style="max-width: 120px;" />
</div>
<CfPivot
:data="data"
row-field="weekday"
col-field="hour"
value-field="count"
:aggregator="agg"
heatmap
:show-totals="false"
caption="周 × 小时 出行热力(模拟数据)"
/>
</template> Drill-down
onCellClick 回调会拿到 { row, col, value, rows } —— rows 是聚合时归并到这个单元格的所有原始数据,可用于触发抽屉、弹窗等下钻交互。
背景
| product/store | 上海 | 北京 | 深圳 | 合计 |
|---|---|---|---|---|
| A | 6,000 | 1,900 | 7,900 | |
| B | 5,400 | 6,100 | 4,200 | 15,700 |
| C | 1,200 | 2,400 | 3,300 | 6,900 |
| 合计 | 12,600 | 10,400 | 7,500 | 30,500 |
<script setup lang="ts">
import { ref } from 'vue';
import { CfPivot } from '@chufix-design/vue';
interface Sale {
product: string;
store: string;
amount: number;
qty: number;
}
const data: Sale[] = [
{ product: 'A', store: '上海', amount: 3200, qty: 12 },
{ product: 'A', store: '上海', amount: 2800, qty: 9 },
{ product: 'A', store: '北京', amount: 1900, qty: 7 },
{ product: 'B', store: '上海', amount: 5400, qty: 22 },
{ product: 'B', store: '北京', amount: 6100, qty: 26 },
{ product: 'B', store: '深圳', amount: 4200, qty: 18 },
{ product: 'C', store: '上海', amount: 1200, qty: 4 },
{ product: 'C', store: '北京', amount: 2400, qty: 8 },
{ product: 'C', store: '深圳', amount: 3300, qty: 11 },
];
const drilled = ref<{ row: string; col: string; rows: Sale[] } | null>(null);
</script>
<template>
<CfPivot
:data="data"
row-field="product"
col-field="store"
value-field="amount"
aggregator="sum"
:on-cell-click="(p: { row: string; col: string; rows: unknown[] }) => drilled = { row: p.row, col: p.col, rows: p.rows as Sale[] }"
/>
<div v-if="drilled" style="margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line-1); border-radius: 6px; background: var(--bg-2); font-size: 12px;">
<div style="margin-bottom: 6px;">
<strong>{{ drilled.row }} × {{ drilled.col }}</strong> 命中 {{ drilled.rows.length }} 条原始记录
</div>
<ul style="margin: 0; padding-left: 18px; color: var(--fg-2);">
<li v-for="(r, i) in drilled.rows" :key="i">
¥{{ r.amount.toLocaleString() }} · {{ r.qty }} 件
</li>
</ul>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfPivot } from '@chufix-design/vue';
const data= [
{ product: 'A', store: '上海', amount: 3200, qty: 12 },
{ product: 'A', store: '上海', amount: 2800, qty: 9 },
{ product: 'A', store: '北京', amount: 1900, qty: 7 },
{ product: 'B', store: '上海', amount: 5400, qty: 22 },
{ product: 'B', store: '北京', amount: 6100, qty: 26 },
{ product: 'B', store: '深圳', amount: 4200, qty: 18 },
{ product: 'C', store: '上海', amount: 1200, qty: 4 },
{ product: 'C', store: '北京', amount: 2400, qty: 8 },
{ product: 'C', store: '深圳', amount: 3300, qty: 11 },
];
const drilled = ref<{ row: string; col: string; rows: Sale[] } | null>(null);
</script>
<template>
<CfPivot
:data="data"
row-field="product"
col-field="store"
value-field="amount"
aggregator="sum"
:on-cell-click="(p: { row: string; col: string; rows: unknown[] }) => drilled = { row: p.row, col: p.col, rows: p.rows}"
/>
<div v-if="drilled" style="margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line-1); border-radius: 6px; background: var(--bg-2); font-size: 12px;">
<div style="margin-bottom: 6px;">
<strong>{{ drilled.row }} × {{ drilled.col }}</strong> 命中 {{ drilled.rows.length }} 条原始记录
</div>
<ul style="margin: 0; padding-left: 18px; color: var(--fg-2);">
<li v-for="(r, i) in drilled.rows" :key="i">
¥{{ r.amount.toLocaleString() }} · {{ r.qty }} 件
</li>
</ul>
</div>
</template> API
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
data | T[] | — | 扁平数据数组 |
rowField | keyof T | — | 用于分行的字段 |
colField | keyof T | — | 用于分列的字段 |
valueField | keyof T | — | 数值字段(aggregator='count' 时可省略) |
aggregator | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'sum' | 聚合方式 |
format | (value, { row, col }) => string | — | 单元格自定义格式化 |
showTotals | boolean | true | 是否显示行 / 列合计 |
heatmap | boolean | false | 按数值大小渲染热力背景 |
heatmapColor | string | var(--accent-1) | 热力主色 |
caption | string | — | 顶部说明文字 |
size | 'sm' | 'md' | 'lg' | 'md' | 字号档位 |
onCellClick | (cell) => void | — | 单元格点击回调,参数包含原始 rows |
工具函数
import { pivotCompute, pivotAggregate } from '@chufix-design/vue';
const result = pivotCompute(data, 'region', 'channel', 'amount', 'sum');
// → { rowKeys, colKeys, cells, raw, rowTotals, colTotals, grandTotal, min, max }
pivotCompute 是组件内部用的纯函数,外部也可以直接复用:导出 CSV、画图、做下钻等场景都用得上。
反馈与讨论
Pivot 透视表 的讨论