Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,41 @@ const YourChart: React.FC<IYourChartProps> = props => {
export default YourChart;
```

You can also use `PivotTable` as a standalone API when you only need a pivot table without field drag-and-drop UI.

```typescript
import { PivotTable, type IViewField } from '@kanaries/graphic-walker';

const rowDimensions: IViewField[] = [
{ fid: 'region', name: 'Region', analyticType: 'dimension', semanticType: 'nominal' },
];

const columnDimensions: IViewField[] = [
{ fid: 'year', name: 'Year', analyticType: 'dimension', semanticType: 'ordinal' },
];

const values = [
{
fid: 'sales',
name: 'Sales',
analyticType: 'measure',
semanticType: 'quantitative',
aggName: 'sum',
placement: 'column' as const,
},
];

const YourPivotTable = ({ data }) => (
<PivotTable
data={data}
rowDimensions={rowDimensions}
columnDimensions={columnDimensions}
values={values}
numberFormat=".2s"
/>
);
```

### try local (dev mode)
```bash
# packages/graphic-walker
Expand Down
35 changes: 35 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,41 @@ const YourChart: React.FC = props => {
export default YourChart;
```

如果你只想使用数据透视表(Pivot Table),也可以直接使用独立的 `PivotTable` API,不需要字段拖拽配置 UI。

```typescript
import { PivotTable, type IViewField } from '@kanaries/graphic-walker';

const rowDimensions: IViewField[] = [
{ fid: 'region', name: '地区', analyticType: 'dimension', semanticType: 'nominal' },
];

const columnDimensions: IViewField[] = [
{ fid: 'year', name: '年份', analyticType: 'dimension', semanticType: 'ordinal' },
];

const values = [
{
fid: 'sales',
name: '销售额',
analyticType: 'measure',
semanticType: 'quantitative',
aggName: 'sum',
placement: 'column' as const,
},
];

const YourPivotTable = ({ data }) => (
<PivotTable
data={data}
rowDimensions={rowDimensions}
columnDimensions={columnDimensions}
values={values}
numberFormat=".2s"
/>
);
```

#### 在Vue3中使用

虽然`graphic-walker`是一个React组件,但我们还是可以通过一些技巧在Vue3中使用它,比如利用`veaury`。
Expand Down
5 changes: 4 additions & 1 deletion packages/graphic-walker/src/components/pivotTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ const PivotTable: React.FC<PivotTableProps> = function PivotTableComponent(props
}, [enableCollapse, tableCollapsedHeaderMap]);

useEffect(() => {
if (!enableCollapse && !showTableSummary) {
return;
}
aggregateThenGenerate();
}, [showTableSummary]);
}, [enableCollapse, showTableSummary]);

const aggregateThenGenerate = async () => {
await aggregateGroupbyData();
Expand Down
2 changes: 2 additions & 0 deletions packages/graphic-walker/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './root';
export { default as PureRenderer } from './renderer/pureRenderer';
export type { ILocalPureRendererProps, IRemotePureRendererProps } from './renderer/pureRenderer';
export { default as PivotTable } from './pivotTable';
export type { IPivotTableProps, IPivotTableValueField } from './pivotTable';
export { embedGraphicWalker, embedGraphicRenderer, embedPureRenderer, embedTableWalker } from './vanilla';
export * from './interfaces';
export * from './store/visualSpecStore';
Expand Down
147 changes: 147 additions & 0 deletions packages/graphic-walker/src/pivotTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useMemo } from 'react';
import PivotTableImpl from './components/pivotTable';
import { IFilterField, IRow, IViewField, IVisualConfigNew } from './interfaces';
import { emptyVisualLayout, initEncoding } from './utils/save';
import { useRenderer } from './renderer/hooks';
import { getComputation } from './computation/clientComputation';
import { getSort } from './utils';

export interface IPivotTableValueField extends IViewField {
placement?: 'row' | 'column';
}

export interface IPivotTableProps {
data: IRow[];
rowDimensions?: IViewField[];
columnDimensions?: IViewField[];
values?: IPivotTableValueField[];
enableCollapse?: boolean;
numberFormat?: string;
timezoneDisplayOffset?: number;
className?: string;
style?: React.CSSProperties;
}

function toViewField(field: IPivotTableValueField): IViewField {
const { placement: _placement, ...viewField } = field;
if (viewField.analyticType === 'measure' && !viewField.aggName) {
return {
...viewField,
aggName: 'sum',
};
}
return viewField;
}

function getPivotFieldKey(field: IViewField): string {
return [field.fid, field.aggName ?? '', field.sort ?? '', field.semanticType, field.analyticType].join(':');
}

const PivotTable: React.FC<IPivotTableProps> = ({
data,
rowDimensions = [],
columnDimensions = [],
values = [],
enableCollapse = false,
numberFormat,
timezoneDisplayOffset,
className,
style,
}) => {
const [rowValues, columnValues] = useMemo<[IViewField[], IViewField[]]>(() => {
const rowValues: IViewField[] = [];
const columnValues: IViewField[] = [];
values.forEach((field) => {
if ((field.placement ?? 'column') === 'row') {
rowValues.push(toViewField(field));
} else {
columnValues.push(toViewField(field));
}
});
return [rowValues, columnValues];
}, [values]);

const rows = useMemo(() => [...rowDimensions, ...rowValues], [rowDimensions, rowValues]);
const columns = useMemo(() => [...columnDimensions, ...columnValues], [columnDimensions, columnValues]);

const dimensions = useMemo(() => [...rows, ...columns].filter((field) => field.analyticType === 'dimension'), [rows, columns]);
const measures = useMemo(() => [...rows, ...columns].filter((field) => field.analyticType === 'measure'), [rows, columns]);

const draggableFieldState = useMemo(
() => ({
...initEncoding(),
dimensions,
measures,
rows,
columns,
}),
[dimensions, measures, rows, columns]
);

const allFields = useMemo(() => {
const map = new Map<string, IViewField>();
[...dimensions, ...measures].forEach((field) => {
map.set(getPivotFieldKey(field), field);
});
return [...map.values()];
}, [dimensions, measures]);

const computation = useMemo(() => getComputation(data), [data]);
const sort = useMemo(() => getSort({ rows, columns }), [rows, columns]);
const filters = useMemo<IFilterField[]>(() => [], []);

const { viewData } = useRenderer({
allFields,
viewDimensions: dimensions,
viewMeasures: measures,
filters,
defaultAggregated: true,
sort,
limit: -1,
computationFunction: computation,
timezoneDisplayOffset,
});

const visualConfig = useMemo<IVisualConfigNew>(
() => ({
defaultAggregated: true,
geoms: ['table'],
limit: -1,
timezoneDisplayOffset,
}),
[timezoneDisplayOffset]
);

const layout = useMemo(
() => ({
...emptyVisualLayout,
showTableSummary: false,
format: {
...emptyVisualLayout.format,
numberFormat,
},
}),
[numberFormat]
);

const pivotTableKey = useMemo(
() =>
`${rows.map(getPivotFieldKey).join('|')}::${columns.map(getPivotFieldKey).join('|')}::${timezoneDisplayOffset ?? ''}::${numberFormat ?? ''}::${enableCollapse}`,
[rows, columns, timezoneDisplayOffset, numberFormat, enableCollapse]
);

return (
<div className={className} style={style}>
<PivotTableImpl
key={pivotTableKey}
data={viewData}
draggableFieldState={draggableFieldState}
visualConfig={visualConfig}
layout={layout}
disableCollapse={!enableCollapse}
/>
</div>
);
};

export default PivotTable;
4 changes: 4 additions & 0 deletions packages/playground/src/examples/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const pages = [
comp: () => import('./pages/table'),
name: 'TableWalker',
},
{
comp: () => import('./pages/pivotTable'),
name: 'PivotTable',
},
{
comp: () => import('./pages/tableSettings'),
name: 'TableWalker Settings',
Expand Down
101 changes: 101 additions & 0 deletions packages/playground/src/examples/pages/pivotTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as GW from '@kanaries/graphic-walker';
import { IViewField } from '@kanaries/graphic-walker';
import { IDataSource, useFetch } from '../util';
import { ComponentType, useState } from 'react';

type PivotTableValueField = IViewField & {
placement?: 'row' | 'column';
};

const rowDimensions: IViewField[] = [
{
fid: 'gender',
name: 'gender',
analyticType: 'dimension',
semanticType: 'nominal',
},
{
fid: 'race/ethnicity',
name: 'race/ethnicity',
analyticType: 'dimension',
semanticType: 'nominal',
},
{
fid: 'parental level of education',
name: 'parental level of education',
analyticType: 'dimension',
semanticType: 'nominal',
},
];

const columnDimensions: IViewField[] = [
{
fid: 'test preparation course',
name: 'test preparation course',
analyticType: 'dimension',
semanticType: 'nominal',
},
{
fid: 'lunch',
name: 'lunch',
analyticType: 'dimension',
semanticType: 'nominal',
},
];

const values: PivotTableValueField[] = [
{
fid: 'math score',
name: 'math score',
analyticType: 'measure',
semanticType: 'quantitative',
aggName: 'mean',
placement: 'column',
},
{
fid: 'reading score',
name: 'reading score',
analyticType: 'measure',
semanticType: 'quantitative',
aggName: 'mean',
placement: 'column',
},
{
fid: 'writing score',
name: 'writing score',
analyticType: 'measure',
semanticType: 'quantitative',
aggName: 'median',
placement: 'column',
},
];

export default function PivotTableComponent() {
const { dataSource } = useFetch<IDataSource>('https://pub-2422ed4100b443659f588f2382cfc7b1.r2.dev/datasets/ds-students-service.json');
const PivotTable = (GW as unknown as { PivotTable?: ComponentType<any> }).PivotTable;
const [enableCollapse, setEnableCollapse] = useState(true);

if (!PivotTable) {
return <div className="p-4">`PivotTable` export is not available in current build.</div>;
}

return (
<div className="p-4 flex flex-col gap-3">
<label className="inline-flex items-center gap-2 text-sm">
<input type="checkbox" checked={enableCollapse} onChange={(e) => setEnableCollapse(e.target.checked)} />
Enable collapse / expand
</label>
<div className="text-xs text-zinc-600 dark:text-zinc-300">
Try clicking the +/- icon beside row and column headers (for example: gender, race/ethnicity, test preparation course).
</div>
<PivotTable
data={dataSource}
rowDimensions={rowDimensions}
columnDimensions={columnDimensions}
values={values}
numberFormat=".2f"
enableCollapse={enableCollapse}
/>
</div>
);
}
11 changes: 11 additions & 0 deletions packages/playground/src/examples/pages/pivotTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Example from '../components/examplePage';
import code from './pivotTable.stories?raw';
import Comp from './pivotTable.stories';

export default function PivotTableExample() {
return (
<Example name="PivotTable" desc="Standalone PivotTable API with controlled collapse/expand interaction." code={code}>
<Comp />
</Example>
);
}