Pivot
2D aggregation table — sum / avg / count / min / max aggregators, built-in row/col totals, optional heatmap shading, cell drill-down.
Basic usage
Group a flat array of objects by rowField (rows) and colField (columns), aggregating valueField. Row totals, column totals, and grand total are computed automatically.
| 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 mode
With heatmap, each cell is shaded with a translucent accent color proportional to its value (larger = deeper). Combined with different aggregator settings it gives an at-a-glance distribution view.
周 × 小时 出行热力(模拟数据)
| weekday/hour | 00 | 04 | 08 | 12 | 16 | 20 |
|---|---|---|---|---|---|---|
| 周一 | 104 | 88 | 327 | 285 | 287 | 93 |
| 周二 | 88 | 77 | 305 | 311 | 312 | 88 |
| 周三 | 68 | 91 | 319 | 318 | 319 | 63 |
| 周四 | 55 | 71 | 281 | 322 | 307 | 76 |
| 周五 | 95 | 74 | 315 | 330 | 284 | 104 |
| 周六 | 160 | 159 | 127 | 315 | 176 | 335 |
| 周日 | 177 | 158 | 152 | 328 | 121 | 331 |
<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 receives { row, col, value, rows } — rows is every source record that fell into this cell, ideal for triggering a drawer or modal with the underlying detail.
| 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 | Type | Default | Description |
|---|---|---|---|
data | T[] | — | Flat data array |
rowField | keyof T | — | Row grouping field |
colField | keyof T | — | Column grouping field |
valueField | keyof T | — | Numeric field (omittable with aggregator='count') |
aggregator | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'sum' | Aggregation method |
format | (value, { row, col }) => string | — | Custom cell formatter |
showTotals | boolean | true | Render row + column totals |
heatmap | boolean | false | Shade cells by value |
heatmapColor | string | var(--accent-1) | Heatmap base color |
caption | string | — | Caption above the table |
size | 'sm' | 'md' | 'lg' | 'md' | Font size scale |
onCellClick | (cell) => void | — | Cell click callback (receives source rows) |
Utility
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 is the same pure function the component uses internally — re-use it for CSV export, charting, or custom drill-down flows.
反馈与讨论
Pivot · Discussion