diff --git a/README.md b/README.md index d6c6aa50f..21a1ccd3e 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,41 @@ const YourChart: React.FC = 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 }) => ( + +); +``` + ### try local (dev mode) ```bash # packages/graphic-walker diff --git a/README.zh-CN.md b/README.zh-CN.md index 52f8820c5..1b3ef8055 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 }) => ( + +); +``` + #### 在Vue3中使用 虽然`graphic-walker`是一个React组件,但我们还是可以通过一些技巧在Vue3中使用它,比如利用`veaury`。 diff --git a/packages/graphic-walker/src/components/pivotTable/index.tsx b/packages/graphic-walker/src/components/pivotTable/index.tsx index 6b15eb460..f9359ff7e 100644 --- a/packages/graphic-walker/src/components/pivotTable/index.tsx +++ b/packages/graphic-walker/src/components/pivotTable/index.tsx @@ -105,8 +105,11 @@ const PivotTable: React.FC = function PivotTableComponent(props }, [enableCollapse, tableCollapsedHeaderMap]); useEffect(() => { + if (!enableCollapse && !showTableSummary) { + return; + } aggregateThenGenerate(); - }, [showTableSummary]); + }, [enableCollapse, showTableSummary]); const aggregateThenGenerate = async () => { await aggregateGroupbyData(); diff --git a/packages/graphic-walker/src/index.tsx b/packages/graphic-walker/src/index.tsx index addcafe69..2aa46063d 100644 --- a/packages/graphic-walker/src/index.tsx +++ b/packages/graphic-walker/src/index.tsx @@ -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'; diff --git a/packages/graphic-walker/src/pivotTable.tsx b/packages/graphic-walker/src/pivotTable.tsx new file mode 100644 index 000000000..641568bee --- /dev/null +++ b/packages/graphic-walker/src/pivotTable.tsx @@ -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 = ({ + 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(); + [...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(() => [], []); + + const { viewData } = useRenderer({ + allFields, + viewDimensions: dimensions, + viewMeasures: measures, + filters, + defaultAggregated: true, + sort, + limit: -1, + computationFunction: computation, + timezoneDisplayOffset, + }); + + const visualConfig = useMemo( + () => ({ + 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 ( +
+ +
+ ); +}; + +export default PivotTable; diff --git a/packages/playground/src/examples/nav.tsx b/packages/playground/src/examples/nav.tsx index c4ca3156a..bb0e9f176 100644 --- a/packages/playground/src/examples/nav.tsx +++ b/packages/playground/src/examples/nav.tsx @@ -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', diff --git a/packages/playground/src/examples/pages/pivotTable.stories.tsx b/packages/playground/src/examples/pages/pivotTable.stories.tsx new file mode 100644 index 000000000..2c078143d --- /dev/null +++ b/packages/playground/src/examples/pages/pivotTable.stories.tsx @@ -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('https://pub-2422ed4100b443659f588f2382cfc7b1.r2.dev/datasets/ds-students-service.json'); + const PivotTable = (GW as unknown as { PivotTable?: ComponentType }).PivotTable; + const [enableCollapse, setEnableCollapse] = useState(true); + + if (!PivotTable) { + return
`PivotTable` export is not available in current build.
; + } + + return ( +
+ +
+ Try clicking the +/- icon beside row and column headers (for example: gender, race/ethnicity, test preparation course). +
+ +
+ ); +} diff --git a/packages/playground/src/examples/pages/pivotTable.tsx b/packages/playground/src/examples/pages/pivotTable.tsx new file mode 100644 index 000000000..f1b7d3d5f --- /dev/null +++ b/packages/playground/src/examples/pages/pivotTable.tsx @@ -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 ( + + + + ); +}