diff --git a/common/changes/@visactor/vtable/1091-bug-selectBoderExtend_2024-02-07-09-29.json b/common/changes/@visactor/vtable/1091-bug-selectBoderExtend_2024-02-07-09-29.json deleted file mode 100644 index a20dd8c11..000000000 --- a/common/changes/@visactor/vtable/1091-bug-selectBoderExtend_2024-02-07-09-29.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: edit right frozen cell input position error\n\n", - "type": "none", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "892739385@qq.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/1112-bug-onmouseleavecell-trigger_2024-02-20-07-59.json b/common/changes/@visactor/vtable/1112-bug-onmouseleavecell-trigger_2024-02-20-07-59.json deleted file mode 100644 index baeda1440..000000000 --- a/common/changes/@visactor/vtable/1112-bug-onmouseleavecell-trigger_2024-02-20-07-59.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: mouseleave_cell event trigger #1112\n\n", - "type": "none", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "892739385@qq.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-cellBgColor_2024-02-22-09-01.json b/common/changes/@visactor/vtable/fix-cellBgColor_2024-02-22-09-01.json deleted file mode 100644 index 31b0a9a74..000000000 --- a/common/changes/@visactor/vtable/fix-cellBgColor_2024-02-22-09-01.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vtable", - "comment": "fix: fix cellBgColor judgement in isCellHover()", - "type": "none" - } - ], - "packageName": "@visactor/vtable" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-custom-merge-height_2024-02-06-08-19.json b/common/changes/@visactor/vtable/fix-custom-merge-height_2024-02-06-08-19.json deleted file mode 100644 index 8d5109f06..000000000 --- a/common/changes/@visactor/vtable/fix-custom-merge-height_2024-02-06-08-19.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vtable", - "comment": "fix: fix custom merge cell computed height&width", - "type": "none" - } - ], - "packageName": "@visactor/vtable" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-merge-setDropDownMenuHighlight_2024-02-20-10-09.json b/common/changes/@visactor/vtable/fix-merge-setDropDownMenuHighlight_2024-02-20-10-09.json deleted file mode 100644 index e9147d3f2..000000000 --- a/common/changes/@visactor/vtable/fix-merge-setDropDownMenuHighlight_2024-02-20-10-09.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vtable", - "comment": "fix: merge cell update in setDropDownMenuHighlight()", - "type": "none" - } - ], - "packageName": "@visactor/vtable" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-react-strict_2024-02-06-09-17.json b/common/changes/@visactor/vtable/fix-react-strict_2024-02-06-09-17.json deleted file mode 100644 index 6f30fc4d6..000000000 --- a/common/changes/@visactor/vtable/fix-react-strict_2024-02-06-09-17.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vtable", - "comment": "fix: fix react-vtable display error in react strict mode #990", - "type": "none" - } - ], - "packageName": "@visactor/vtable" -} \ No newline at end of file diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index ae5a028b3..a2147593e 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"0.19.1","mainProject":"@visactor/vtable","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"0.20.1","mainProject":"@visactor/vtable","nextBump":"patch"}] diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index 9e1b07ee2..f4235a7f5 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -13,6 +13,7 @@ Update table configuration items, which will be automatically redrawn after bein */ updateOption(options: BaseTableConstructorOptions) => void ``` + If you need to update a single configuration item, please refer to the other `update**` interfaces below ## updateTheme(Function) @@ -26,15 +27,20 @@ Update the table theme and it will be automatically redrawn after calling it. */ updateTheme(theme: ITableThemeDefine) => void ``` + use: + ``` tableInstance.updateTheme(newTheme) ``` + Corresponding attribute update interface(https://visactor.io/vtable/guide/basic_function/update_option): + ``` // will not automatically redraw after calling tableInstance.theme = newTheme; ``` + ## updateColumns(Function) Update the configuration information of the columns field of the table, and it will be automatically redrawn after calling @@ -46,15 +52,20 @@ Update the configuration information of the columns field of the table, and it w */ updateColumns(columns: ColumnsDefine) => void ``` + use: + ``` tableInstance. updateColumns(newColumns) ``` + Corresponding attribute update interface(https://visactor.io/vtable/guide/basic_function/update_option): + ``` // will not automatically redraw after calling tableInstance.columns = newColumns; ``` + ## updatePagination(Function) Update page number configuration information, and it will be automatically redrawn after calling @@ -66,7 +77,9 @@ Update page number configuration information, and it will be automatically redra */ updatePagination(pagination: IPagination): void; ``` + IPagination type define: + ``` /** *Paging configuration @@ -80,14 +93,17 @@ export interface IPagination { currentPage?: number; } ``` + The basic table and VTable data analysis pivot table support paging, but the pivot combination chart does not support paging. Note! The perPageCount in the pivot table will be automatically corrected to an integer multiple of the number of indicators. ## renderWithRecreateCells(Function) + Re-collect the cell objects and re-render the table. Use scenarios such as: Refresh after batch updating multiple configuration items: + ``` tableInstance.theme = newThemeObj; tableInstance.widthMode = 'autoWidth'; @@ -128,8 +144,10 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** The basic t ``` ## getDrawRange(Function) + Get the boundRect value of the actual drawn content area of the table like + ``` { "bounds": { @@ -146,6 +164,7 @@ like width: 1580 } ``` + ## selectCell(Function) Select a cell. If empty is passed, the currently selected highlight state will be cleared. @@ -176,6 +195,7 @@ Select one or more cell ranges ``` ## getSelectedCellInfos(Function) + Get the selected cell information, and the returned result is a two-dimensional array. The first-level array item represents a row, and each item of the second-level array represents a cell information of the row. ``` @@ -234,6 +254,7 @@ Getting the style of a cell */ getCellStyle(col: number, row: number) => CellStyle ``` + ## getRecordByCell(Function) Get the data item of this cell @@ -256,6 +277,7 @@ Get the column index and row index in the body part according to the row and col /** Get the column index and row index in the body part based on the row and column numbers of the table cells */ getBodyIndexByTableIndex: (col: number, row: number) => CellAddress; ``` + ## getTableIndexByBodyIndex(Function) Get the row and column number of the cell based on the column index and row index of the body part @@ -266,40 +288,59 @@ Get the row and column number of the cell based on the column index and row inde ``` ## getTableIndexByRecordIndex(Function) -Get the index row number or column number displayed in the table based on the index of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). + +Get the index row number or column number displayed in the table based on the index of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). Note: ListTable specific interface ``` /** - * Get the index row number or column number displayed in the table based on the index of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). - + * Get the index row number or column number displayed in the table based on the index of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). + Note: ListTable specific interface * @param recordIndex */ getTableIndexByRecordIndex: (recordIndex: number) => number; ``` +## getRecordIndexByCell(Function) + +Get the number of data in the current cell in the data source. + +If it is a table in tree mode, an array will be returned, such as [1,2], the 3rd item in the children of the 2nd item in the data source. + +** ListTable proprietary ** + +``` + /** Get the number of the data in the current cell in the data source. + * If it is a table in tree mode, an array will be returned, such as [1,2], the 3rd item in the children of the 2nd item in the data source + * Note: ListTable specific interface */ + getRecordIndexByCell(col: number, row: number): number | number[] +** ListTable proprietary ** +``` + ## getTableIndexByField(Function) + Get the index row number or column number displayed in the table according to the field of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). - Note: ListTable specific interface +Note: ListTable specific interface + ``` /** - * Get the index row number or column number displayed in the table according to the field of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). - + * Get the index row number or column number displayed in the table according to the field of the data source (Related to transposition, the non-transposition obtains the row number, and the transposed table obtains the column number). + Note: ListTable specific interface * @param recordIndex */ getTableIndexByField: (field: FieldDef) => number; ``` - ## getRecordShowIndexByCell(Function) Get the index of the current cell data in the body part, that is, remove the index of the header level number by the row and column number.(Related to transpose, the non-transpose gets the body row number, and the transpose table gets the body column number) ** ListTable proprietary ** + ``` /** Get the display index of the current cell in the body part,it is ( row / col )- headerLevelCount. Note: ListTable specific interface */ getRecordShowIndexByCell(col: number, row: number): number @@ -307,9 +348,10 @@ Get the index of the current cell data in the body part, that is, remove the ind ## getCellAddrByFieldRecord(Function) -Get the cell row and column number based on the index and field in the data source. +Get the cell row and column number based on the index and field in the data source. Note: ListTable specific interface + ``` /** * Get the cell row and column number based on the index and field in the data source. Note: ListTable specific interface @@ -319,6 +361,7 @@ Note: ListTable specific interface */ getCellAddrByFieldRecord: (field: FieldDef, recordIndex: number) => CellAddress; ``` + ## getCellOriginRecord(Function) Get the source data item of this cell. @@ -405,7 +448,9 @@ Get the text of the cell with omitted text. ``` ## getCellRect(Function) + Get the specific position of the cell in the entire table. + ``` /** * Get the range of cells. The return value is Rect type. Regardless of whether it is a merged cell, the coordinates start from 0 @@ -417,7 +462,9 @@ Get the specific position of the cell in the entire table. ``` ## getCellRelativeRect(Function) + Get the specific position of the cell in the entire table. Relative position is based on the upper left corner of the table (scroll condition minus scroll value) + ``` /** * The obtained position is relative to the upper left corner of the table display interface. In case of scrolling, if the cell has rolled out of the top of the table, the y of this cell will be a negative value. @@ -444,7 +491,6 @@ Get the path to the row list header {{ use: ICellHeaderPaths() }} - ## getCellHeaderTreeNodes(Function) Obtain the header tree node based on the row and column number, which includes the user's custom attributes on the custom tree rowTree and columnTree trees (it is also the node of the internal layout tree, please do not modify it at will after obtaining it).Under normal circumstances, just use getCellHeaderPaths. @@ -492,28 +538,37 @@ For pivot table interfaces, get specific cell addresses based on the header dime | IDimensionInfo[] ) => CellAddress ``` + ## getCheckboxState(Function) + Get the selected status of all data in the checkbox under a certain field. The order corresponds to the original incoming data records. It does not correspond to the status value of the row displayed in the table. + ``` getCheckboxState(field?: string | number): Array ``` ## getCellCheckboxState(Function) + Get the status of a cell checkbox + ``` getCellCheckboxState(col: number, row: number): Array ``` ## getScrollTop(Function) + Get the current vertical scroll position ## getScrollLeft(Function) + Get the current horizontal scroll position ## setScrollTop(Function) + Set the vertical scroll position (the rendering interface will be updated) ## setScrollLeft(Function) + Set the horizontal scroll position (the rendering interface will be updated) ## scrollToCell(Function) @@ -527,8 +582,11 @@ Scroll to a specific cell location */ scrollToCell(cellAddr: { col?: number; row?: number })=>void ``` + ## toggleHierarchyState(Function) + Tree expand and collapse state switch + ``` /** * Header switches level status @@ -537,8 +595,11 @@ Tree expand and collapse state switch */ toggleHierarchyState(col: number, row: number) ``` + ## getHierarchyState(Function) + Get the tree-shaped expanded or collapsed status of a certain cell + ``` /** * Get the collapsed and expanded status of hierarchical nodes @@ -554,10 +615,13 @@ enum HierarchyState { none = 'none' } ``` + ## getLayoutRowTree(Function) + ** PivotTable Proprietary ** Get the table row header tree structure + ``` /** * Get the table row tree structure @@ -567,11 +631,13 @@ Get the table row header tree structure ``` ## getLayoutRowTreeCount(Function) + ** PivotTable Proprietary ** Get the total number of nodes occupying the table row header tree structure. Note: The logic distinguishes between flat and tree hierarchies. + ``` /** * Get the total number of nodes occupying the table row header tree structure. @@ -592,9 +658,11 @@ Update the sort status, ListTable exclusive */ updateSortState(sortState: SortState[] | SortState | null, executeSort: boolean = true) ``` + ## updateSortRules(Function) Pivot table update sorting rules, exclusive to PivotTable + ``` /** * Full update of sorting rules @@ -745,6 +813,7 @@ Export a picture of a certain cell range ``` ## changeCellValue(Function) + Change the value of a cell: ``` @@ -753,6 +822,7 @@ Change the value of a cell: ``` ## changeCellValues(Function) + Change the value of cells in batches: ``` @@ -797,13 +867,14 @@ End editing Get all data of the current table ## dataSouce(CachedDataSource) + Set the data source for the VTable table component instance. For specific usage, please refer to [Asynchronous data lazy loading demo](../demo/performance/async-data) and [Tutorial](../guide/data/async_data) ## addRecords(Function) - Add data, support multiple pieces of data +Add data, support multiple pieces of data -** Note: ListTable specific interface ** +** Note: ListTable specific interface ** ``` /** @@ -818,9 +889,9 @@ Set the data source for the VTable table component instance. For specific usage, ## addRecord(Function) - Add data, single piece of data +Add data, single piece of data -** Note: ListTable specific interface ** +** Note: ListTable specific interface ** ``` /** @@ -837,7 +908,7 @@ Set the data source for the VTable table component instance. For specific usage, Delete data supports multiple pieces of data -** Note: ListTable specific interface ** +** Note: ListTable specific interface ** ``` /** @@ -846,11 +917,13 @@ Delete data supports multiple pieces of data */ deleteRecords(recordIndexs: number[]) ``` + ## updateRecords(Function) Modify data to support multiple pieces of data ** ListTable proprietary ** + ``` /** * Modify data to support multiple pieces of data @@ -872,10 +945,12 @@ Get the display cell range of the table body part ## getBodyVisibleColRange(Function) Get the displayed column number range in the body part of the table + ``` /** Get the displayed column number range in the body part of the table */ getBodyVisibleColRange: () => { colStart: number; colEnd: number }; ``` + ## getBodyVisibleRowRange(Function) Get the displayed row number range of the table body part @@ -883,4 +958,8 @@ Get the displayed row number range of the table body part ``` /** Get the displayed row number range of the table body */ getBodyVisibleRowRange: () => { rowStart: number; rowEnd: number }; -``` \ No newline at end of file +``` + +## getAggregateValuesByField(Function) + +Get aggregation summary value diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index 3fdfb03fd..cacc07a8c 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -13,6 +13,7 @@ */ updateOption(options: BaseTableConstructorOptions) => void ``` + 如果需要更新单个配置项,请参考下面其他`update**`接口 ## updateTheme(Function) @@ -26,18 +27,23 @@ */ updateTheme(theme: ITableThemeDefine) => void ``` + 使用: + ``` tableInstance.updateTheme(newTheme) ``` + 对应属性更新接口(可参考教程:https://visactor.io/vtable/guide/basic_function/update_option): + ``` // 调用后不会自动重绘 tableInstance.theme = newTheme; ``` + ## updateColumns(Function) -更新表格的columns字段配置信息,调用后会自动重绘。 +更新表格的 columns 字段配置信息,调用后会自动重绘。 ```ts /** @@ -46,15 +52,20 @@ tableInstance.theme = newTheme; */ updateColumns(columns: ColumnsDefine) => void ``` + 使用: + ``` tableInstance.updateColumns(newColumns) ``` + 对应属性更新接口(可参考教程:https://visactor.io/vtable/guide/basic_function/update_option): + ``` // 调用后不会自动重绘 tableInstance.columns = newColumns; ``` + ## updatePagination(Function) 更新页码配置信息 调用后会自动重绘。 @@ -66,7 +77,9 @@ tableInstance.columns = newColumns; */ updatePagination(pagination: IPagination): void; ``` + 其中类型: + ``` /** * 分页配置 @@ -80,14 +93,17 @@ export interface IPagination { currentPage?: number; } ``` -基本表格和VTable数据分析透视表支持分页,透视组合图不支持分页。 -注意! 透视表中perPageCount会自动修正为指标数量的整数倍。 +基本表格和 VTable 数据分析透视表支持分页,透视组合图不支持分页。 + +注意! 透视表中 perPageCount 会自动修正为指标数量的整数倍。 ## renderWithRecreateCells(Function) + 重新组织单元格对象树并重新渲染表格,使用场景如: 批量更新多个配置项后的刷新: + ``` tableInstance.theme = newThemeObj; tableInstance.widthMode = 'autoWidth'; @@ -119,7 +135,7 @@ tableInstance.renderWithRecreateCells(); ## setRecords(Function) 设置表格数据接口,可作为更新接口调用。 -** 基本表格可同时设置排序状态对表格数据排序,sort设置为空清空排序状态,如果不设置则按当前排序状态对传入数据排序 ** +** 基本表格可同时设置排序状态对表格数据排序,sort 设置为空清空排序状态,如果不设置则按当前排序状态对传入数据排序 ** ``` setRecords(records: Array) //透视表 @@ -127,8 +143,10 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ``` ## getDrawRange(Function) -获取表格实际绘制内容区域的boundRect的值 + +获取表格实际绘制内容区域的 boundRect 的值 如 + ``` { "bounds": { @@ -145,6 +163,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 width: 1580 } ``` + ## selectCell(Function) 选中某个单元格。如果传空,则清除当前选中高亮状态。 @@ -169,6 +188,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 */ selectCells(cellRanges: CellRange[]): void ``` + 其中: {{ use: CellRange() }} @@ -200,7 +220,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ## getCellOriginValue(Function) -获取单元格展示数据的format前的值 +获取单元格展示数据的 format 前的值 ``` /** @@ -232,6 +252,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 */ getCellStyle(col: number, row: number) => CellStyle ``` + ## getRecordByCell(Function) 获取该单元格的数据项 @@ -248,15 +269,16 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ## getBodyIndexByTableIndex(Function) -根据表格单元格的行列号 获取在body部分的列索引及行索引 +根据表格单元格的行列号 获取在 body 部分的列索引及行索引 ``` /** 根据表格单元格的行列号 获取在body部分的列索引及行索引 */ getBodyIndexByTableIndex: (col: number, row: number) => CellAddress; ``` + ## getTableIndexByBodyIndex(Function) -根据body部分的列索引及行索引,获取单元格的行列号 +根据 body 部分的列索引及行索引,获取单元格的行列号 ``` /** 根据body部分的列索引及行索引,获取单元格的行列号 */ @@ -264,24 +286,43 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ``` ## getTableIndexByRecordIndex(Function) -根据数据源的index 获取显示到表格中的index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。 -** ListTable 专有 ** +根据数据源的 index 获取显示到表格中的 index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。 + +** ListTable 专有 ** ``` /** * 根据数据源的index 获取显示到表格中的index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。 - + 注:ListTable特有接口 * @param recordIndex */ getTableIndexByRecordIndex: (recordIndex: number) => number; ``` +## getRecordIndexByCell(Function) + +获取当前单元格的数据是数据源中的第几条。 + +如果是树形模式的表格,将返回数组,如[1,2] 数据源中第2条数据中children中的第3条。 + +** ListTable 专有 ** + +``` + /** 获取当前单元格的数据是数据源中的第几条。 + * 如果是树形模式的表格,将返回数组,如[1,2] 数据源中第2条数据中children中的第3条 + * 注:ListTable特有接口 */ + getRecordIndexByCell(col: number, row: number): number | number[] +** ListTable 专有 ** +``` + ## getTableIndexByField(Function) -根据数据源的field 获取显示到表格中的index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。 -** ListTable 专有 ** +根据数据源的 field 获取显示到表格中的 index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。 + +** ListTable 专有 ** + ``` /** * 根据数据源的field 获取显示到表格中的index 行号或者列号(与转置相关,非转置获取的是行号,转置表获取的是列号)。注:ListTable特有接口 @@ -290,12 +331,12 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 getTableIndexByField: (field: FieldDef) => number; ``` - ## getRecordShowIndexByCell(Function) -获取当前单元格数据在body部分的索引,即通过行列号去除表头层级数的索引(与转置相关,非转置获取的是body行号,转置表获取的是body列号)。 +获取当前单元格数据在 body 部分的索引,即通过行列号去除表头层级数的索引(与转置相关,非转置获取的是 body 行号,转置表获取的是 body 列号)。 + +** ListTable 专有 ** -** ListTable 专有 ** ``` /** 获取当前单元格在body部分的展示索引,即( row / col )- headerLevelCount。注:ListTable特有接口 */ getRecordShowIndexByCell(col: number, row: number): number @@ -303,9 +344,10 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ## getCellAddrByFieldRecord(Function) -根据数据源中的index和field获取单元格行列号。 +根据数据源中的 index 和 field 获取单元格行列号。 + +注:ListTable 特有接口 -注:ListTable特有接口 ``` /** * 根据数据源中的index和field获取单元格行列号。注:ListTable特有接口 @@ -315,6 +357,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 */ getCellAddrByFieldRecord: (field: FieldDef, recordIndex: number) => CellAddress; ``` + ## getCellOriginRecord(Function) 获取该单元格的源数据项。 @@ -332,6 +375,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 */ getCellOriginRecord(col: number, row: number) ``` + ## getAllCells(Function) 获取所有单元格上下文信息 @@ -400,7 +444,9 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ``` ## getCellRect(Function) + 获取单元格在整张表格中的具体位置。 + ``` /** * 获取单元格的范围 返回值为Rect类型。不考虑是否为合并单元格的情况,坐标从0开始 @@ -412,7 +458,9 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ``` ## getCellRelativeRect(Function) + 获取单元格在整张表格中的具体位置。相对位置是基于表格左上角(滚动情况减去滚动值) + ``` /** * 获取的位置是相对表格显示界面的左上角 情况滚动情况 如单元格已经滚出表格上方 则这个单元格的y将为负值 @@ -441,7 +489,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ## getCellHeaderTreeNodes(Function) -根据行列号获取表头tree节点,包含了用户在自定义树rowTree及columnTree树上的自定义属性(也是内部布局树的节点,获取后请不要随意修改)。一般情况下用getCellHeaderPaths即可。 +根据行列号获取表头 tree 节点,包含了用户在自定义树 rowTree 及 columnTree 树上的自定义属性(也是内部布局树的节点,获取后请不要随意修改)。一般情况下用 getCellHeaderPaths 即可。 ``` /** @@ -455,7 +503,7 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ## getCellAddress(Function) -根据数据和 field 属性字段名称获取 body 中某条数据的行列号。目前仅支持基本表格ListTable +根据数据和 field 属性字段名称获取 body 中某条数据的行列号。目前仅支持基本表格 ListTable ``` /** @@ -488,32 +536,41 @@ setRecords(records: Array, sort?: SortState | SortState[]) //** 基本表 ``` ## getCheckboxState(Function) -获取某个字段下checkbox 全部数据的选中状态 顺序对应原始传入数据records 不是对应表格展示row的状态值 + +获取某个字段下 checkbox 全部数据的选中状态 顺序对应原始传入数据 records 不是对应表格展示 row 的状态值 + ``` getCheckboxState(field?: string | number): Array ``` ## getCellCheckboxState(Function) -获取某个单元格checkbox的状态 + +获取某个单元格 checkbox 的状态 + ``` getCellCheckboxState(col: number, row: number): Array ``` + ## getScrollTop(Function) + 获取当前竖向滚动位置 ## getScrollLeft(Function) + 获取当前横向滚动位置 ## setScrollTop(Function) + 设置竖向滚动位置 (会更新渲染界面) ## setScrollLeft(Function) + 设置横向滚动位置(会更新渲染界面) ## scrollToCell(Function) 滚动到具体某个单元格位置。 -col或者row可以为空,为空的话也就是只移动x方向或者y方向。 +col 或者 row 可以为空,为空的话也就是只移动 x 方向或者 y 方向。 ``` /** @@ -522,18 +579,24 @@ col或者row可以为空,为空的话也就是只移动x方向或者y方向。 */ scrollToCell(cellAddr: { col?: number; row?: number })=>void ``` + ## toggleHierarchyState(Function) + 树形展开收起状态切换 + ``` /** * 表头切换层级状态 * @param col * @param row */ - toggleHierarchyState(col: number, row: number) + toggleHierarchyState(col: number, row: number) ``` + ## getHierarchyState(Function) -获取某个单元格树形展开or收起状态 + +获取某个单元格树形展开 or 收起状态 + ``` /** * 获取层级节点收起展开的状态 @@ -551,9 +614,11 @@ enum HierarchyState { ``` ## getLayoutRowTree(Function) -** PivotTable 专有 ** + +** PivotTable 专有 ** 获取表格行头树形结构 + ``` /** * 获取表格行树状结构 @@ -563,11 +628,13 @@ enum HierarchyState { ``` ## getLayoutRowTreeCount(Function) -** PivotTable 专有 ** + +** PivotTable 专有 ** 获取表格行头树形结构的占位的总节点数。 注意:逻辑中区分了平铺和树形层级结构 + ``` /** * 获取表格行头树形结构的占位的总节点数。 @@ -588,9 +655,11 @@ enum HierarchyState { */ updateSortState(sortState: SortState[] | SortState | null, executeSort: boolean = true) ``` + ## updateSortRules(Function) 透视表更新排序规则,PivotTable 专有 + ``` /** * 全量更新排序规则 @@ -741,7 +810,8 @@ use case: 点击图例项后 更新过滤规则 来更新图表 ``` ## changeCellValue(Function) -更改单元格的value值: + +更改单元格的 value 值: ``` /** 设置单元格的value值,注意对应的是源数据的原始值,vtable实例records会做对应修改 */ @@ -749,7 +819,8 @@ use case: 点击图例项后 更新过滤规则 来更新图表 ``` ## changeCellValues(Function) -批量更改单元格的value: + +批量更改单元格的 value: ``` /** @@ -758,7 +829,7 @@ use case: 点击图例项后 更新过滤规则 来更新图表 * @param row 粘贴数据的起始行号 * @param values 多个单元格的数据数组 */ - changeCellValues(startCol: number, startRow: number, values: string[][]) + changeCellValues(startCol: number, startRow: number, values: string[][]) ``` ## getEditor(Function) @@ -793,13 +864,15 @@ use case: 点击图例项后 更新过滤规则 来更新图表 获取当前表格的全部数据 ## dataSouce(CachedDataSource) -给VTable表格组件实例设置数据源,具体使用可以参考[异步懒加载数据demo](../demo/performance/async-data)及[教程](../guide/data/async_data) + +给 VTable 表格组件实例设置数据源,具体使用可以参考[异步懒加载数据 demo](../demo/performance/async-data)及[教程](../guide/data/async_data) ## addRecords(Function) - 添加数据,支持多条数据 - -** ListTable 专有 ** +添加数据,支持多条数据 + +** ListTable 专有 ** + ``` /** * 添加数据 支持多条数据 @@ -808,14 +881,15 @@ use case: 点击图例项后 更新过滤规则 来更新图表 * 如果设置了排序规则recordIndex无效,会自动适应排序逻辑确定插入顺序。 * recordIndex 可以通过接口getRecordShowIndexByCell获取 */ - addRecords(records: any[], recordIndex?: number) + addRecords(records: any[], recordIndex?: number) ``` ## addRecord(Function) - 添加数据,单条数据 +添加数据,单条数据 + +** ListTable 专有 ** -** ListTable 专有 ** ``` /** * 添加数据 单条数据 @@ -831,20 +905,22 @@ use case: 点击图例项后 更新过滤规则 来更新图表 删除数据 支持多条数据 -** ListTable 专有 ** +** ListTable 专有 ** + ``` /** * 删除数据 支持多条数据 * @param recordIndexs 要删除数据的索引(显示到body中的条目索引) */ - deleteRecords(recordIndexs: number[]) + deleteRecords(recordIndexs: number[]) ``` ## updateRecords(Function) 修改数据 支持多条数据 -** ListTable 专有 ** +** ListTable 专有 ** + ``` /** * 修改数据 支持多条数据 @@ -856,7 +932,7 @@ use case: 点击图例项后 更新过滤规则 来更新图表 ## getBodyVisibleCellRange(Function) -获取表格body部分的显示单元格范围 +获取表格 body 部分的显示单元格范围 ``` /** 获取表格body部分的显示单元格范围 */ @@ -865,16 +941,22 @@ use case: 点击图例项后 更新过滤规则 来更新图表 ## getBodyVisibleColRange(Function) -获取表格body部分的显示列号范围 +获取表格 body 部分的显示列号范围 + ``` /** 获取表格body部分的显示列号范围 */ getBodyVisibleColRange: () => { colStart: number; colEnd: number }; ``` + ## getBodyVisibleRowRange(Function) -获取表格body部分的显示行号范围 +获取表格 body 部分的显示行号范围 ``` /** 获取表格body部分的显示行号范围 */ getBodyVisibleRowRange: () => { rowStart: number; rowEnd: number }; ``` + +## getAggregateValuesByField(Function) + +获取聚合汇总的值 diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index c2000a7d9..e3a163d5b 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,30 @@ +# v0.20.0 + +2024-02-23 + + +**🆕 New feature** + +- **@visactor/vtable**: add aggregation for list table column +- **@visactor/vtable**: add api getAggregateValuesByField +- **@visactor/vtable**: add custom aggregation +- **@visactor/vtable**: chartSpec support function [#1115](https://github.com/VisActor/VTable/issues/1115) +- **@visactor/vtable**: add filter data config [#607](https://github.com/VisActor/VTable/issues/607) + +**🐛 Bug fix** + +- **@visactor/vtable**: edit right frozen cell input position error +- **@visactor/vtable**: mouseleave_cell event trigger [#1112](https://github.com/VisActor/VTable/issues/1112) +- **@visactor/vtable**: fix cellBgColor judgement in isCellHover() +- **@visactor/vtable**: fix custom merge cell computed height&width +- **@visactor/vtable**: fix content position update problem +- **@visactor/vtable**: merge cell update in setDropDownMenuHighlight() +- **@visactor/vtable**: fix react-vtable display error in react strict mode [#990](https://github.com/VisActor/VTable/issues/990) + + + +[more detail about v0.20.0](https://github.com/VisActor/VTable/releases/tag/v0.20.0) + # v0.19.1 2024-02-06 diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 724d438c7..eb030e069 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,29 @@ +# v0.20.0 + +2024-02-23 + + +**🆕 新增功能** + +- **@visactor/vtable**:添加列表列的聚合 +- **@visactor/vtable**:添加 api getAggregateValuesByField +- **@visactor/vtable**:添加自定义聚合 +- **@visactor/vtable**:chartSpec 支持函数 [#1115](https://github.com/VisActor/VTable/issues/1115) +- **@visactor/vtable**:添加基本表格的过滤能力 [#607](https://github.com/VisActor/VTable/issues/607) + +**🐛 功能修复** + +- **@visactor/vtable**:编辑右冻结单元格输入位置错误 +- **@visactor/vtable**:mouseleave_cell 事件触发器 [#1112](https://github.com/VisActor/VTable/issues/1112) +- **@visactor/vtable**:修复 isCellHover() 中的 cellBgColor 判断 +- **@visactor/vtable**:修复自定义合并单元计算的高度和宽度 +- **@visactor/vtable**:修复内容位置更新问题 +- **@visactor/vtable**:在 setDropDownMenuHighlight() 中合并单元格更新 +- **@visactor/vtable**:修复react严格模式下的react-vtable显示错误[#990](https://github.com/VisActor/VTable/issues/990) + + +[更多详情请查看 v0.20.0](https://github.com/VisActor/VTable/releases/tag/v0.20.0) + # v0.19.1 2024-02-06 diff --git a/docs/assets/demo/en/cell-type/list-chart.md b/docs/assets/demo/en/cell-type/list-chart.md new file mode 100644 index 000000000..b719c52eb --- /dev/null +++ b/docs/assets/demo/en/cell-type/list-chart.md @@ -0,0 +1,543 @@ +--- +category: examples +group: Cell Type +title: List table integrated chart +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-chart-multiple.png +link: '../guide/cell_type/chart' +option: ListTable-columns-chart#cellType +--- + +# Basic table integrated chart + +Combine vchart chart library and render it into tables to enrich visual display forms and improve multi-chart rendering performance. This example refers to vchart’s bar line pie chart. For details, please refer to: https://visactor.io/vchart/demo/progress/linear-progress-with-target-value + +## Key Configurations + +- `VTable.register.chartModule('vchart', VChart)` registers the chart library for drawing charts. Currently supports VChart +- `cellType: 'chart'` specifies the type chart +- `chartModule: 'vchart'` specifies the registered chart library name +- `chartSpec: {}` chart spec + +## Code Demo + +```javascript livedemo template=vtable + VTable.register.chartModule('vchart', VChart); + const columns = [ + { + field: 'id', + title: 'id', + sort: true, + width: 80, + style: { + textAlign: 'left', + bgColor: '#ea9999' + } + }, + { + field: 'areaChart', + title: 'multiple vchart type', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec(args) { + if (args.row % 3 == 2) + return { + type: 'area', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } + }, + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } + }, + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } + } + }, + + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + else if (args.row % 3 == 1) + return { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + }, + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + return { + type: 'pie', + data: { id: 'data1' }, + categoryField: 'y', + valueField: 'x' + } + } + }, + { + field: 'lineChart', + title: 'vchart line', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + }, + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + }, + { + field: 'barChart', + title: 'vchart bar', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'bar', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + bar: { + state: { + hover: { + fill: 'green' + }, + selected: { + fill: 'orange' + }, + hover_reverse: { + fill: '#ccc' + } + } + } + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + } + }, + { + field: 'scatterChart', + title: 'vchart scatter', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'scatter', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type' + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + } + }, + { + field: 'areaChart', + title: 'vchart area', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'area', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } + }, + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } + }, + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } + } + } + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + }, + ]; + const records = []; + for (let i = 1; i <= 10; i++) + records.push({ + id: i, + areaChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: '773' }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: 796 + 100 * i }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + lineChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + barChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + scatterChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ] + }); + const option = { + records, + columns, + transpose: false, + defaultColWidth: 200, + defaultRowHeight: 200, + defaultHeaderRowHeight: 50 + }; + +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID),option); +window['tableInstance'] = tableInstance; +``` diff --git a/docs/assets/demo/en/cell-type/list-table-chart.md b/docs/assets/demo/en/cell-type/list-table-chart.md index 15d8a5b3b..15c514a11 100644 --- a/docs/assets/demo/en/cell-type/list-table-chart.md +++ b/docs/assets/demo/en/cell-type/list-table-chart.md @@ -7,7 +7,7 @@ link: '../guide/cell_type/chart' option: ListTable-columns-chart#cellType --- -# Basic table integrated chart +# List table integrated chart Combine vchart chart library and render it into tables to enrich visual display forms and improve multi-chart rendering performance. This example refers to vchart’s bar progress bar. For details, please refer to: https://visactor.io/vchart/demo/progress/linear-progress-with-target-value diff --git a/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation-multiple.md b/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation-multiple.md new file mode 100644 index 000000000..3f9de5b0d --- /dev/null +++ b/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation-multiple.md @@ -0,0 +1,244 @@ +--- +category: examples +group: list-table-data-analysis +title: Set multiple aggregation and aggregation summary methods for the same column of data +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-multiple-aggregation.png +link: '../guide/data_analysis/list_table_dataAnalysis' +option: ListTable-columns-text#aggregation(Aggregation%20%7C%20CustomAggregation%20%7C%20Array) +--- + +# Set multiple aggregation and aggregation summary methods for the same column of data + +Basic table aggregation calculation, each column can set the aggregation method, and supports summation, average, maximum and minimum, and custom function summary logic. The same column and multiple aggregation methods are set, and the results are displayed in multiple rows. + +And this sample data supports editing, and the values that need to be aggregated are automatically calculated after editing. + +## Key Configurations + +- `ListTable` +- `columns.aggregation` Configure aggregation calculations + +## Code demo + +```javascript livedemo template=vtable +const input_editor = new VTable_editors.InputEditor({}); +VTable.register.editor('input', input_editor); +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + salary: Math.round(Math.random() * 10000) + })); +}; +var tableInstance; + +const records = generatePersons(300); +const columns = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 1; + }, + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun() { + return '汇总:'; + } + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'salary', + title: 'salary', + width: 100 + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: VTable.TYPES.AggregationType.MAX, + // showOnTop: true, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.MIN, + showOnTop: true, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] + } +]; +const option = { + container: document.getElementById(CONTAINER_ID), + records, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + frozenColCount: 1, + bottomFrozenRowCount: 2, + rightFrozenColCount: 1, + overscrollBehavior: 'none', + autoWrapText: true, + widthMode: 'autoWidth', + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + }, + theme: VTable.themes.DEFAULT.extends({ + bottomFrozenStyle: { + bgColor: '#ECF1F5', + borderLineWidth: [6, 0, 1, 0], + borderColor: ['gray'] + } + }), + editor: 'input', + headerEditor: 'input', + aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: VTable.TYPES.AggregationType.MAX, + formatFun(value) { + return '最大ID:' + Math.round(value) + '号'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return '最小ID:' + Math.round(value) + '号'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: VTable.TYPES.AggregationType.MIN, + formatFun(value) { + return '最低低低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均平均平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ]; + } + return null; + } + // transpose: true + // widthMode: 'adaptive' +}; +tableInstance = new VTable.ListTable(option); +// tableInstance.updateFilterRules([ +// { +// filterKey: 'sex', +// filteredValues: ['boy'] +// } +// ]); +window.tableInstance = tableInstance; +tableInstance.on('change_cell_value', arg => { + console.log(arg); +}); +``` diff --git a/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation.md b/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation.md new file mode 100644 index 000000000..95d39946d --- /dev/null +++ b/docs/assets/demo/en/list-table-data-analysis/list-table-aggregation.md @@ -0,0 +1,236 @@ +--- +category: examples +group: list-table-data-analysis +title: List Table data aggregation summary +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-aggregation.png +link: '../guide/data_analysis/list_table_dataAnalysis' +option: ListTable-columns-text#aggregation(Aggregation%20%7C%20CustomAggregation%20%7C%20Array) +--- + +# List table aggregation summary + +Basic table aggregation calculation, each column can set the aggregation method, and supports summation, average, maximum and minimum, and custom function summary logic. + +## Key Configurations + +- `ListTable` +- `columns.aggregation` Configure aggregation calculations + +## Code demo + +```javascript livedemo template=vtable +var tableInstance; +VTable.register.icon('filter', { + name: 'filter', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); + +VTable.register.icon('filtered', { + name: 'filtered', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); +fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/olympic-winners.json') + .then(res => res.json()) + .then(data => { + const columns = [ + { + field: 'athlete', + title: 'athlete', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun(value) { + return 'Total:'; + } + } + }, + { + field: 'age', + title: 'age', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.AVG, + formatFun(value) { + return Math.round(value) + '(Avg)'; + } + } + }, + { + field: 'country', + title: 'country', + headerIcon: 'filter', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.CUSTOM, + aggregationFun(values, records) { + // 使用 reduce() 方法统计金牌数 + const goldMedalCountByCountry = records.reduce((acc, data) => { + const country = data.country; + const gold = data.gold; + + if (acc[country]) { + acc[country] += gold; + } else { + acc[country] = gold; + } + return acc; + }, {}); + + // 找出金牌数最多的国家 + let maxGoldMedals = 0; + let countryWithMaxGoldMedals = ''; + + for (const country in goldMedalCountByCountry) { + if (goldMedalCountByCountry[country] > maxGoldMedals) { + maxGoldMedals = goldMedalCountByCountry[country]; + countryWithMaxGoldMedals = country; + } + } + return { + country: countryWithMaxGoldMedals, + gold: maxGoldMedals + }; + }, + formatFun(value) { + return `Top country in gold medals: ${value.country},\nwith ${value.gold} gold medals`; + } + } + }, + { field: 'year', title: 'year', headerIcon: 'filter' }, + { field: 'sport', title: 'sport', headerIcon: 'filter' }, + { + field: 'gold', + title: 'gold', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'silver', + title: 'silver', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'bronze', + title: 'bronze', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'total', + title: 'total', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + } + ]; + const option = { + columns, + records: data, + autoWrapText: true, + heightMode: 'autoHeight', + widthMode: 'autoWidth', + bottomFrozenRowCount: 1, + theme: VTable.themes.ARCO.extends({ + bottomFrozenStyle: { + fontFamily: 'PingFang SC', + fontWeight: 500 + } + }) + }; + const t0 = window.performance.now(); + tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + const filterListValues = { + country: ['all', 'China', 'United States', 'Australia'], + year: ['all', '2004', '2008', '2012', '2016', '2020'], + sport: ['all', 'Swimming', 'Cycling', 'Biathlon', 'Short-Track Speed Skating', 'Nordic Combined'] + }; + let filterListSelectedValues = ''; + let lastFilterField; + tableInstance.on('icon_click', args => { + const { col, row, name } = args; + if (name === 'filter') { + const field = tableInstance.getHeaderField(col, row); + if (select && lastFilterField === field) { + removeFilterElement(); + lastFilterField = null; + } else if (!select || lastFilterField !== field) { + const rect = tableInstance.getCellRelativeRect(col, row); + createFilterElement(filterListValues[field], filterListSelectedValues, field, rect); + lastFilterField = field; + } + } + }); + + let filterContainer = tableInstance.getElement(); + let select; + function createFilterElement(values, curValue, field, positonRect) { + // create select tag + select = document.createElement('select'); + select.setAttribute('type', 'text'); + select.style.position = 'absolute'; + select.style.padding = '4px'; + select.style.width = '100%'; + select.style.boxSizing = 'border-box'; + + // create option tags + let opsStr = ''; + values.forEach(item => { + opsStr += + item === curValue + ? `` + : ``; + }); + select.innerHTML = opsStr; + + filterContainer.appendChild(select); + + select.style.top = positonRect.top + positonRect.height + 'px'; + select.style.left = positonRect.left + 'px'; + select.style.width = positonRect.width + 'px'; + select.style.height = positonRect.height + 'px'; + select.addEventListener('change', () => { + filterListSelectedValues = select.value; + tableInstance.updateFilterRules([ + { + filterKey: field, + filteredValues: select.value + } + ]); + removeFilterElement(); + }); + } + function removeFilterElement() { + filterContainer.removeChild(select); + select.removeEventListener('change', () => { + // this.successCallback(); + }); + select = null; + } + }); +``` diff --git a/docs/assets/demo/en/list-table-data-analysis/list-table-data-filter.md b/docs/assets/demo/en/list-table-data-analysis/list-table-data-filter.md new file mode 100644 index 000000000..d66c6424e --- /dev/null +++ b/docs/assets/demo/en/list-table-data-analysis/list-table-data-filter.md @@ -0,0 +1,235 @@ +--- +category: examples +group: list-table-data-analysis +title: List table data filtering +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-filter.gif +link: '../guide/data_analysis/list_table_dataAnalysis' +--- + +# List table data filtering + +The basic table sets filtering through the interface updateFilterRules, supporting value filtering and function filtering. + +## Key Configurations + +- `ListTable` +- `updateFilterRules` sets or updates filtering data rules + +## Code demo + +```javascript livedemo template=vtable +var tableInstance; +VTable.register.icon('filter', { + name: 'filter', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); + +VTable.register.icon('filtered', { + name: 'filtered', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); +fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/olympic-winners.json') + .then(res => res.json()) + .then(data => { + const columns = [ + { + field: 'athlete', + title: 'athlete', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun(value) { + return 'Total:'; + } + } + }, + { + field: 'age', + title: 'age', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.AVG, + formatFun(value) { + return Math.round(value) + '(Avg)'; + } + } + }, + { + field: 'country', + title: 'country', + headerIcon: 'filter', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.CUSTOM, + aggregationFun(values, records) { + // 使用 reduce() 方法统计金牌数 + const goldMedalCountByCountry = records.reduce((acc, data) => { + const country = data.country; + const gold = data.gold; + + if (acc[country]) { + acc[country] += gold; + } else { + acc[country] = gold; + } + return acc; + }, {}); + + // 找出金牌数最多的国家 + let maxGoldMedals = 0; + let countryWithMaxGoldMedals = ''; + + for (const country in goldMedalCountByCountry) { + if (goldMedalCountByCountry[country] > maxGoldMedals) { + maxGoldMedals = goldMedalCountByCountry[country]; + countryWithMaxGoldMedals = country; + } + } + return { + country: countryWithMaxGoldMedals, + gold: maxGoldMedals + }; + }, + formatFun(value) { + return `Top country in gold medals: ${value.country},\nwith ${value.gold} gold medals`; + } + } + }, + { field: 'year', title: 'year', headerIcon: 'filter' }, + { field: 'sport', title: 'sport', headerIcon: 'filter' }, + { + field: 'gold', + title: 'gold', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'silver', + title: 'silver', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'bronze', + title: 'bronze', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'total', + title: 'total', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + } + ]; + const option = { + columns, + records: data, + autoWrapText: true, + heightMode: 'autoHeight', + widthMode: 'autoWidth', + bottomFrozenRowCount: 1, + theme: VTable.themes.ARCO.extends({ + bottomFrozenStyle: { + fontFamily: 'PingFang SC', + fontWeight: 500 + } + }) + }; + const t0 = window.performance.now(); + tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + const filterListValues = { + country: ['all', 'China', 'United States', 'Australia'], + year: ['all', '2004', '2008', '2012', '2016', '2020'], + sport: ['all', 'Swimming', 'Cycling', 'Biathlon', 'Short-Track Speed Skating', 'Nordic Combined'] + }; + let filterListSelectedValues = ''; + let lastFilterField; + tableInstance.on('icon_click', args => { + const { col, row, name } = args; + if (name === 'filter') { + const field = tableInstance.getHeaderField(col, row); + if (select && lastFilterField === field) { + removeFilterElement(); + lastFilterField = null; + } else if (!select || lastFilterField !== field) { + const rect = tableInstance.getCellRelativeRect(col, row); + createFilterElement(filterListValues[field], filterListSelectedValues, field, rect); + lastFilterField = field; + } + } + }); + + let filterContainer = tableInstance.getElement(); + let select; + function createFilterElement(values, curValue, field, positonRect) { + // create select tag + select = document.createElement('select'); + select.setAttribute('type', 'text'); + select.style.position = 'absolute'; + select.style.padding = '4px'; + select.style.width = '100%'; + select.style.boxSizing = 'border-box'; + + // create option tags + let opsStr = ''; + values.forEach(item => { + opsStr += + item === curValue + ? `` + : ``; + }); + select.innerHTML = opsStr; + + filterContainer.appendChild(select); + + select.style.top = positonRect.top + positonRect.height + 'px'; + select.style.left = positonRect.left + 'px'; + select.style.width = positonRect.width + 'px'; + select.style.height = positonRect.height + 'px'; + select.addEventListener('change', () => { + filterListSelectedValues = select.value; + tableInstance.updateFilterRules([ + { + filterKey: field, + filteredValues: select.value + } + ]); + removeFilterElement(); + }); + } + function removeFilterElement() { + filterContainer.removeChild(select); + select.removeEventListener('change', () => { + // this.successCallback(); + }); + select = null; + } + }); +``` diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json index bb449c76e..cc7b0673d 100644 --- a/docs/assets/demo/menu.json +++ b/docs/assets/demo/menu.json @@ -247,17 +247,24 @@ } }, { - "path": "chart", + "path": "list-chart", "title": { - "zh": "透视表图表展示", - "en": "Chart Type In PivotTable" + "zh": "基本表格集成图表", + "en": "Chart Type In ListTable" } }, { "path": "list-table-chart", "title": { - "zh": "基本表格图表展示", - "en": "Chart Type In ListTable" + "zh": "基本表格集成图表 2", + "en": "Chart Type In ListTable 2" + } + }, + { + "path": "chart", + "title": { + "zh": "透视表图表展示", + "en": "Chart Type In PivotTable" } }, { @@ -869,8 +876,8 @@ { "path": "data-analysis", "title": { - "zh": "数据分析", - "en": "Data Analysis" + "zh": "透视表数据分析", + "en": "Pivot Table Data Analysis" }, "children": [ { @@ -931,6 +938,36 @@ } ] }, + { + "path": "list-table-data-analysis", + "title": { + "zh": "基本表数据分析", + "en": "List Table Data Analysis" + }, + "children": [ + { + "path": "list-table-data-filter", + "title": { + "zh": "数据过滤", + "en": "Data Filter" + } + }, + { + "path": "list-table-aggregation", + "title": { + "zh": "数据聚合分析", + "en": "Data Aggregation" + } + }, + { + "path": "list-table-aggregation-multiple", + "title": { + "zh": "多种数据聚合", + "en": "Multiple Data Aggregation" + } + } + ] + }, { "path": "component", "title": { diff --git a/docs/assets/demo/zh/cell-type/list-chart.md b/docs/assets/demo/zh/cell-type/list-chart.md new file mode 100644 index 000000000..d88eeaef2 --- /dev/null +++ b/docs/assets/demo/zh/cell-type/list-chart.md @@ -0,0 +1,542 @@ +--- +category: examples +group: Cell Type +title: 基本表格集成图表 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-chart-multiple.png +link: '../guide/cell_type/chart' +option: ListTable-columns-chart#cellType +--- + +# 图表类型集成到透视表 + +将vchart图表库结合渲染到表格中,丰富可视化展示形式,提升多图表渲染性能。该示例引用了vchart的线柱饼面图,具体可参考:https://visactor.io/vchart/demo/progress/linear-progress-with-target-value + +## 关键配置 + +- `VTable.register.chartModule('vchart', VChart)` 注册绘制图表的图表库 目前支持VChart +- `cellType: 'chart'` 指定类型chart +- `chartModule: 'vchart'` 指定注册的图表库名称 +- `chartSpec: {}|Function` 图表spec +## 代码演示 + +```javascript livedemo template=vtable + VTable.register.chartModule('vchart', VChart); + const columns = [ + { + field: 'id', + title: 'id', + sort: true, + width: 80, + style: { + textAlign: 'left', + bgColor: '#ea9999' + } + }, + { + field: 'areaChart', + title: 'multiple vchart type', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec(args) { + if (args.row % 3 == 2) + return { + type: 'area', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } + }, + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } + }, + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } + } + }, + + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + else if (args.row % 3 == 1) + return { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + }, + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + return { + type: 'pie', + data: { id: 'data1' }, + categoryField: 'y', + valueField: 'x' + } + } + }, + { + field: 'lineChart', + title: 'vchart line', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + }, + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + }, + { + field: 'barChart', + title: 'vchart bar', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'bar', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + bar: { + state: { + hover: { + fill: 'green' + }, + selected: { + fill: 'orange' + }, + hover_reverse: { + fill: '#ccc' + } + } + } + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + } + }, + { + field: 'scatterChart', + title: 'vchart scatter', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'scatter', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type' + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + } + }, + { + field: 'areaChart', + title: 'vchart area', + width: '320', + cellType: 'chart', + chartModule: 'vchart', + chartSpec: { + type: 'common', + series: [ + { + type: 'area', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } + }, + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } + }, + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } + } + } + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + }, + ]; + const records = []; + for (let i = 1; i <= 10; i++) + records.push({ + id: i, + areaChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: '773' }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: 796 + 100 * i }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + lineChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + barChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ], + scatterChart: [ + { x: '0', type: 'A', y: 100 * i }, + { x: '1', type: 'A', y: '707' }, + { x: '2', type: 'A', y: '832' }, + { x: '3', type: 'A', y: '726' }, + { x: '4', type: 'A', y: '756' }, + { x: '5', type: 'A', y: '777' }, + { x: '6', type: 'A', y: '689' }, + { x: '7', type: 'A', y: '795' }, + { x: '8', type: 'A', y: '889' }, + { x: '9', type: 'A', y: '757' }, + { x: '0', type: 'B', y: 500 }, + { x: '1', type: 'B', y: '785' }, + { x: '2', type: 'B', y: '635' }, + { x: '3', type: 'B', y: '813' }, + { x: '4', type: 'B', y: '678' }, + { x: '5', type: 'B', y: '796' }, + { x: '6', type: 'B', y: '652' }, + { x: '7', type: 'B', y: '623' }, + { x: '8', type: 'B', y: '649' }, + { x: '9', type: 'B', y: '630' } + ] + }); + const option = { + records, + columns, + transpose: false, + defaultColWidth: 200, + defaultRowHeight: 200, + defaultHeaderRowHeight: 50 + }; + +const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID),option); +window['tableInstance'] = tableInstance; +``` diff --git a/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation-multiple.md b/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation-multiple.md new file mode 100644 index 000000000..b38121d38 --- /dev/null +++ b/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation-multiple.md @@ -0,0 +1,244 @@ +--- +category: examples +group: list-table-data-analysis +title: 同一列数据设置多种聚合汇总方式 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-multiple-aggregation.png +link: '../guide/data_analysis/list_table_dataAnalysis' +option: ListTable-columns-text#aggregation(Aggregation%20%7C%20CustomAggregation%20%7C%20Array) +--- + +# 同一列数据设置多种聚合汇总方式 + +基本表格聚合计算,每一列可以设置聚合方式,支持求和,平均,最大最小,自定义函数汇总逻辑。同一列和设置多种聚合方式,结果展示在多行。 + +且该示例数据支持编辑,编辑后自动计算需要聚合的值。 + +## 关键配置 + +- `ListTable` +- `columns.aggregation` 配置聚合计算 + +## 代码演示 + +```javascript livedemo template=vtable +const input_editor = new VTable_editors.InputEditor({}); +VTable.register.editor('input', input_editor); +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + salary: Math.round(Math.random() * 10000) + })); +}; +var tableInstance; + +const records = generatePersons(300); +const columns = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 1; + }, + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun() { + return '汇总:'; + } + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'salary', + title: 'salary', + width: 100 + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: VTable.TYPES.AggregationType.MAX, + // showOnTop: true, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.MIN, + showOnTop: true, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] + } +]; +const option = { + container: document.getElementById(CONTAINER_ID), + records, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + frozenColCount: 1, + bottomFrozenRowCount: 2, + rightFrozenColCount: 1, + overscrollBehavior: 'none', + autoWrapText: true, + widthMode: 'autoWidth', + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + }, + theme: VTable.themes.DEFAULT.extends({ + bottomFrozenStyle: { + bgColor: '#ECF1F5', + borderLineWidth: [6, 0, 1, 0], + borderColor: ['gray'] + } + }), + editor: 'input', + headerEditor: 'input', + aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: VTable.TYPES.AggregationType.MAX, + formatFun(value) { + return '最大ID:' + Math.round(value) + '号'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return '最小ID:' + Math.round(value) + '号'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: VTable.TYPES.AggregationType.MIN, + formatFun(value) { + return '最低低低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: VTable.TYPES.AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均平均平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ]; + } + return null; + } + // transpose: true + // widthMode: 'adaptive' +}; +tableInstance = new VTable.ListTable(option); +// tableInstance.updateFilterRules([ +// { +// filterKey: 'sex', +// filteredValues: ['boy'] +// } +// ]); +window.tableInstance = tableInstance; +tableInstance.on('change_cell_value', arg => { + console.log(arg); +}); +``` diff --git a/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation.md b/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation.md new file mode 100644 index 000000000..b83808d9a --- /dev/null +++ b/docs/assets/demo/zh/list-table-data-analysis/list-table-aggregation.md @@ -0,0 +1,236 @@ +--- +category: examples +group: list-table-data-analysis +title: 基本表格数据聚合分析 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-aggregation.png +link: '../guide/data_analysis/list_table_dataAnalysis' +option: ListTable-columns-text#aggregation(Aggregation%20%7C%20CustomAggregation%20%7C%20Array) +--- + +# 数据聚合汇总 + +基本表格聚合计算,每一列可以设置聚合方式,支持求和,平均,最大最小,自定义函数汇总逻辑。 + +## 关键配置 + +- `ListTable` +- `columns.aggregation` 配置聚合计算 + +## 代码演示 + +```javascript livedemo template=vtable +var tableInstance; +VTable.register.icon('filter', { + name: 'filter', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); + +VTable.register.icon('filtered', { + name: 'filtered', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); +fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/olympic-winners.json') + .then(res => res.json()) + .then(data => { + const columns = [ + { + field: 'athlete', + title: 'athlete', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun(value) { + return 'Total:'; + } + } + }, + { + field: 'age', + title: 'age', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.AVG, + formatFun(value) { + return Math.round(value) + '(Avg)'; + } + } + }, + { + field: 'country', + title: 'country', + headerIcon: 'filter', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.CUSTOM, + aggregationFun(values, records) { + // 使用 reduce() 方法统计金牌数 + const goldMedalCountByCountry = records.reduce((acc, data) => { + const country = data.country; + const gold = data.gold; + + if (acc[country]) { + acc[country] += gold; + } else { + acc[country] = gold; + } + return acc; + }, {}); + + // 找出金牌数最多的国家 + let maxGoldMedals = 0; + let countryWithMaxGoldMedals = ''; + + for (const country in goldMedalCountByCountry) { + if (goldMedalCountByCountry[country] > maxGoldMedals) { + maxGoldMedals = goldMedalCountByCountry[country]; + countryWithMaxGoldMedals = country; + } + } + return { + country: countryWithMaxGoldMedals, + gold: maxGoldMedals + }; + }, + formatFun(value) { + return `Top country in gold medals: ${value.country},\nwith ${value.gold} gold medals`; + } + } + }, + { field: 'year', title: 'year', headerIcon: 'filter' }, + { field: 'sport', title: 'sport', headerIcon: 'filter' }, + { + field: 'gold', + title: 'gold', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'silver', + title: 'silver', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'bronze', + title: 'bronze', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'total', + title: 'total', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + } + ]; + const option = { + columns, + records: data, + autoWrapText: true, + heightMode: 'autoHeight', + widthMode: 'autoWidth', + bottomFrozenRowCount: 1, + theme: VTable.themes.ARCO.extends({ + bottomFrozenStyle: { + fontFamily: 'PingFang SC', + fontWeight: 500 + } + }) + }; + const t0 = window.performance.now(); + tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + const filterListValues = { + country: ['all', 'China', 'United States', 'Australia'], + year: ['all', '2004', '2008', '2012', '2016', '2020'], + sport: ['all', 'Swimming', 'Cycling', 'Biathlon', 'Short-Track Speed Skating', 'Nordic Combined'] + }; + let filterListSelectedValues = ''; + let lastFilterField; + tableInstance.on('icon_click', args => { + const { col, row, name } = args; + if (name === 'filter') { + const field = tableInstance.getHeaderField(col, row); + if (select && lastFilterField === field) { + removeFilterElement(); + lastFilterField = null; + } else if (!select || lastFilterField !== field) { + const rect = tableInstance.getCellRelativeRect(col, row); + createFilterElement(filterListValues[field], filterListSelectedValues, field, rect); + lastFilterField = field; + } + } + }); + + let filterContainer = tableInstance.getElement(); + let select; + function createFilterElement(values, curValue, field, positonRect) { + // create select tag + select = document.createElement('select'); + select.setAttribute('type', 'text'); + select.style.position = 'absolute'; + select.style.padding = '4px'; + select.style.width = '100%'; + select.style.boxSizing = 'border-box'; + + // create option tags + let opsStr = ''; + values.forEach(item => { + opsStr += + item === curValue + ? `` + : ``; + }); + select.innerHTML = opsStr; + + filterContainer.appendChild(select); + + select.style.top = positonRect.top + positonRect.height + 'px'; + select.style.left = positonRect.left + 'px'; + select.style.width = positonRect.width + 'px'; + select.style.height = positonRect.height + 'px'; + select.addEventListener('change', () => { + filterListSelectedValues = select.value; + tableInstance.updateFilterRules([ + { + filterKey: field, + filteredValues: select.value + } + ]); + removeFilterElement(); + }); + } + function removeFilterElement() { + filterContainer.removeChild(select); + select.removeEventListener('change', () => { + // this.successCallback(); + }); + select = null; + } + }); +``` diff --git a/docs/assets/demo/zh/list-table-data-analysis/list-table-data-filter.md b/docs/assets/demo/zh/list-table-data-analysis/list-table-data-filter.md new file mode 100644 index 000000000..97f48f9f7 --- /dev/null +++ b/docs/assets/demo/zh/list-table-data-analysis/list-table-data-filter.md @@ -0,0 +1,235 @@ +--- +category: examples +group: list-table-data-analysis +title: 基本表格数据过滤 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/preview/list-table-filter.gif +link: '../guide/data_analysis/list_table_dataAnalysis' +--- + +# 基本表格数据过滤 + +基本表格通过接口 updateFilterRules 来设置过滤,支持值过滤和函数过滤。 + +## 关键配置 + +- `ListTable` +- `updateFilterRules` 设置或者更新过滤数据规则 + +## 代码演示 + +```javascript livedemo template=vtable +var tableInstance; +VTable.register.icon('filter', { + name: 'filter', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); + +VTable.register.icon('filtered', { + name: 'filtered', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' +}); +fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/olympic-winners.json') + .then(res => res.json()) + .then(data => { + const columns = [ + { + field: 'athlete', + title: 'athlete', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun(value) { + return 'Total:'; + } + } + }, + { + field: 'age', + title: 'age', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.AVG, + formatFun(value) { + return Math.round(value) + '(Avg)'; + } + } + }, + { + field: 'country', + title: 'country', + headerIcon: 'filter', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.CUSTOM, + aggregationFun(values, records) { + // 使用 reduce() 方法统计金牌数 + const goldMedalCountByCountry = records.reduce((acc, data) => { + const country = data.country; + const gold = data.gold; + + if (acc[country]) { + acc[country] += gold; + } else { + acc[country] = gold; + } + return acc; + }, {}); + + // 找出金牌数最多的国家 + let maxGoldMedals = 0; + let countryWithMaxGoldMedals = ''; + + for (const country in goldMedalCountByCountry) { + if (goldMedalCountByCountry[country] > maxGoldMedals) { + maxGoldMedals = goldMedalCountByCountry[country]; + countryWithMaxGoldMedals = country; + } + } + return { + country: countryWithMaxGoldMedals, + gold: maxGoldMedals + }; + }, + formatFun(value) { + return `Top country in gold medals: ${value.country},\nwith ${value.gold} gold medals`; + } + } + }, + { field: 'year', title: 'year', headerIcon: 'filter' }, + { field: 'sport', title: 'sport', headerIcon: 'filter' }, + { + field: 'gold', + title: 'gold', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'silver', + title: 'silver', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'bronze', + title: 'bronze', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'total', + title: 'total', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + } + ]; + const option = { + columns, + records: data, + autoWrapText: true, + heightMode: 'autoHeight', + widthMode: 'autoWidth', + bottomFrozenRowCount: 1, + theme: VTable.themes.ARCO.extends({ + bottomFrozenStyle: { + fontFamily: 'PingFang SC', + fontWeight: 500 + } + }) + }; + const t0 = window.performance.now(); + tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + const filterListValues = { + country: ['all', 'China', 'United States', 'Australia'], + year: ['all', '2004', '2008', '2012', '2016', '2020'], + sport: ['all', 'Swimming', 'Cycling', 'Biathlon', 'Short-Track Speed Skating', 'Nordic Combined'] + }; + let filterListSelectedValues = ''; + let lastFilterField; + tableInstance.on('icon_click', args => { + const { col, row, name } = args; + if (name === 'filter') { + const field = tableInstance.getHeaderField(col, row); + if (select && lastFilterField === field) { + removeFilterElement(); + lastFilterField = null; + } else if (!select || lastFilterField !== field) { + const rect = tableInstance.getCellRelativeRect(col, row); + createFilterElement(filterListValues[field], filterListSelectedValues, field, rect); + lastFilterField = field; + } + } + }); + + let filterContainer = tableInstance.getElement(); + let select; + function createFilterElement(values, curValue, field, positonRect) { + // create select tag + select = document.createElement('select'); + select.setAttribute('type', 'text'); + select.style.position = 'absolute'; + select.style.padding = '4px'; + select.style.width = '100%'; + select.style.boxSizing = 'border-box'; + + // create option tags + let opsStr = ''; + values.forEach(item => { + opsStr += + item === curValue + ? `` + : ``; + }); + select.innerHTML = opsStr; + + filterContainer.appendChild(select); + + select.style.top = positonRect.top + positonRect.height + 'px'; + select.style.left = positonRect.left + 'px'; + select.style.width = positonRect.width + 'px'; + select.style.height = positonRect.height + 'px'; + select.addEventListener('change', () => { + filterListSelectedValues = select.value; + tableInstance.updateFilterRules([ + { + filterKey: field, + filteredValues: select.value + } + ]); + removeFilterElement(); + }); + } + function removeFilterElement() { + filterContainer.removeChild(select); + select.removeEventListener('change', () => { + // this.successCallback(); + }); + select = null; + } + }); +``` diff --git a/docs/assets/faq/en/21-How to cancel the bubbling of the table event.md b/docs/assets/faq/en/21-How to cancel the bubbling of the table event.md new file mode 100644 index 000000000..f11da3e46 --- /dev/null +++ b/docs/assets/faq/en/21-How to cancel the bubbling of the table event.md @@ -0,0 +1,44 @@ +# How to cancel the bubbling of the table mousedown event + +## Problem Description + +In my business scenario, I need to drag the entire table to move the position. However, if the mouse point is dragged on the cell, it will trigger the box selection interaction of the table. In this way, I do not expect to drag the entire table. When the mouse point is clicked, Then respond to the entire table dragging behavior in the blank area of the table. + +Based on this demand background, how to determine whether the click is on a cell or a blank area of the table? + +## solution + +This problem can be handled in VTable by listening to the `mousedown_cell` event, but it should be noted that VTable internally listens to pointer events! + +Therefore, if you cancel bubbling directly, you can only cancel the pointerdown event. +``` + tableInstance.on('mousedown_cell', arg => { + arg.event.stopPropagation(); + }); +``` +Therefore, you need to listen to mousedown again to determine the organization event. For correct processing, you can see the following example: + +## Code Example + +```javascript + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + let isPointerDownOnTable = false; + tableInstance.on('mousedown_cell', arg => { + isPointerDownOnTable = true; + setTimeout(() => { + isPointerDownOnTable = false; + }, 0); + arg.event?.stopPropagation(); + }); + tableInstance.getElement().addEventListener('mousedown', e => { + if (isPointerDownOnTable) { + e.stopPropagation(); + } + }); +``` + +## Related documents + +- [Tutorial](https://visactor.io/vtable/guide/Event/event_list) +- [github](https://github.com/VisActor/VTable) \ No newline at end of file diff --git a/docs/assets/faq/menu.json b/docs/assets/faq/menu.json index 0bcc40396..540535d70 100644 --- a/docs/assets/faq/menu.json +++ b/docs/assets/faq/menu.json @@ -140,6 +140,13 @@ "zh": "20.如何在Vue中使用Vtable?", "en": "20.How to use VTable in Vue?" } + }, + { + "path": "21-How to cancel the bubbling of the table event", + "title": { + "zh": "21.怎么取消表格mousedown事件的冒泡?", + "en": "21.How to cancel the bubbling of the table event?" + } } ] } \ No newline at end of file diff --git a/docs/assets/faq/zh/21-How to cancel the bubbling of the table event.md b/docs/assets/faq/zh/21-How to cancel the bubbling of the table event.md new file mode 100644 index 000000000..6e51976e0 --- /dev/null +++ b/docs/assets/faq/zh/21-How to cancel the bubbling of the table event.md @@ -0,0 +1,44 @@ +# 怎么取消表格mousedown事件的冒泡 + +## 问题描述 + +在我的业务场景中需要对表格整体进行拖拽来移动位置,但是如果鼠标点在单元格上拖拽会触发表格的框选交互,这样我就预期不进行整表拖拽了,当鼠标点在表格空白区域再响应整表拖拽行为。 + +基于这个需求背景,怎么判断是点击在了单元格上还是表格空白区域呢? + +## 解决方案 + +在 VTable 中可以通过监听`mousedown_cell`事件来处理这个问题,不过需要注意的是VTable内部都是监听的pointer事件哦! + +所以如果直接取消冒泡,也仅能取消pointerdown事件。 +``` + tableInstance.on('mousedown_cell', arg => { + arg.event.stopPropagation(); + }); +``` +所以需要再监听mousedown来判断组织事件,正确处理可以看下面示例: + +## 代码示例 + +```javascript + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + let isPointerDownOnTable = false; + tableInstance.on('mousedown_cell', arg => { + isPointerDownOnTable = true; + setTimeout(() => { + isPointerDownOnTable = false; + }, 0); + arg.event?.stopPropagation(); + }); + tableInstance.getElement().addEventListener('mousedown', e => { + if (isPointerDownOnTable) { + e.stopPropagation(); + } + }); +``` + +## 相关文档 + +- [教程](https://visactor.io/vtable/guide/Event/event_list) +- [github](https://github.com/VisActor/VTable) diff --git a/docs/assets/guide/en/cell_type/chart.md b/docs/assets/guide/en/cell_type/chart.md index 578c199a6..f2536ee46 100644 --- a/docs/assets/guide/en/cell_type/chart.md +++ b/docs/assets/guide/en/cell_type/chart.md @@ -17,7 +17,7 @@ Table display type`cellType`Set to`chart`Used to generate charts. * cellType: 'chart'//chart chart type * chartModule: 'vchart'//vchart is the name configured during registration -* Chart Spec :{ } // chart configuration item +* Chart Spec :{ } // chart configuration item, support funciton define Where the chartSpec configuration item corresponds[VChart configuration](https://visactor.io/vchart/option) diff --git a/docs/assets/guide/en/data_analysis/list_table_dataAnalysis.md b/docs/assets/guide/en/data_analysis/list_table_dataAnalysis.md new file mode 100644 index 000000000..2c26a0250 --- /dev/null +++ b/docs/assets/guide/en/data_analysis/list_table_dataAnalysis.md @@ -0,0 +1,141 @@ +# Basic table data analysis + +Currently supported capabilities include sorting, filtering, and data aggregation calculations. + +# Data sorting + +For details, please refer to the tutorial: https://visactor.io/vtable/guide/basic_function/sort + +# Data filtering + +The basic table component sets data filtering rules through the interface `updateFilterRules`, supporting value filtering and function filtering. Here is a usage example of filtering data: + +```javascript +tableInstance.updateFilterRules([ + { + filterKey: 'sex', + filteredValues: ['boy'] + }, + { + filterFunc: (record: Record) => { + return record.age > 30; + } + } +]); +``` + +In the above example, we set up value filtering through `filterKey` and `filteredValues` to only display data with a gender of "boy"; at the same time, we used function filtering to customize the filtering logic through `filterFunc` and only displayed `age The `field is the data whose age is greater than 30. + +Specific example: https://visactor.io/vtable/demo/list-table-data-analysis/list-table-data-filter + +# Data aggregation + +The basic table supports aggregation calculation of data, and different aggregation methods can be set for each column, including sum, average, maximum value, minimum value, and custom function summary logic. Multiple aggregation methods can be set for the same column, and the aggregation results will be displayed in multiple rows. + +## Aggregation calculation type + +- To sum, set `aggregationType` to `AggregationType.SUM` +- Average, set `aggregationType` to `AggregationType.AVG` +- Maximum value, set `aggregationType` to `AggregationType.MAX` +- Minimum value, set `aggregationType` to `AggregationType.MIN` +- Count, set `aggregationType` to `AggregationType.COUNT` +- Custom function, set `aggregationType` to `AggregationType.CUSTOM`, and set custom aggregation logic through `aggregationFun` + +## Aggregate value formatting function + +Use `formatFun` to set the formatting function of the aggregate value, and you can customize the display format of the aggregate value. + +## Aggregated result placement + +Use `showOnTop` to control the display position of the aggregation results. The default is `false`, that is, the aggregation results are displayed at the bottom of the body. If set to `true`, the aggregation results are displayed at the top of the body. + +Note: Currently, the aggregate value does not have the ability to customize freezing. It needs to be combined with bottomFrozonRowCount to achieve fixed display. In addition, the embarrassing thing is that topFrozonRowCount has not been added yet, so it is recommended to display the aggregation result at the bottom of the body first. Comprehensive freezing capabilities will be supported in the future. + +## Aggregation configuration + +Aggregation configuration can be set in the `columns` column definition or configured in the table global `option`. + +### Configure aggregation method in column definition + +In the column definition, the aggregation method can be configured through the `aggregation` attribute. Here is an example of an aggregation configuration: + +```javascript +columns: { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return 'Maximum salary:' + Math.round(value) + 'yuan'; + } + }, + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return 'Minimum salary:' + Math.round(value) + 'yuan'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return 'Average:' + Math.round(value) + 'Yuan (total' + table.recordsCount + 'data)'; + } + } + ] +} +``` + +In the above example, we set three aggregation methods for the `salary` column: maximum value, minimum value and average value. Use `aggregationType` to specify the aggregation method, and then use `formatFun` to customize the display format of the aggregation results, and use `showOnTop` to control whether the aggregation results are displayed at the top or bottom of the body. + +### Table global configuration aggregation method + +In addition to configuring the aggregation method in the column definition, you can also set it in the table global configuration. Here is an example of global configuration: + +```javascript +aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return 'Maximum ID:' + Math.round(value) + 'number'; + } + }, + { + aggregationType: AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return 'Minimum ID:' + Math.round(value) + 'number'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return 'Minimum salary:' + Math.round(value) + 'yuan'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return 'Average salary:' + Math.round(value) + 'Yuan (total' + table.recordsCount + 'data)'; + } + } + ]; + } + return null; +} +``` + +In the above example, we set the aggregation method through the `aggregation` function of global configuration, and return different aggregation configurations according to different conditions. For example, when `args.col === 1`, we set the aggregation method of the maximum and minimum values; when `args.field === 'salary'`, we set the aggregation of the minimum and average values Way. + +For specific examples, please refer to: https://visactor.io/vtable/demo/list-table-data-analysis/list-table-aggregation-multiple + +The above is the tutorial document for basic table data analysis capabilities, covering the configuration and usage of data filtering and data aggregation. Hope this document can be helpful to you! If you have any questions, please feel free to ask. diff --git a/docs/assets/guide/en/data_analysis/pivot_table_dataAnalysis.md b/docs/assets/guide/en/data_analysis/pivot_table_dataAnalysis.md new file mode 100644 index 000000000..87e6d3bc5 --- /dev/null +++ b/docs/assets/guide/en/data_analysis/pivot_table_dataAnalysis.md @@ -0,0 +1,377 @@ +# Pivot data analysis + +In the figure below, there are four business dimensions: region, province, year, quarter, and indicators: sales, profit. + +
+ +

Pivot table structure description

+
+Regarding the sales data in the figure, the location is in cell [5, 5], that is, the data in column 5 and row 5: represents the sales profit value of Heilongjiang Province in the Northeast region in the Q2 quarter of 2016. That is to say, it corresponds to the row dimension value: ['Northeast', 'Heilongjiang'], the column dimension: ['2016', '2016-Q2'], and the indicator: 'Profit'. Next, we will introduce how to use VTable to implement this multi-dimensional table. + +# VTable implements multi-dimensional tables + +## Concept mapping to configuration items + +The configuration of the pivot table above is as follows: + +``` +const option={ + rows:['region','province'], //row dimensions + columns:['year','quarter'], //column dimensions + indicators:['sales','profit'], //Indicators + enableDataAnalysis: true, //Whether to enable data analysis function + records:[ //Data source。 If summary data is passed in, use user incoming data + { + region:'东北', + province:'黑龙江', + year:'2016', + quarter:'2016-Q1', + sales:1243, + profit:546 + }, + ... + ] +} +``` + +This configuration is the simplest configuration for multidimensional tables. As the functional requirements become more complex, various configurations can be added for each function point to meet the needs. + +## Data analysis related configuration: + +| Configuration item | Type | Description | +| :--------------------------- | :----------------------------- | :---------------------------------------------------------------------------------- | +| rows | (IRowDimension \| string)[] | Row dimension field array, used to parse out the corresponding dimension members | +| columns | (IColumnDimension \| string)[] | Column dimension field array, used to parse out the corresponding dimension members | +| indicators | (IIndicator \| string)[] | Specific display indicators | +| dataConfig.aggregationRules | aggregationRule[] | Aggregation value calculation rules according to row and column dimensions | +| dataConfig.derivedFieldRules | DerivedFieldRule[] | Derived fields | +| dataConfig.sortRules | sortRule[] | Sort rules | +| dataConfig.filterRules | filterRule[] | Filter Rules | +| dataConfig.totals | totalRule[] | Subtotal or total | + +dataConfig configuration definition: + +``` +/** + * Data processing configuration + */ +export interface IDataConfig { + aggregationRules?: AggregationRules; //按照行列维度聚合值计算规则; + sortRules?: SortRules; //排序规则; + filterRules?: FilterRules; //过滤规则; + totals?: Totals; //小计或总计; + derivedFieldRules?: DerivedFieldRules; //派生字段定义 + ... +} +``` + +dataConfig application example: + +### 1. Totals + +[option description](../../../option/PivotTable#dataConfig.totals) +Configuration example: + +``` +dataConfig: { + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + grandTotalLabel: 'row total', + subTotalLabel: 'Subtotal', + showGrandTotalsOnTop: true //totals show on top + }, + column: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['quarter'], + grandTotalLabel: 'column total', + subTotalLabel: 'Subtotal' + } + } + }, +``` + +Online demo:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-total + +### 2. Sorting rules + +[option description](../../../option/PivotTable#dataConfig.sortRules) +Configuration example: + +``` + sortRules: [ + { + sortField: 'city', + sortByIndicator: 'sales', + sortType: VTable.TYPES.SortType.DESC, + query: ['office supplies', 'pen'] + } as VTable.TYPES.SortByIndicatorRule + ] + +``` + +If you need to modify the sorting rules of the pivot table, you can use the interface `updateSortRules`. + +Online demo:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-sort-dimension + +### 3. Filter rules + +[option description](../../../option/PivotTable#dataConfig.filterRules) +Configuration example: + +``` +filterRules: [ + { + filterFunc: (record: Record) => { + return record.province !== 'Sichuan Province' || record.category !== 'Furniture'; + } + } + ] +``` + +Online demo:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-filter + +### 4. Aggregation method + +[option description](../../../option/PivotTable#dataConfig.aggregationRules) +Configuration example: + +``` + aggregationRules: [ + //The basis for doing aggregate calculations, such as sales. If there is no configuration, the cell content will be displayed by default based on the aggregate sum calculation result. + { + indicatorKey: 'TotalSales', //Indicator name + field: 'Sales', //Indicator based on field + aggregationType: VTable.TYPES.AggregationType.SUM, //Calculation type + formatFun: sumNumberFormat + }, + { + indicatorKey: 'OrderCount', //Indicator name + field: 'Sales', //Indicator based on field + aggregationType: VTable.TYPES.AggregationType.COUNT, //Computation type + formatFun: countNumberFormat + }, + { + indicatorKey: 'AverageOrderSales', //Indicator name + field: 'Sales', //Indicator based on field + aggregationType: VTable.TYPES.AggregationType.AVG, //Computation type + }, + { + indicatorKey: 'MaxOrderSales', //Indicator name + field: 'Sales', //Indicator based on field + aggregationType: VTable.TYPES.AggregationType.MAX, //Computation type , caculate max value + }, + { + indicatorKey: 'OrderSalesValue', //Indicator name + field: 'Sales', //Indicator based on field + aggregationType: VTable.TYPES.AggregationType.NONE, //don't aggregate + } + ] +``` + +Online demo:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-aggregation + +### 5. Derive Field + +[option description](../../../option/PivotTable#dataConfig.derivedFieldRules) +Configuration example: + +``` + derivedFieldRules: [ + { + fieldName: 'Year', + derivedFunc: VTable.DataStatistics.dateFormat('Order Date', '%y', true), + }, + { + fieldName: 'Month', + derivedFunc: VTable.DataStatistics.dateFormat('Order Date', '%n', true), + } + ] +``` + +Online demo:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-derivedField + +## Data analysis process + +Dependent configuration: dimensions, indicators and dataConfig. + +### The process of traversing data: + +Traverse the records once, parse the row header dimension value to display the header cell, distribute all data in the records to the corresponding row and column path set, and calculate the aggregate value of the body part indicator cell. + +
+ +

Data analysis process

+
+ +### Data dimension tree + +According to the above traversed structure, a dimension tree will be generated, from which the value of the cell and the original data entry of the value can be found. + +
+ +

Organize dimension tree to aggregate data

+
+ After analysis and calculation of record grouping and aggregation, the corresponding relationship between the cell data in the table and the records data source is finally presented: +
+ +

Correspondence between data source entries and cells

+
+ +### Custom dimension tree + +Although multi-dimensional tables with analytical capabilities can automatically analyze the dimension values of each dimension to form a tree structure of row and column headers, and can be sorted according to `dataConfig.sortRules`, scenarios with complex business logic still expect to be able to **customize Row column header dimension value ** and order. Then these business requirement scenarios can be realized through rowTree and columnTree. + +- enableDataAnalysis needs to be set to false to turn off the analysis of aggregated data within VTable. + +
+ +

custom rowTree columnTree

+
+ +Custom tree configuration: + +``` +const option = { + rowTree: [{ + dimensionKey: 'region', + value: '中南', + children: [ + { + dimensionKey: 'province', + value: '广东', + }, + { + dimensionKey: 'province', + value: '广西', + } + ] + }, + { + dimensionKey: 'region', + value: '华东', + children: [ + { + dimensionKey: 'province', + value: '上海', + }, + { + dimensionKey: 'province', + value: '山东', + } + ] + }], + columnTree: [{ + dimensionKey: 'year', + value: '2016', + children: [ + { + dimensionKey: 'quarter', + value: '2016-Q1', + children: [ + { + indicatorKey: 'sales', + value: 'sales' + }, + { + indicatorKey: 'profit', + value: 'profit' + } + ] + }, + { + dimensionKey: 'quarter', + value: '2016-Q2', + children: [ + { + indicatorKey: 'sales', + value: 'sales' + }, + { + indicatorKey: 'profit', + value: 'profit' + } + ] + } + ] + }], + indicators: ['sales', 'profit'], + //enableDataAnalysis:true, + corner: { + titleOnDimension: 'none' + }, + records: [ + { + region: '中南', + province: '广东', + year: '2016', + quarter: '2016-Q1', + sales: 1243, + profit: 546 + }, + { + region: '中南', + province: '广东', + year: '2016', + quarter: '2016-Q2', + sales: 2243, + profit: 169 + }, { + region: '中南', + province: '广西', + year: '2016', + quarter: '2016-Q1', + sales: 3043, + profit: 1546 + }, + { + region: '中南', + province: '广西', + year: '2016', + quarter: '2016-Q2', + sales: 1463, + profit: 609 + }, + { + region: '华东', + province: '上海', + year: '2016', + quarter: '2016-Q1', + sales: 4003, + profit: 1045 + }, + { + region: '华东', + province: '上海', + year: '2016', + quarter: '2016-Q2', + sales: 5243, + profit: 3169 + }, { + region: '华东', + province: '山东', + year: '2016', + quarter: '2016-Q1', + sales: 4543, + profit: 3456 + }, + { + region: '华东', + province: '山东', + year: '2016', + quarter: '2016-Q2', + sales: 6563, + profit: 3409 + } + ] +}; +``` + +VTable official website example: https://visactor.io/vtable/demo/table-type/pivot-table. + +The complexity of the custom tree lies in the formation of the row, column and dimension trees. You can choose to use it according to the business scenario. If you have complex sorting, aggregation or paging rules, you can choose to use a custom method. + +**Note: If you choose the custom tree configuration method, the data aggregation capability inside the VTable will not be enabled, that is, one of the matched data entries will be used as the cell indicator value. ** diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index b323ed93e..dc482abde 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -410,6 +410,29 @@ } ] }, + { + "path": "data_analysis", + "title": { + "zh": "数据分析", + "en": "data analysis" + }, + "children": [ + { + "path": "pivot_table_dataAnalysis", + "title": { + "zh": "透视表数据分析", + "en": "pivot table data analysis" + } + }, + { + "path": "list_table_dataAnalysis", + "title": { + "zh": "基本表数据分析", + "en": "list table data analysis" + } + } + ] + }, { "path": "components", "title": { diff --git a/docs/assets/guide/zh/cell_type/chart.md b/docs/assets/guide/zh/cell_type/chart.md index cb5bb4284..e4334dc48 100644 --- a/docs/assets/guide/zh/cell_type/chart.md +++ b/docs/assets/guide/zh/cell_type/chart.md @@ -16,7 +16,7 @@ VTable.register.chartModule('vchart', VChart); 表格展示类型`cellType`设置成`chart`用于生成图表。 - cellType: 'chart' //chart图表类型 - chartModule: 'vchart' // vchart是注册时配置的名称 -- chartSpec:{ } //chart配置项 +- chartSpec:{ } //chart配置项 支持函数形式返回spec 其中chartSpec配置项对应[VChart配置](https://visactor.io/vchart/option) diff --git a/docs/assets/guide/zh/data_analysis/list_table_dataAnalysis.md b/docs/assets/guide/zh/data_analysis/list_table_dataAnalysis.md new file mode 100644 index 000000000..c1dd3f9aa --- /dev/null +++ b/docs/assets/guide/zh/data_analysis/list_table_dataAnalysis.md @@ -0,0 +1,141 @@ +# 基础表格数据分析 + +目前支持的能力有排序,过滤,数据聚合计算。 + +# 数据排序 + +具体可参阅教程:https://visactor.io/vtable/guide/basic_function/sort + +# 数据过滤 + +基础表格组件通过接口`updateFilterRules`来设置数据过滤规则,支持值过滤和函数过滤。下面是过滤数据的用法示例: + +```javascript +tableInstance.updateFilterRules([ + { + filterKey: 'sex', + filteredValues: ['boy'] + }, + { + filterFunc: (record: Record) => { + return record.age > 30; + } + } +]); +``` + +在上述示例中,我们通过`filterKey`和`filteredValues`来设置值过滤,只显示性别为"boy"的数据;同时,我们使用了函数过滤,通过`filterFunc`来自定义过滤逻辑,只显示`age`字段即年龄大于 30 的数据。 + +具体示例:https://visactor.io/vtable/demo/list-table-data-analysis/list-table-data-filter + +# 数据聚合 + +基础表格支持对数据进行聚合计算,每一列可以设置不同的聚合方式,包括求和、平均、最大值、最小值,以及自定义函数汇总逻辑。同一列可以设置多种聚合方式,聚合结果会展示在多行。 + +## 聚合计算类型 + +- 求和,设置`aggregationType`为`AggregationType.SUM` +- 平均,设置`aggregationType`为`AggregationType.AVG` +- 最大值,设置`aggregationType`为`AggregationType.MAX` +- 最小值,设置`aggregationType`为`AggregationType.MIN` +- 计数,设置`aggregationType`为`AggregationType.COUNT` +- 自定义函数,设置`aggregationType`为`AggregationType.CUSTOM`,通过`aggregationFun`来设置自定义的聚合逻辑 + +## 聚合值格式化函数 + +通过`formatFun`来设置聚合值的格式化函数,可以自定义聚合值的展示格式。 + +## 聚合结果展示位置 + +通过`showOnTop`来控制聚合结果的展示位置,默认为`false`,即聚合结果展示在 body 的底部。如果设置为`true`,则聚合结果展示在 body 的顶部。 + +注意:目前聚合值没有自定冻结能力,需要结合 bottomFrozonRowCount 来实现固定显示,另外尴尬的是目前还没有增加 topFrozonRowCount,所以建议可以先将聚合结果显示在 body 底部。后续会支持全面的冻结能力。 + +## 聚合配置 + +聚合配置可以在 `columns` 列定义中进行设置,也可以在表格全局 `option` 中配置中。 + +### 列定义中配置聚合方式 + +在列定义中,可以通过`aggregation`属性来配置聚合方式。下面是一个聚合配置的示例: + +```javascript +columns: { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] +} +``` + +在上述示例中,我们针对`salary`这一列设置了三种聚合方式:最大值、最小值和平均值。通过`aggregationType`来指定聚合方式,然后可以通过`formatFun`来自定义聚合结果的展示格式,通过`showOnTop`来控制将聚合结果展示在 body 的顶部还是底部。 + +### 表格全局配置聚合方式 + +除了在列定义中配置聚合方式,也可以在表格全局配置中进行设置。下面是一个全局配置的示例: + +```javascript +aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最大ID:' + Math.round(value) + '号'; + } + }, + { + aggregationType: AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return '最小ID:' + Math.round(value) + '号'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均薪资:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ]; + } + return null; +} +``` + +在上述示例中,我们通过全局配置的`aggregation`函数来设置聚合方式,根据不同的条件返回不同的聚合配置。例如,当`args.col === 1`时,我们设置了最大值和最小值的聚合方式;当`args.field === 'salary'`时,我们设置了最低值和平均值的聚合方式。 + +具体示例可参考:https://visactor.io/vtable/demo/list-table-data-analysis/list-table-aggregation-multiple + +以上就是基础表格数据分析能力的教程文档,涵盖了数据过滤和数据聚合的配置和用法。希望这份文档能对你有所帮助!如有任何疑问,请随时提问。 diff --git a/docs/assets/guide/zh/data_analysis/pivot_table_dataAnalysis.md b/docs/assets/guide/zh/data_analysis/pivot_table_dataAnalysis.md new file mode 100644 index 000000000..f2955e934 --- /dev/null +++ b/docs/assets/guide/zh/data_analysis/pivot_table_dataAnalysis.md @@ -0,0 +1,339 @@ +# 透视数据分析 + +下图中一共有四个业务维度:地区、省份、年份、季度,看数指标:销售额,利润。 +
+ +

透视表结构说明

+
+针对图中销售数据,位置在单元格[5, 5],即列5行5的数据:代表了2016年Q2季度下东北地区黑龙江省的销售利润值。也就是对应到行维度值:['东北', '黑龙江'],列维度:['2016', '2016-Q2'],指标:'利润'。接下来将介绍如何用VTable实现这种多维表格。 + +# VTable实现多维表格 +## 概念映射到配置项 +上图透视表的配置如下: +``` +const option={ + rows:['region','province'], //行维度 + columns:['year','quarter'], //列维度 + indicators:['sales','profit'], //指标 + enableDataAnalysis: true, //是否开启数据分析功能 + records:[ //数据源 如果传入了汇总数据则使用用户传入数据 + { + region:'东北', + province:'黑龙江', + year:'2016', + quarter:'2016-Q1', + sales:1243, + profit:546 + }, + ... + ] +} +``` + +该配置是多维表格最简配置。随着对功能要求的复杂性可以针对各功能点来添加各项配置来满足需求。 +## 数据分析相关配置: +|配置项|类型|描述| +|:----|:----|:----| +|rows|(IRowDimension \| string)[]|行维度字段数组,用于解析出对应的维度成员| +|columns|(IColumnDimension \| string)[]|列维度字段数组,用于解析出对应的维度成员| +|indicators|(IIndicator \| string)[]|具体展示指标| +|dataConfig.aggregationRules|aggregationRule[]|按照行列维度聚合值计算规则| +|dataConfig.derivedFieldRules|DerivedFieldRule[]|派生字段| +|dataConfig.sortRules|sortRule[]|排序规则| +|dataConfig.filterRules|filterRule[]|过滤规则| +|dataConfig.totals|totalRule[]|小计或总计| + +dataConfig配置定义: +``` +/** + * 数据处理配置 + */ +export interface IDataConfig { + aggregationRules?: AggregationRules; //按照行列维度聚合值计算规则; + sortRules?: SortRules; //排序规则; + filterRules?: FilterRules; //过滤规则; + totals?: Totals; //小计或总计; + derivedFieldRules?: DerivedFieldRules; //派生字段定义 + ... +} +``` +dataConfig 应用举例: +### 1. 数据汇总规则 +[option说明](../../../option/PivotTable#dataConfig.totals) +配置示例: +``` +dataConfig: { + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + grandTotalLabel: '行总计', + subTotalLabel: '小计', + showGrandTotalsOnTop: true //汇总值显示在上 + }, + column: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['quarter'], + grandTotalLabel: '列总计', + subTotalLabel: '小计' + } + } + }, +``` +具体示例:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-total +### 2. 排序规则 +[option说明](../../../option/PivotTable#dataConfig.sortRules) +配置示例: +``` + sortRules: [ + { + sortField: 'city', + sortByIndicator: 'sales', + sortType: VTable.TYPES.SortType.DESC, + query: ['办公用品', '笔'] + } as VTable.TYPES.SortByIndicatorRule + ] + +``` + +如果需要修改排序规则 透视表可以使用接口 `updateSortRules`。 + +具体示例:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-sort-dimension +### 3. 过滤规则 +[option说明](../../../option/PivotTable#dataConfig.filterRules) +配置示例: +``` +filterRules: [ + { + filterFunc: (record: Record) => { + return record.province !== '四川省' || record.category !== '家具'; + } + } + ] +``` +具体示例:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-filter +### 4. 聚合方式 +[option说明](../../../option/PivotTable#dataConfig.aggregationRules) +配置示例: +``` + aggregationRules: [ + //做聚合计算的依据,如销售额如果没有配置则默认按聚合sum计算结果显示单元格内容 + { + indicatorKey: 'TotalSales', //指标名称 + field: 'Sales', //指标依据字段 + aggregationType: VTable.TYPES.AggregationType.SUM, //计算类型 + formatFun: sumNumberFormat + }, + { + indicatorKey: 'OrderCount', //指标名称 + field: 'Sales', //指标依据字段 + aggregationType: VTable.TYPES.AggregationType.COUNT, //计算类型 求数量 + formatFun: countNumberFormat + }, + { + indicatorKey: 'AverageOrderSales', //指标名称 + field: 'Sales', //指标依据字段 + aggregationType: VTable.TYPES.AggregationType.AVG, //计算类型 求平均 + }, + { + indicatorKey: 'MaxOrderSales', //指标名称 + field: 'Sales', //指标依据字段 + aggregationType: VTable.TYPES.AggregationType.MAX, //计算类型 求最大 + }, + { + indicatorKey: 'OrderSalesValue', //指标名称 + field: 'Sales', //指标依据字段 + aggregationType: VTable.TYPES.AggregationType.NONE, //不做聚合 匹配到其中对应数据获取其对应field的值 + } + ] +``` +具体示例:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-aggregation +### 5. 派生字段 +[option说明](../../../option/PivotTable#dataConfig.derivedFieldRules) +配置示例: +``` + derivedFieldRules: [ + { + fieldName: 'Year', + derivedFunc: VTable.DataStatistics.dateFormat('Order Date', '%y', true), + }, + { + fieldName: 'Month', + derivedFunc: VTable.DataStatistics.dateFormat('Order Date', '%n', true), + } + ] +``` +具体示例:https://visactor.io/vtable/demo/data-analysis/pivot-analysis-derivedField +## 数据分析过程 +依赖配置:维度,指标及dataConfig。 +### 遍历数据的流程: +遍历一遍records,解析出行列表头维度值用于展示表头单元格,将records中所有数据分配到对应的行列路径集合中并计算出body部分指标单元格的聚合值。 +
+ +

数据分析过程

+
+ +### 数据维度tree +根据上述遍历的结构,将产生一棵维度树,从这棵树可以查找到单元格的值及值的原始数据条目。 +
+ +

组织维度树聚合数据

+
+ 经过对record分组聚合的分析计算,最终呈现到表格中单元格数据和records数据源的对应关系: +
+ +

数据源条目和单元格的对应关系

+
+ +### 自定义维度树 +虽然具有分析能力的多维表格可以自动分析各个维度的维度值组成行列表头的树形结构,并且可以根据`dataConfig.sortRules`进行排序,但具有复杂业务逻辑的场景还是期望可以能够**自定义行列表头维度值**及顺序。那么可以通过rowTree和columnTree来实现这些业务需求场景。 +- enableDataAnalysis需设置为false来关闭VTable内部聚合数据的分析,提升一定的性能。 + +
+ +

custom rowTree columnTree

+
+ +自定义树的配置: +``` +const option = { + rowTree: [{ + dimensionKey: 'region', + value: '中南', + children: [ + { + dimensionKey: 'province', + value: '广东', + }, + { + dimensionKey: 'province', + value: '广西', + } + ] + }, + { + dimensionKey: 'region', + value: '华东', + children: [ + { + dimensionKey: 'province', + value: '上海', + }, + { + dimensionKey: 'province', + value: '山东', + } + ] + }], + columnTree: [{ + dimensionKey: 'year', + value: '2016', + children: [ + { + dimensionKey: 'quarter', + value: '2016-Q1', + children: [ + { + indicatorKey: 'sales', + value: 'sales' + }, + { + indicatorKey: 'profit', + value: 'profit' + } + ] + }, + { + dimensionKey: 'quarter', + value: '2016-Q2', + children: [ + { + indicatorKey: 'sales', + value: 'sales' + }, + { + indicatorKey: 'profit', + value: 'profit' + } + ] + } + ] + }], + indicators: ['sales', 'profit'], + //enableDataAnalysis:true, + corner: { + titleOnDimension: 'none' + }, + records: [ + { + region: '中南', + province: '广东', + year: '2016', + quarter: '2016-Q1', + sales: 1243, + profit: 546 + }, + { + region: '中南', + province: '广东', + year: '2016', + quarter: '2016-Q2', + sales: 2243, + profit: 169 + }, { + region: '中南', + province: '广西', + year: '2016', + quarter: '2016-Q1', + sales: 3043, + profit: 1546 + }, + { + region: '中南', + province: '广西', + year: '2016', + quarter: '2016-Q2', + sales: 1463, + profit: 609 + }, + { + region: '华东', + province: '上海', + year: '2016', + quarter: '2016-Q1', + sales: 4003, + profit: 1045 + }, + { + region: '华东', + province: '上海', + year: '2016', + quarter: '2016-Q2', + sales: 5243, + profit: 3169 + }, { + region: '华东', + province: '山东', + year: '2016', + quarter: '2016-Q1', + sales: 4543, + profit: 3456 + }, + { + region: '华东', + province: '山东', + year: '2016', + quarter: '2016-Q2', + sales: 6563, + profit: 3409 + } + ] +}; +``` +VTable官网示例:https://visactor.io/vtable/demo/table-type/pivot-table. + +自定义树的复杂在于组建行列维度树,可酌情根据业务场景来选择使用,如果具有复杂的排序、汇总或分页规则可选择使用自定义方式。 + +**注意:如果选择自定义树的配置方式将不开启VTable内部的数据聚合能力,即匹配到的数据条目中的某一条作为单元格指标值。** \ No newline at end of file diff --git a/docs/assets/option/en/column/base-column-type.md b/docs/assets/option/en/column/base-column-type.md index ceeacb5e1..3c6dd921b 100644 --- a/docs/assets/option/en/column/base-column-type.md +++ b/docs/assets/option/en/column/base-column-type.md @@ -11,6 +11,7 @@ ${prefix} field(string) ${prefix} fieldFormat(FieldFormat) Configure data formatting + ``` type FieldFormat = (record: any) => any; ``` @@ -45,9 +46,11 @@ Header cell style, configuration options are slightly different depending on the ${prefix} style Body cell style, type declaration: + ``` style?: IStyleOption | ((styleArg: StylePropertyFunctionArg) => IStyleOption); ``` + {{ use: common-StylePropertyFunctionArg() }} The type structure of IStyleOption is as follows: @@ -65,6 +68,7 @@ Header cell icon configuration. Available configuration types are: ``` string | ColumnIconOption | (string | ColumnIconOption)[]; ``` + For the specific configuration of ColumnIconOption, refer to the [definition](/zh/option.html#ListTable-columns-text.icon.ColumnIconOption定义:) ${prefix} icon(string|Object|Array|Funciton) @@ -80,9 +84,11 @@ icon?: ``` #${prefix} ColumnIconOption definition: + ``` type ColumnIconOption = ImageIcon | SvgIcon; ``` + #${prefix} ImageIcon(Object) {{ use: image-icon( prefix = '##' + ${prefix}) }} @@ -120,6 +126,7 @@ ${prefix} headerCustomRender(Function|Object) Custom rendering of header cell, in function or object form. The type is: `ICustomRenderFuc | ICustomRenderObj`. The definition of ICustomRenderFuc is: + ``` type ICustomRenderFuc = (args: CustomRenderFunctionArg) => ICustomRenderObj; ``` @@ -130,7 +137,6 @@ The definition of ICustomRenderFuc is: prefix = '#' + ${prefix}, ) }} - ${prefix} headerCustomLayout(Function) Custom layout element definition for header cell, suitable for complex layout cell content. @@ -138,6 +144,7 @@ Custom layout element definition for header cell, suitable for complex layout ce ``` (args: CustomRenderFunctionArg) => ICustomLayoutObj; ``` + {{ use: common-CustomRenderFunctionArg() }} {{ use: custom-layout( @@ -148,16 +155,17 @@ ${prefix} customRender(Function|Object) Custom rendering for body cell header cell, in function or object form. The type is: `ICustomRenderFuc | ICustomRenderObj`. The definition of ICustomRenderFuc is: + ``` type ICustomRenderFuc = (args: CustomRenderFunctionArg) => ICustomRenderObj; ``` + {{ use: common-CustomRenderFunctionArg() }} {{ use: common-custom-render-object( prefix = '#' + ${prefix}, ) }} - ${prefix} customLayout(Function) Custom layout element definition for body cell, suitable for complex layout content. @@ -165,6 +173,7 @@ Custom layout element definition for body cell, suitable for complex layout cont ``` (args: CustomRenderFunctionArg) => ICustomLayoutObj; ``` + {{ use: common-CustomRenderFunctionArg() }} {{ use: custom-layout( @@ -182,6 +191,7 @@ Whether to disable column width adjustment. If it is a transposed table or a piv ${prefix} tree (boolean) Whether to display this column as a tree structure, which needs to be combined with the records data structure to be implemented, the nodes that need to be expanded are configured with `children` to accommodate sub-node data. For example: + ``` { "department": "Human Resources Department", @@ -204,18 +214,33 @@ Whether to display this column as a tree structure, which needs to be combined w ${prefix} editor (string|Object|Function) Configure the column cell editor + ``` editor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` -Among them, IEditor is the editor interface defined in @visactor/vtable-editors. For details, please refer to the source code: https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts . +Among them, IEditor is the editor interface defined in @visactor/vtable-editors. For details, please refer to the source code: https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts . ${prefix} headerEditor (string|Object|Function) Configure the display title of this column header + ``` headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` ${prefix} columns (Array) Configure arrays with upper columns, nesting structures to describe column grouping relationships. + +${prefix} hideColumnsSubHeader(boolean) = false +Whether to hide the header title of the subtable header. The default value is not hidden. + +${prefix} aggregation(Aggregation | CustomAggregation | Array) + +Not required. + +Data aggregation summary configuration to analyze the column data. + +Global options can also be configured to configure aggregation rules for each column. + +Please refer to the tutorial document diff --git a/docs/assets/option/en/column/chart-column-type.md b/docs/assets/option/en/column/chart-column-type.md index 5a440eabc..669d31391 100644 --- a/docs/assets/option/en/column/chart-column-type.md +++ b/docs/assets/option/en/column/chart-column-type.md @@ -22,4 +22,4 @@ Corresponding to the injected chart library component name **Chart type exclusive configuration options** -Corresponding to the chart library's spec, the value is provided in the records +Set the spec of the chart, or set it to a function that returns a different spec. The data displayed in the chart is provided by records. diff --git a/docs/assets/option/en/indicator/chart-indicator-type.md b/docs/assets/option/en/indicator/chart-indicator-type.md index b235567d3..ff81adccf 100644 --- a/docs/assets/option/en/indicator/chart-indicator-type.md +++ b/docs/assets/option/en/indicator/chart-indicator-type.md @@ -18,7 +18,7 @@ Corresponding to the name of the injected chart library component, the injection **Exclusive configuration options for chart type** -Corresponds to the chart library's spec, where value corresponds to the provided records +Set the spec of the chart, or set it to a function that returns a different spec. The data displayed in the chart is provided by records. {{ use: base-indicator-type( prefix = '##'+${prefix} diff --git a/docs/assets/option/en/table/listTable.md b/docs/assets/option/en/table/listTable.md index 95b09fb82..5bb5fd6dd 100644 --- a/docs/assets/option/en/table/listTable.md +++ b/docs/assets/option/en/table/listTable.md @@ -74,7 +74,9 @@ order: 'desc' | 'asc' | 'normal'; Global configuration cell editor ``` + editor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); + ``` Among them, IEditor is the editor interface defined in @visactor/vtable-editors. For details, please refer to the source code: https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts . @@ -82,7 +84,9 @@ ${prefix} headerEditor (string|Object|Function) Global configuration for the editor of the display title in the table header ``` + headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); + ``` {{ use: common-option-secondary( @@ -106,3 +110,35 @@ Drag the table header to move the position. Rules for frozen parts. The default - "disabled" (disables adjusting the position of frozen columns): The headers of other columns are not allowed to be moved into the frozen column, nor are the frozen columns allowed to be moved out. The frozen column remains unchanged. - "adjustFrozenCount" (adjust the number of frozen columns based on the interaction results): allows the headers of other columns to move into the frozen column, and the frozen column to move out, and adjusts the number of frozen columns based on the dragging action. When the headers of other columns are dragged into the frozen column position, the number of frozen columns increases; when the headers of other columns are dragged out of the frozen column position, the number of frozen columns decreases. - "fixedFrozenCount" (can adjust frozen columns and keep the number of frozen columns unchanged): Allows you to freely drag the headers of other columns into or out of the frozen column position while keeping the number of frozen columns unchanged. + +## aggregation(Aggregation|CustomAggregation|Array|Function) + +Data aggregation summary analysis configuration, global configuration, each column will have aggregation logic, it can also be configured in the column (columns) definition, the configuration in the column has a higher priority. + +``` +aggregation?: + | Aggregation + | CustomAggregation + | (Aggregation | CustomAggregation)[] + | ((args: { + col: number; + field: string; + }) => Aggregation | CustomAggregation | (Aggregation | CustomAggregation)[] | null); +``` + +Among them: + +``` +type Aggregation = { + aggregationType: AggregationType; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; + +type CustomAggregation = { + aggregationType: AggregationType.CUSTOM; + aggregationFun: (values: any[], records: any[]) => any; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; +``` diff --git a/docs/assets/option/zh/column/base-column-type.md b/docs/assets/option/zh/column/base-column-type.md index 5aaeac17d..f8d9e7ead 100644 --- a/docs/assets/option/zh/column/base-column-type.md +++ b/docs/assets/option/zh/column/base-column-type.md @@ -214,17 +214,33 @@ ${prefix} tree (boolean) ${prefix} editor (string|Object|Function) 配置该列单元格编辑器 + ``` editor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` -其中IEditor是@visactor/vtable-editors中定义的编辑器接口,具体可以参看源码:https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts。 + +其中 IEditor 是@visactor/vtable-editors 中定义的编辑器接口,具体可以参看源码:https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts。 ${prefix} headerEditor (string|Object|Function) -配置该列表头显示标题title +配置该列表头显示标题 title + ``` headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` ${prefix} columns (Array) 同上层的列配置数组,嵌套结构来描述列分组关系。 + +${prefix} hideColumnsSubHeader(boolean) = false +是否隐藏子表头的 header 标题,默认不隐藏。 + +${prefix} aggregation(Aggregation | CustomAggregation | Array) + +非必填。 + +数据聚合配置,对该列数据进行汇总分析。 + +全局 option 也可以配置,对每一列都配置聚合规则。 + +可参考教程文档 diff --git a/docs/assets/option/zh/column/chart-column-type.md b/docs/assets/option/zh/column/chart-column-type.md index 509c032c6..376e3ba30 100644 --- a/docs/assets/option/zh/column/chart-column-type.md +++ b/docs/assets/option/zh/column/chart-column-type.md @@ -22,4 +22,4 @@ **chart 类型专属配置项** -对应图表库的 spec 其中 value 对应在 records 中提供 +设置图表的 spec,或者设置成函数返回不同的spec。其中显示在图表的数据是对应在 records 中提供。 diff --git a/docs/assets/option/zh/indicator/chart-indicator-type.md b/docs/assets/option/zh/indicator/chart-indicator-type.md index 74e1159b8..3501d0fc7 100644 --- a/docs/assets/option/zh/indicator/chart-indicator-type.md +++ b/docs/assets/option/zh/indicator/chart-indicator-type.md @@ -18,7 +18,7 @@ **chart 类型专属配置项** -对应图表库的 spec,其中图表所需数据会对应在 records 中提供。 +设置图表的 spec,或者设置成函数返回不同的spec。其中显示在图表的数据由 records 提供。 {{ use: base-indicator-type( prefix = '##'+${prefix} diff --git a/docs/assets/option/zh/table/listTable.md b/docs/assets/option/zh/table/listTable.md index 7e8b7b643..89ebc928e 100644 --- a/docs/assets/option/zh/table/listTable.md +++ b/docs/assets/option/zh/table/listTable.md @@ -36,7 +36,7 @@ 分页配置。 -基本表格和VTable数据分析透视表支持分页,透视组合图不支持分页。 +基本表格和 VTable 数据分析透视表支持分页,透视组合图不支持分页。 IPagination 的具体类型如下: @@ -44,13 +44,13 @@ IPagination 的具体类型如下: 数据总条数。 -非必传!透视表中这个字段VTable会自动补充,帮助用户获取到总共数据条数 +非必传!透视表中这个字段 VTable 会自动补充,帮助用户获取到总共数据条数 ### perPageCount (number) 每页显示数据条数。 -注意! 透视表中perPageCount会自动修正为指标数量的整数倍。 +注意! 透视表中 perPageCount 会自动修正为指标数量的整数倍。 ### currentPage (number) @@ -72,14 +72,17 @@ SortState { ## editor (string|Object|Function) 全局配置单元格编辑器 + ``` editor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` -其中IEditor是@visactor/vtable-editors中定义的编辑器接口,具体可以参看源码:https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts。 + +其中 IEditor 是@visactor/vtable-editors 中定义的编辑器接口,具体可以参看源码:https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts。 ${prefix} headerEditor (string|Object|Function) -全局配置表头显示标题title的编辑器 +全局配置表头显示标题 title 的编辑器 + ``` headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); ``` @@ -95,13 +98,44 @@ headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI } ## hierarchyExpandLevel(number) -展示为树形结构时,默认展开层数。默认为1只显示根节点,配置为`Infinity`则全部展开。 - +展示为树形结构时,默认展开层数。默认为 1 只显示根节点,配置为`Infinity`则全部展开。 ## frozenColDragHeaderMode(string) = 'fixedFrozenCount' -拖拽表头移动位置 针对冻结部分的规则 默认为fixedFrozenCount +拖拽表头移动位置 针对冻结部分的规则 默认为 fixedFrozenCount - "disabled"(禁止调整冻结列位置):不允许其他列的表头移入冻结列,也不允许冻结列移出,冻结列保持不变。 - "adjustFrozenCount"(根据交互结果调整冻结数量):允许其他列的表头移入冻结列,及冻结列移出,并根据拖拽的动作调整冻结列的数量。当其他列的表头被拖拽进入冻结列位置时,冻结列数量增加;当其他列的表头被拖拽移出冻结列位置时,冻结列数量减少。 -- "fixedFrozenCount"(可调整冻结列,并维持冻结数量不变):允许自由拖拽其他列的表头移入或移出冻结列位置,同时保持冻结列的数量不变。 \ No newline at end of file +- "fixedFrozenCount"(可调整冻结列,并维持冻结数量不变):允许自由拖拽其他列的表头移入或移出冻结列位置,同时保持冻结列的数量不变。 + +## aggregation(Aggregation|CustomAggregation|Array|Function) + +数据聚合汇总分析配置,全局配置每一列都将有聚合逻辑,也可以在列(columns)定义中配置,列中配置的优先级更高。 + +``` +aggregation?: + | Aggregation + | CustomAggregation + | (Aggregation | CustomAggregation)[] + | ((args: { + col: number; + field: string; + }) => Aggregation | CustomAggregation | (Aggregation | CustomAggregation)[] | null); +``` + +其中: + +``` +type Aggregation = { + aggregationType: AggregationType; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; + +type CustomAggregation = { + aggregationType: AggregationType.CUSTOM; + aggregationFun: (values: any[], records: any[]) => any; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; +``` diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index 12ea3bd6d..71bddedf7 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vtable", - "version": "0.19.1", + "version": "0.20.1", "description": "The react version of VTable", "keywords": [ "react", diff --git a/packages/react-vtable/src/components/component/menu.tsx b/packages/react-vtable/src/components/component/menu.tsx index 7065db12a..282e09123 100644 --- a/packages/react-vtable/src/components/component/menu.tsx +++ b/packages/react-vtable/src/components/component/menu.tsx @@ -8,7 +8,7 @@ export type MenuProps = { /** 内置下拉菜单的全局设置项 目前只针对基本表格有效 会对每个表头单元格开启默认的下拉菜单功能。代替原来的option.dropDownMenu*/ defaultHeaderMenuItems?: TYPES.MenuListItem[]; /** 右键菜单。代替原来的option.contextmenu */ - contextMenuItems?: TYPES.MenuListItem[] | ((field: string, row: number) => TYPES.MenuListItem[]); + contextMenuItems?: TYPES.MenuListItem[] | ((field: string, row: number, col: number) => TYPES.MenuListItem[]); /** 设置选中状态的菜单。代替原来的option.dropDownMenuHighlight */ dropDownMenuHighlight?: TYPES.DropDownMenuHighlightInfo[]; } & BaseComponentProps; diff --git a/packages/react-vtable/src/tables/base-table.tsx b/packages/react-vtable/src/tables/base-table.tsx index a626a4bd5..a8027810e 100644 --- a/packages/react-vtable/src/tables/base-table.tsx +++ b/packages/react-vtable/src/tables/base-table.tsx @@ -191,7 +191,9 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { !isEqual(eventsBinded.current.records, props.records, { skipFunction: skipFunctionDiff }) ) { eventsBinded.current = props; - tableContext.current.table.setRecords(props.records); + tableContext.current.table.setRecords(props.records, { + restoreHierarchyState: props.option.restoreHierarchyState + }); handleTableRender(); } return; @@ -202,7 +204,7 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { if ( !isEqual(newOption, prevOption.current, { skipFunction: skipFunctionDiff }) || // tableContext.current.isChildrenUpdated - !isEqual(newOptionFromChildren, optionFromChildren.current) + !isEqual(newOptionFromChildren, optionFromChildren.current, { skipFunction: skipFunctionDiff }) ) { prevOption.current = newOption; optionFromChildren.current = newOptionFromChildren; @@ -212,7 +214,9 @@ const BaseTable: React.FC = React.forwardRef((props, ref) => { handleTableRender(); } else if (hasRecords && !isEqual(props.records, prevRecords.current, { skipFunction: skipFunctionDiff })) { prevRecords.current = props.records; - tableContext.current.table.setRecords(props.records); + tableContext.current.table.setRecords(props.records, { + restoreHierarchyState: props.option.restoreHierarchyState + }); handleTableRender(); } // tableContext.current = { diff --git a/packages/vtable-editors/package.json b/packages/vtable-editors/package.json index a09624fef..70848d722 100644 --- a/packages/vtable-editors/package.json +++ b/packages/vtable-editors/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-editors", - "version": "0.19.1", + "version": "0.20.1", "description": "", "sideEffects": false, "main": "cjs/index.js", diff --git a/packages/vtable-export/package.json b/packages/vtable-export/package.json index 99c224180..a9cbd3bdc 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-export", - "version": "0.19.1", + "version": "0.20.1", "description": "The export util of VTable", "author": { "name": "VisActor", diff --git a/packages/vtable/CHANGELOG.json b/packages/vtable/CHANGELOG.json index 42899acb0..61f85bcc7 100644 --- a/packages/vtable/CHANGELOG.json +++ b/packages/vtable/CHANGELOG.json @@ -1,6 +1,96 @@ { "name": "@visactor/vtable", "entries": [ + { + "version": "0.20.1", + "tag": "@visactor/vtable_v0.20.1", + "date": "Thu, 29 Feb 2024 11:57:42 GMT", + "comments": { + "none": [ + { + "comment": "fix: hideColumnsSubheader with three levels show error #1105\n\n" + }, + { + "comment": "feat: add api getRecordIndexByCell #1121\n\n" + }, + { + "comment": "refactor: rename resize_column_end event arguments #1129\n\n" + }, + { + "comment": "refactor: api return value type\n\n" + }, + { + "comment": "refactor: setRecords support restoreHierarchyState #1148\n\n" + }, + { + "comment": "fix: customlayout flex render error #1163\n\n" + }, + { + "comment": "refactor: vtable not stop event bubble #892" + }, + { + "comment": "fix: when scroll tooltip hide #905\n\n" + }, + { + "comment": "fix: fix axis innerOffset" + }, + { + "comment": "fix-contextMenuItems-add-col-param" + }, + { + "comment": "fix: add skipFunctionDiff in react-vtable" + }, + { + "comment": "refactor: remove Circular dependency\n\n" + } + ] + } + }, + { + "version": "0.20.0", + "tag": "@visactor/vtable_v0.20.0", + "date": "Fri, 23 Feb 2024 10:06:24 GMT", + "comments": { + "none": [ + { + "comment": "feat: add aggregation for list table column\n\n" + }, + { + "comment": "feat: add api getAggregateValuesByField\n\n" + }, + { + "comment": "feat: add custom aggregation\n\n" + }, + { + "comment": "fix: edit right frozen cell input position error\n\n" + }, + { + "comment": "fix: mouseleave_cell event trigger #1112\n\n" + }, + { + "comment": "feat: chartSpec support function #1115\n\n" + }, + { + "comment": "feat: add filter data config #607\n\n" + }, + { + "comment": "fix: fix cellBgColor judgement in isCellHover()" + }, + { + "comment": "fix: fix custom merge cell computed height&width" + }, + { + "comment": "fix: fix content position update problem" + }, + { + "comment": "fix: merge cell update in setDropDownMenuHighlight()" + }, + { + "comment": "fix: fix react-vtable display error in react strict mode #990" + } + ] + } + }, { "version": "0.19.1", "tag": "@visactor/vtable_v0.19.1", diff --git a/packages/vtable/CHANGELOG.md b/packages/vtable/CHANGELOG.md index dbad8447a..0a595defb 100644 --- a/packages/vtable/CHANGELOG.md +++ b/packages/vtable/CHANGELOG.md @@ -1,6 +1,72 @@ # Change Log - @visactor/vtable -This log was last generated on Mon, 05 Feb 2024 12:36:17 GMT and should not be manually modified. +This log was last generated on Thu, 29 Feb 2024 11:57:42 GMT and should not be manually modified. + +## 0.20.1 +Thu, 29 Feb 2024 11:57:42 GMT + +### Updates + +- fix: hideColumnsSubheader with three levels show error #1105 + + +- feat: add api getRecordIndexByCell #1121 + + +- refactor: rename resize_column_end event arguments #1129 + + +- refactor: api return value type + + +- refactor: setRecords support restoreHierarchyState #1148 + + +- fix: customlayout flex render error #1163 + + +- refactor: vtable not stop event bubble #892 +- fix: when scroll tooltip hide #905 + + +- fix: fix axis innerOffset +- fix-contextMenuItems-add-col-param +- fix: add skipFunctionDiff in react-vtable +- refactor: remove Circular dependency + + + +## 0.20.0 +Fri, 23 Feb 2024 10:06:24 GMT + +### Updates + +- feat: add aggregation for list table column + + +- feat: add api getAggregateValuesByField + + +- feat: add custom aggregation + + +- fix: edit right frozen cell input position error + + +- fix: mouseleave_cell event trigger #1112 + + +- feat: chartSpec support function #1115 + + +- feat: add filter data config #607 + + +- fix: fix cellBgColor judgement in isCellHover() +- fix: fix custom merge cell computed height&width +- fix: fix content position update problem +- fix: merge cell update in setDropDownMenuHighlight() +- fix: fix react-vtable display error in react strict mode #990 ## 0.19.1 Mon, 05 Feb 2024 12:36:17 GMT diff --git a/packages/vtable/examples/header/merge-cell.ts b/packages/vtable/examples/header/merge-cell.ts index c67574f88..08e701e39 100644 --- a/packages/vtable/examples/header/merge-cell.ts +++ b/packages/vtable/examples/header/merge-cell.ts @@ -24,6 +24,121 @@ export function createTable() { id: 4, name: 'd' }, + { + progress: 28, + id: 5, + name: 'e' + }, + { + progress: 100, + id: 1, + name: 'a' + }, + { + progress: 80, + id: 2, + name: 'a' + }, + { + progress: 1, + id: 3, + name: 'c' + }, + { + progress: 55, + id: 4, + name: 'd' + }, + { + progress: 28, + id: 5, + name: 'e' + }, + { + progress: 100, + id: 1, + name: 'a' + }, + { + progress: 80, + id: 2, + name: 'a' + }, + { + progress: 1, + id: 3, + name: 'c' + }, + { + progress: 55, + id: 4, + name: 'd' + }, + { + progress: 28, + id: 5, + name: 'e' + }, + { + progress: 100, + id: 1, + name: 'a' + }, + { + progress: 80, + id: 2, + name: 'a' + }, + { + progress: 1, + id: 3, + name: 'c' + }, + { + progress: 55, + id: 4, + name: 'd' + }, + { + progress: 28, + id: 5, + name: 'e' + }, + { + progress: 100, + id: 1, + name: 'a' + }, + { + progress: 80, + id: 2, + name: 'a' + }, + { + progress: 1, + id: 3, + name: 'c' + }, + { + progress: 55, + id: 4, + name: 'd' + }, + { + progress: 28, + id: 5, + name: 'e' + }, + { + progress: 1, + id: 3, + name: 'c' + }, + { + progress: 55, + id: 4, + name: 'd' + }, { progress: 28, id: 5, @@ -40,7 +155,10 @@ export function createTable() { }, title: 'progress', description: '这是一个标题的详细描述', - width: 150 + width: 150, + style: { + // textStick: true + } }, { field: 'id', @@ -57,6 +175,9 @@ export function createTable() { } return v1 === v2 ? 0 : v1 > v2 ? 1 : -1; }, + style: { + textStick: true + }, width: 100, mergeCell: true }, @@ -112,31 +233,79 @@ export function createTable() { console.log(v, v2); return v === v2; } + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 + }, + { + field: 'id', + title: 'ID说明', + width: 150 } ], showFrozenIcon: true, //显示VTable内置冻结列图标 widthMode: 'standard', - allowFrozenColCount: 2 + allowFrozenColCount: 2, + customMergeCell: (col, row, table) => { + if (col <= 1 && row > 0 && row < 11) { + return { + text: 'customMergeCell', + range: { + start: { + col: 0, + row: 1 + }, + end: { + col: 1, + row: 10 + } + }, + style: { + bgColor: 'red', + textStick: true + } + }; + } + } }; const instance = new ListTable(option); - + window.tableInstance = instance; //设置表格数据 instance.setRecords(personsDataSource, { field: 'id', order: 'desc' }); - // instance.setRecords(personsDataSource); - - VTable.bindDebugTool(instance.scenegraph.stage as any, { - customGrapicKeys: ['role', '_updateTag'] - }); - - // instance.updateSortState({ - // field: 'id', - // order: 'desc', - // }); - - // 只为了方便控制太调试用,不要拷贝 - window.tableInstance = instance; } diff --git a/packages/vtable/examples/list-analysis/list-aggregation-edit.ts b/packages/vtable/examples/list-analysis/list-aggregation-edit.ts new file mode 100644 index 000000000..04e832633 --- /dev/null +++ b/packages/vtable/examples/list-analysis/list-aggregation-edit.ts @@ -0,0 +1,225 @@ +import * as VTable from '../../src'; +import { AggregationType } from '../../src/ts-types'; +import { InputEditor } from '@visactor/vtable-editors'; +const CONTAINER_ID = 'vTable'; +const input_editor = new InputEditor({}); +VTable.register.editor('input', input_editor); +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + salary: Math.round(Math.random() * 10000) + })); +}; + +export function createTable() { + const records = generatePersons(300); + const columns: VTable.ColumnsDefine = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 2; + }, + aggregation: { + aggregationType: AggregationType.NONE, + formatFun() { + return '汇总:'; + } + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'salary', + title: 'salary', + width: 100 + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.MAX, + // showOnTop: true, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.MIN, + showOnTop: true, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + frozenColCount: 1, + bottomFrozenRowCount: 2, + rightFrozenColCount: 1, + overscrollBehavior: 'none', + autoWrapText: true, + widthMode: 'autoWidth', + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + }, + theme: VTable.themes.DEFAULT.extends({ + bottomFrozenStyle: { + bgColor: '#ECF1F5', + borderLineWidth: [6, 0, 1, 0], + borderColor: ['gray'] + } + }), + editor: 'input', + headerEditor: 'input', + aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最大ID:' + Math.round(value) + '号'; + } + }, + { + aggregationType: AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return '最小ID:' + Math.round(value) + '号'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低低低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均平均平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ]; + } + return null; + } + // transpose: true + // widthMode: 'adaptive' + }; + const tableInstance = new VTable.ListTable(option); + // tableInstance.updateFilterRules([ + // { + // filterKey: 'sex', + // filteredValues: ['boy'] + // } + // ]); + window.tableInstance = tableInstance; + tableInstance.on('change_cell_value', arg => { + console.log(arg); + }); +} diff --git a/packages/vtable/examples/list-analysis/list-aggregation-global.ts b/packages/vtable/examples/list-analysis/list-aggregation-global.ts new file mode 100644 index 000000000..19aa352de --- /dev/null +++ b/packages/vtable/examples/list-analysis/list-aggregation-global.ts @@ -0,0 +1,265 @@ +import * as VTable from '../../src'; +import { AggregationType } from '../../src/ts-types'; +const CONTAINER_ID = 'vTable'; +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + salary: Math.round(Math.random() * 10000) + })); +}; + +export function createTable() { + const records = generatePersons(300); + const columns: VTable.ColumnsDefine = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 1; + }, + aggregation: { + aggregationType: AggregationType.NONE, + formatFun() { + return '汇总:'; + } + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'salary', + title: 'salary', + width: 100 + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + sortState: { + field: 'id', + order: 'desc' + }, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + // frozenColCount: 1, + bottomFrozenRowCount: 3, + rightFrozenColCount: 1, + overscrollBehavior: 'none', + autoWrapText: true, + widthMode: 'autoWidth', + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + }, + theme: VTable.themes.DEFAULT.extends({ + bottomFrozenStyle: { + bgColor: '#ECF1F5', + borderLineWidth: [6, 0, 1, 0], + borderColor: ['gray'] + } + }), + // customMergeCell: (col, row, table) => { + // if (col >= 0 && col < table.colCount && row === table.rowCount - 1) { + // return { + // text: '统计数据中平均薪资:' + table.getCellOriginValue(table.colCount - 1, table.rowCount - 1), + // range: { + // start: { + // col: 0, + // row: table.rowCount - 1 + // }, + // end: { + // col: table.colCount - 1, + // row: table.rowCount - 1 + // } + // }, + // style: { + // borderLineWidth: [6, 1, 1, 1], + // borderColor: ['gray'], + // textAlign: 'center' + // } + // }; + // } + // if (col >= 0 && col < table.colCount && row === table.rowCount - 2) { + // return { + // text: '统计数据中最低薪资:' + table.getCellOriginValue(table.colCount - 1, table.rowCount - 2), + // range: { + // start: { + // col: 0, + // row: table.rowCount - 2 + // }, + // end: { + // col: table.colCount - 1, + // row: table.rowCount - 2 + // } + // }, + // style: { + // textStick: true, + // borderLineWidth: [6, 1, 1, 1], + // borderColor: ['gray'], + // textAlign: 'center' + // } + // }; + // } + // }, + aggregation(args) { + if (args.col === 1) { + return [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最大ID:' + Math.round(value) + '号'; + } + }, + { + aggregationType: AggregationType.MIN, + showOnTop: false, + formatFun(value, col, row, table) { + return '最小ID:' + Math.round(value) + '号'; + } + } + ]; + } + if (args.field === 'salary') { + return [ + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低低低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均平均平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ]; + } + return null; + } + // transpose: true + // widthMode: 'adaptive' + }; + const tableInstance = new VTable.ListTable(option); + // tableInstance.updateFilterRules([ + // { + // filterKey: 'sex', + // filteredValues: ['boy'] + // } + // ]); + window.tableInstance = tableInstance; + tableInstance.on('change_cell_value', arg => { + console.log(arg); + }); +} diff --git a/packages/vtable/examples/list-analysis/list-aggregation.ts b/packages/vtable/examples/list-analysis/list-aggregation.ts new file mode 100644 index 000000000..813ce0740 --- /dev/null +++ b/packages/vtable/examples/list-analysis/list-aggregation.ts @@ -0,0 +1,230 @@ +import * as VTable from '../../src'; +import { AggregationType } from '../../src/ts-types'; +const CONTAINER_ID = 'vTable'; +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + salary: Math.round(Math.random() * 10000) + })); +}; + +export function createTable() { + const records = generatePersons(300); + const columns: VTable.ColumnsDefine = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 1; + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.AVG + }, + { + aggregationType: AggregationType.SUM, + showOnTop: true + } + ] + }, + { + field: 'salary', + title: 'salary', + width: 100, + aggregation: [ + { + aggregationType: AggregationType.MAX, + formatFun(value) { + return '最高薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.MIN, + formatFun(value) { + return '最低薪资:' + Math.round(value) + '元'; + } + }, + { + aggregationType: AggregationType.AVG, + showOnTop: false, + formatFun(value, col, row, table) { + return '平均:' + Math.round(value) + '元 (共计' + table.recordsCount + '条数据)'; + } + } + ] + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + sortState: { + field: 'id', + order: 'desc' + }, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + // frozenColCount: 1, + bottomFrozenRowCount: 3, + rightFrozenColCount: 1, + overscrollBehavior: 'none', + autoWrapText: true, + widthMode: 'autoWidth', + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + }, + theme: VTable.themes.DEFAULT.extends({ + cornerRightBottomCellStyle: { + bgColor: 'rgba(1,1,1,0.1)', + borderColor: 'red' + } + }), + customMergeCell: (col, row, table) => { + if (col >= 0 && col < table.colCount && row === table.rowCount - 1) { + return { + text: '统计数据中平均薪资:' + table.getCellOriginValue(table.colCount - 1, table.rowCount - 1), + range: { + start: { + col: 0, + row: table.rowCount - 1 + }, + end: { + col: table.colCount - 1, + row: table.rowCount - 1 + } + }, + style: { + borderLineWidth: [6, 1, 1, 1], + borderColor: ['gray'], + textAlign: 'center' + } + }; + } + if (col >= 0 && col < table.colCount && row === table.rowCount - 2) { + return { + text: '统计数据中最低薪资:' + table.getCellOriginValue(table.colCount - 1, table.rowCount - 2), + range: { + start: { + col: 0, + row: table.rowCount - 2 + }, + end: { + col: table.colCount - 1, + row: table.rowCount - 2 + } + }, + style: { + textStick: true, + borderLineWidth: [6, 1, 1, 1], + borderColor: ['gray'], + textAlign: 'center' + } + }; + } + } + // transpose: true + // widthMode: 'adaptive' + }; + const tableInstance = new VTable.ListTable(option); + // tableInstance.updateFilterRules([ + // { + // filterKey: 'sex', + // filteredValues: ['boy'] + // } + // ]); + window.tableInstance = tableInstance; + tableInstance.on('change_cell_value', arg => { + console.log(arg); + }); +} diff --git a/packages/vtable/examples/list-analysis/list-filter.ts b/packages/vtable/examples/list-analysis/list-filter.ts new file mode 100644 index 000000000..1353ca118 --- /dev/null +++ b/packages/vtable/examples/list-analysis/list-filter.ts @@ -0,0 +1,365 @@ +import * as VTable from '../../src'; +const CONTAINER_ID = 'vTable'; +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing' + })); +}; + +export function createTable() { + const records = generatePersons(1000000); + const columns: VTable.ColumnsDefine = [ + { + field: '', + title: '行号', + width: 80, + fieldFormat(data, col, row, table) { + return row - 1; + } + }, + { + field: 'id', + title: 'ID', + width: '1%', + minWidth: 200, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + }, + { + field: 'tel', + title: 'telephone', + width: 150 + }, + { + field: 'work', + title: 'job', + width: 200 + }, + { + field: 'city', + title: 'city', + width: 150 + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + sortState: { + field: 'id', + order: 'desc' + }, + // dataConfig: { + // filterRules: [ + // { + // filterFunc: (record: Record) => { + // return record.id % 2 === 0; + // } + // } + // ] + // }, + columns, + tooltip: { + isShowOverflowTextTooltip: true + }, + frozenColCount: 1, + bottomFrozenRowCount: 2, + rightFrozenColCount: 2, + overscrollBehavior: 'none', + autoWrapText: true, + heightMode: 'autoHeight', + dragHeaderMode: 'all', + keyboardOptions: { + pasteValueToCell: true + }, + eventOptions: { + preventDefaultContextMenu: false + }, + pagination: { + perPageCount: 100, + currentPage: 0 + } + // widthMode: 'adaptive' + }; + const tableInstance = new VTable.ListTable(option); + setTimeout(() => { + console.log(tableInstance.rowCount); + tableInstance.updateFilterRules([ + { + filterKey: 'sex', + filteredValues: ['boy'] + }, + { + filterFunc: (record: Record) => { + return record.id % 3 === 0; + } + } + ]); + }, 3000); + window.tableInstance = tableInstance; + tableInstance.on('change_cell_value', arg => { + console.log(arg); + }); +} diff --git a/packages/vtable/examples/list-analysis/olympic-winners.ts b/packages/vtable/examples/list-analysis/olympic-winners.ts new file mode 100644 index 000000000..468c98eb7 --- /dev/null +++ b/packages/vtable/examples/list-analysis/olympic-winners.ts @@ -0,0 +1,220 @@ +/* eslint-disable */ +import * as VTable from '../../src'; +const CONTAINER_ID = 'vTable'; + +export function createTable() { + var tableInstance; + VTable.register.icon('filter', { + name: 'filter', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' + }); + + VTable.register.icon('filtered', { + name: 'filtered', + type: 'svg', + width: 20, + height: 20, + marginRight: 6, + positionType: VTable.TYPES.IconPosition.right, + // interactive: true, + svg: '' + }); + fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/olympic-winners.json') + .then(res => res.json()) + .then(data => { + const columns = [ + { + field: 'athlete', + title: 'athlete', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.NONE, + formatFun(value) { + return 'Total:'; + } + } + }, + { + field: 'age', + title: 'age', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.AVG, + formatFun(value) { + return Math.round(value) + '(Avg)'; + } + } + }, + { + field: 'country', + title: 'country', + headerIcon: 'filter', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.CUSTOM, + aggregationFun(values, records) { + // 使用 reduce() 方法统计金牌数 + const goldMedalCountByCountry = records.reduce((acc, data) => { + const country = data.country; + const gold = data.gold; + + if (acc[country]) { + acc[country] += gold; + } else { + acc[country] = gold; + } + return acc; + }, {}); + + // 找出金牌数最多的国家 + let maxGoldMedals = 0; + let countryWithMaxGoldMedals = ''; + + for (const country in goldMedalCountByCountry) { + if (goldMedalCountByCountry[country] > maxGoldMedals) { + maxGoldMedals = goldMedalCountByCountry[country]; + countryWithMaxGoldMedals = country; + } + } + return { + country: countryWithMaxGoldMedals, + gold: maxGoldMedals + }; + }, + formatFun(value) { + return `Top country in gold medals: ${value.country},\nwith ${value.gold} gold medals`; + } + } + }, + { field: 'year', title: 'year', headerIcon: 'filter' }, + { field: 'sport', title: 'sport', headerIcon: 'filter' }, + { + field: 'gold', + title: 'gold', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'silver', + title: 'silver', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'bronze', + title: 'bronze', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + }, + { + field: 'total', + title: 'total', + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM, + formatFun(value) { + return Math.round(value) + '(Sum)'; + } + } + } + ]; + const option = { + columns, + records: data, + autoWrapText: true, + heightMode: 'autoHeight', + widthMode: 'autoWidth', + bottomFrozenRowCount: 1, + theme: VTable.themes.ARCO.extends({ + bottomFrozenStyle: { + fontFamily: 'PingFang SC', + fontWeight: 500 + } + }) + }; + const t0 = window.performance.now(); + tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID), option); + window.tableInstance = tableInstance; + const filterListValues = { + country: ['all', 'China', 'United States', 'Australia'], + year: ['all', '2004', '2008', '2012', '2016', '2020'], + sport: ['all', 'Swimming', 'Cycling', 'Biathlon', 'Short-Track Speed Skating', 'Nordic Combined'] + }; + let filterListSelectedValues = ''; + let lastFilterField; + tableInstance.on('icon_click', args => { + const { col, row, name } = args; + if (name === 'filter') { + const field = tableInstance.getHeaderField(col, row); + if (select && lastFilterField === field) { + removeFilterElement(); + lastFilterField = null; + } else if (!select || lastFilterField !== field) { + const rect = tableInstance.getCellRelativeRect(col, row); + createFilterElement(filterListValues[field], filterListSelectedValues, field, rect); + lastFilterField = field; + } + } + }); + + let filterContainer = tableInstance.getElement(); + let select; + function createFilterElement(values, curValue, field, positonRect) { + // create select tag + select = document.createElement('select'); + select.setAttribute('type', 'text'); + select.style.position = 'absolute'; + select.style.padding = '4px'; + select.style.width = '100%'; + select.style.boxSizing = 'border-box'; + + // create option tags + let opsStr = ''; + values.forEach(item => { + opsStr += + item === curValue + ? `` + : ``; + }); + select.innerHTML = opsStr; + + filterContainer.appendChild(select); + + select.style.top = positonRect.top + positonRect.height + 'px'; + select.style.left = positonRect.left + 'px'; + select.style.width = positonRect.width + 'px'; + select.style.height = positonRect.height + 'px'; + select.addEventListener('change', () => { + filterListSelectedValues = select.value; + tableInstance.updateFilterRules([ + { + filterKey: field, + filteredValues: select.value + } + ]); + removeFilterElement(); + }); + } + function removeFilterElement() { + filterContainer.removeChild(select); + select.removeEventListener('change', () => { + // this.successCallback(); + }); + select = null; + } + }); +} diff --git a/packages/vtable/examples/list/list-chart.ts b/packages/vtable/examples/list/list-chart.ts index f70740210..4e28ef3a7 100644 --- a/packages/vtable/examples/list/list-chart.ts +++ b/packages/vtable/examples/list/list-chart.ts @@ -4,10 +4,10 @@ import VChart from '@visactor/vchart'; VTable.register.chartModule('vchart', VChart); const CONTAINER_ID = 'vTable'; export function createTable() { - const columns = [ + const columns: VTable.ColumnsDefine = [ { - field: 'personid', - title: 'personid', + field: 'id', + title: 'id', description: '这是一个标题的详细描述', sort: true, width: 80, @@ -18,83 +18,160 @@ export function createTable() { }, { field: 'areaChart', - title: 'vchart area', + title: 'multiple vchart type', width: '320', cellType: 'chart', chartModule: 'vchart', - chartSpec: { - type: 'area', - data: { - id: 'data' - }, - xField: 'x', - yField: 'y', - seriesField: 'type', - point: { - style: { - fillOpacity: 1, - stroke: '#000', - strokeWidth: 4 - }, - state: { - hover: { - fillOpacity: 0.5, - stroke: 'blue', - strokeWidth: 2 + chartSpec(args) { + if (args.row % 3 == 2) + return { + type: 'area', + data: { + id: 'data' }, - selected: { - fill: 'red' - } - } - }, - area: { - style: { - fillOpacity: 0.3, - stroke: '#000', - strokeWidth: 4 - }, - state: { - hover: { - fillOpacity: 1 + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } }, - selected: { - fill: 'red', - fillOpacity: 1 - } - } - }, - line: { - state: { - hover: { - stroke: 'red' + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } }, - selected: { - stroke: 'yellow' - } - } - }, - - axes: [ - { - orient: 'left', - range: { - min: 0 - } - }, - { - orient: 'bottom', - label: { - visible: true + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } + } }, - type: 'band' - } - ], - legends: [ - { - visible: true, - orient: 'bottom' - } - ] + + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + else if (args.row % 3 == 1) + return { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + }, + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'bottom' + } + ] + }; + return { + type: 'pie', + data: { id: 'data1' }, + categoryField: 'y', + valueField: 'x' + }; } }, { @@ -173,7 +250,7 @@ export function createTable() { }, { field: 'barChart', - title: 'vchart line', + title: 'vchart bar', width: '320', cellType: 'chart', chartModule: 'vchart', @@ -222,7 +299,7 @@ export function createTable() { }, { field: 'scatterChart', - title: 'vchart line', + title: 'vchart scatter', width: '320', cellType: 'chart', chartModule: 'vchart', @@ -340,171 +417,12 @@ export function createTable() { } ] } - }, - { - field: 'lineChart', - title: 'vchart line', - width: '320', - cellType: 'chart', - chartModule: 'vchart', - chartSpec: { - type: 'common', - series: [ - { - type: 'line', - data: { - id: 'data' - }, - xField: 'x', - yField: 'y', - seriesField: 'type', - line: { - state: { - hover: { - strokeWidth: 4 - }, - selected: { - stroke: 'red' - }, - hover_reverse: { - stroke: '#ddd' - } - } - }, - point: { - state: { - hover: { - fill: 'red' - }, - selected: { - fill: 'yellow' - }, - hover_reverse: { - fill: '#ddd' - } - } - }, - legends: [ - { - visible: true, - orient: 'bottom' - } - ] - } - ], - axes: [ - { - orient: 'left', - range: { - min: 0 - } - }, - { - orient: 'bottom', - label: { - visible: true - }, - type: 'band' - } - ], - legends: [ - { - visible: true, - orient: 'bottom' - } - ] - } - }, - { - field: 'barChart', - title: 'vchart line', - width: '320', - cellType: 'chart', - chartModule: 'vchart', - chartSpec: { - type: 'common', - series: [ - { - type: 'bar', - data: { - id: 'data' - }, - xField: 'x', - yField: 'y', - seriesField: 'type', - bar: { - state: { - hover: { - fill: 'green' - }, - selected: { - fill: 'orange' - }, - hover_reverse: { - fill: '#ccc' - } - } - } - } - ], - axes: [ - { - orient: 'left', - range: { - min: 0 - } - }, - { - orient: 'bottom', - label: { - visible: true - }, - type: 'band' - } - ] - } - }, - { - field: 'scatterChart', - title: 'vchart line', - width: '320', - cellType: 'chart', - chartModule: 'vchart', - chartSpec: { - type: 'common', - series: [ - { - type: 'scatter', - data: { - id: 'data' - }, - xField: 'x', - yField: 'y', - seriesField: 'type' - } - ], - axes: [ - { - orient: 'left', - range: { - min: 0 - } - }, - { - orient: 'bottom', - label: { - visible: true - }, - type: 'band' - } - ] - } } ]; const records: any[] = []; // = generatePersonsDataSource(10); - for (let i = 1; i <= 40; i++) + for (let i = 1; i <= 10; i++) records.push({ - personid: i, + id: i, areaChart: [ { x: '0', type: 'A', y: 100 * i }, { x: '1', type: 'A', y: '707' }, diff --git a/packages/vtable/examples/list/list-tree.ts b/packages/vtable/examples/list/list-tree.ts index 019880993..2cd4a05e3 100644 --- a/packages/vtable/examples/list/list-tree.ts +++ b/packages/vtable/examples/list/list-tree.ts @@ -165,7 +165,7 @@ export function createTable() { sort: true } ], - showPin: true, //显示VTable内置冻结列图标 + showFrozenIcon: true, //显示VTable内置冻结列图标 widthMode: 'standard', autoFillHeight: true, // heightMode: 'adaptive', diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 705b73934..819c1eb09 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -97,6 +97,31 @@ export const menus = [ } ] }, + { + menu: '基本表格分析', + children: [ + { + path: 'list-analysis', + name: 'list-filter' + }, + { + path: 'list-analysis', + name: 'list-aggregation' + }, + { + path: 'list-analysis', + name: 'list-aggregation-global' + }, + { + path: 'list-analysis', + name: 'list-aggregation-edit' + }, + { + path: 'list-analysis', + name: 'olympic-winners' + } + ] + }, { menu: '透视表', children: [ diff --git a/packages/vtable/examples/pivot-analysis/pivot-analysis-aggregationRules.ts b/packages/vtable/examples/pivot-analysis/pivot-analysis-aggregationRules.ts index b5ad07791..448703cc2 100644 --- a/packages/vtable/examples/pivot-analysis/pivot-analysis-aggregationRules.ts +++ b/packages/vtable/examples/pivot-analysis/pivot-analysis-aggregationRules.ts @@ -24,7 +24,9 @@ export function createTable() { indicatorKey: '销售总额', //指标名称 field: 'sales', //指标依据字段 aggregationType: VTable.TYPES.AggregationType.SUM, //计算类型 - formatFun: sumNumberFormat + formatFun(value, col, row, table) { + return value; + } }, { indicatorKey: '订单数', //指标名称 diff --git a/packages/vtable/examples/pivot-analysis/pivot-analysis-filter.ts b/packages/vtable/examples/pivot-analysis/pivot-analysis-filter.ts index 69e0a1307..fb48db0b5 100644 --- a/packages/vtable/examples/pivot-analysis/pivot-analysis-filter.ts +++ b/packages/vtable/examples/pivot-analysis/pivot-analysis-filter.ts @@ -27,7 +27,6 @@ export function createTable() { rows: ['province', 'city'], columns: ['category', 'sub_category'], indicators: ['sales', 'number'], - enableDataAnalysis: true, indicatorTitle: '指标名称', indicatorsAsCol: false, dataConfig: { diff --git a/packages/vtable/examples/style/border.ts b/packages/vtable/examples/style/border.ts index f1db2a667..2177184d0 100644 --- a/packages/vtable/examples/style/border.ts +++ b/packages/vtable/examples/style/border.ts @@ -311,8 +311,8 @@ export function createTable() { menu: { renderMode: 'html', defaultHeaderMenuItems: ['升序排序', '降序排序', '冻结列'], - contextMenuItems: (field: string, row: number) => { - console.log(field, row); + contextMenuItems: (field: string, row: number, col: number) => { + console.log(field, row, col); return [ { text: '复制表头', menuKey: '复制表头$1' }, { text: '复制单元格', menuKey: '复制单元格$1' } diff --git a/packages/vtable/examples/type/chart.ts b/packages/vtable/examples/type/chart.ts index 43280054b..89a5ab79d 100644 --- a/packages/vtable/examples/type/chart.ts +++ b/packages/vtable/examples/type/chart.ts @@ -439,30 +439,120 @@ export function createTable() { cellType: 'chart', chartModule: 'vchart', width: 500, - chartSpec: { - type: 'common', - series: [ - { - type: 'line', - data: { - id: 'data', - transforms: [ - { - type: 'fold', - options: { - key: 'x', // 转化后,原始数据的 key 放入这个配置对应的字段中作为值 - value: 'y', // 转化后,原始数据的 value 放入这个配置对应的字段中作为值 - fields: Object.keys(temperatureList[rowTree[0].value].day) // 需要转化的维度 + chartSpec: args => { + if (args.row % 2 === 0) { + return { + type: 'common', + series: [ + { + type: 'area', + data: { + id: 'data', + transforms: [ + { + type: 'fold', + options: { + key: 'x', // 转化后,原始数据的 key 放入这个配置对应的字段中作为值 + value: 'y', // 转化后,原始数据的 value 放入这个配置对应的字段中作为值 + fields: Object.keys(temperatureList[rowTree[0].value].month) // 需要转化的维度 + } + } + ] + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fillOpacity: 1, + strokeWidth: 0 + }, + state: { + hover: { + fillOpacity: 0.5, + stroke: 'blue', + strokeWidth: 2 + }, + selected: { + fill: 'red' + } + } + }, + area: { + style: { + fillOpacity: 0.3, + stroke: '#000', + strokeWidth: 4 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: 'red', + fillOpacity: 1 + } + } + }, + line: { + state: { + hover: { + stroke: 'red' + }, + selected: { + stroke: 'yellow' + } } } - ] - }, - xField: 'x', - yField: 'y', - seriesField: 'type' - } - ], - axes: [{ orient: 'left' }, { orient: 'bottom', label: { visible: true } }] + } + ], + axes: [{ orient: 'left' }, { orient: 'bottom', label: { visible: true } }], + + markLine: [ + { + y: 0, + line: { + // 配置线样式 + style: { + lineWidth: 1, + stroke: 'black', + lineDash: [5, 5] + } + }, + endSymbol: { + style: { + visible: false + } + } + } + ] + }; + } + return { + type: 'common', + series: [ + { + type: 'line', + data: { + id: 'data', + transforms: [ + { + type: 'fold', + options: { + key: 'x', // 转化后,原始数据的 key 放入这个配置对应的字段中作为值 + value: 'y', // 转化后,原始数据的 value 放入这个配置对应的字段中作为值 + fields: Object.keys(temperatureList[rowTree[0].value].day) // 需要转化的维度 + } + } + ] + }, + xField: 'x', + yField: 'y', + seriesField: 'type' + } + ], + axes: [{ orient: 'left' }, { orient: 'bottom', label: { visible: true } }] + }; } }, { diff --git a/packages/vtable/package.json b/packages/vtable/package.json index 13febc158..81a9493b1 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable", - "version": "0.19.1", + "version": "0.20.1", "description": "canvas table width high performance", "keywords": [ "grid", diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 8242a66c5..e774b002e 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -1,4 +1,5 @@ import type { + AggregationType, CellAddress, CellRange, ColumnsDefine, @@ -7,6 +8,7 @@ import type { FieldDef, FieldFormat, FieldKeyDef, + FilterRules, IPagination, ListTableAPI, ListTableConstructorOptions, @@ -19,7 +21,7 @@ import { SimpleHeaderLayoutMap } from './layout'; import { isValid } from '@visactor/vutils'; import { _setDataSource, _setRecords, sortRecords } from './core/tableHelper'; import { BaseTable } from './core'; -import type { ListTableProtected } from './ts-types/base-table'; +import type { BaseTableAPI, ListTableProtected } from './ts-types/base-table'; import { TABLE_EVENT_TYPE } from './core/TABLE_EVENT_TYPE'; import { Title } from './components/title/title'; import { cloneDeep } from '@visactor/vutils'; @@ -31,6 +33,7 @@ import { computeColWidth } from './scenegraph/layout/compute-col-width'; import { computeRowHeight } from './scenegraph/layout/compute-row-height'; import { defaultOrderFn } from './tools/util'; import type { IEditor } from '@visactor/vtable-editors'; +import type { ColumnData } from './ts-types/list-table/layout-map/api'; export class ListTable extends BaseTable implements ListTableAPI { declare internalProps: ListTableProtected; @@ -62,6 +65,7 @@ export class ListTable extends BaseTable implements ListTableAPI { //分页配置 this.pagination = options.pagination; internalProps.sortState = options.sortState; + internalProps.dataConfig = {}; //cloneDeep(options.dataConfig ?? {}); internalProps.columns = options.columns ? cloneDeep(options.columns) : options.header @@ -85,7 +89,7 @@ export class ListTable extends BaseTable implements ListTableAPI { if (options.dataSource) { _setDataSource(this, options.dataSource); } else if (options.records) { - this.setRecords(options.records as any, internalProps.sortState); + this.setRecords(options.records as any, { sortState: internalProps.sortState }); } else { this.setRecords([]); } @@ -113,6 +117,14 @@ export class ListTable extends BaseTable implements ListTableAPI { get sortState(): SortState | SortState[] { return this.internalProps.sortState; } + + get records() { + return this.dataSource?.source; + } + + get recordsCount() { + return this.dataSource.source.length; + } // /** // * Gets the define of the header. // */ @@ -206,6 +218,14 @@ export class ListTable extends BaseTable implements ListTableAPI { if (table.internalProps.layoutMap.isHeader(col, row)) { const { title } = table.internalProps.layoutMap.getHeader(col, row); return typeof title === 'function' ? title() : title; + } else if (table.internalProps.layoutMap.isAggregation(col, row)) { + if (table.internalProps.layoutMap.isTopAggregation(col, row)) { + const aggregator = table.internalProps.layoutMap.getAggregatorOnTop(col, row); + return aggregator?.formatValue ? aggregator.formatValue(col, row, this as BaseTableAPI) : ''; + } else if (table.internalProps.layoutMap.isBottomAggregation(col, row)) { + const aggregator = table.internalProps.layoutMap.getAggregatorOnBottom(col, row); + return aggregator?.formatValue ? aggregator.formatValue(col, row, this as BaseTableAPI) : ''; + } } const { field, fieldFormat } = table.internalProps.layoutMap.getBody(col, row); return table.getFieldData(fieldFormat || field, col, row); @@ -219,6 +239,14 @@ export class ListTable extends BaseTable implements ListTableAPI { if (table.internalProps.layoutMap.isHeader(col, row)) { const { title } = table.internalProps.layoutMap.getHeader(col, row); return typeof title === 'function' ? title() : title; + } else if (table.internalProps.layoutMap.isAggregation(col, row)) { + if (table.internalProps.layoutMap.isTopAggregation(col, row)) { + const aggregator = table.internalProps.layoutMap.getAggregatorOnTop(col, row); + return aggregator?.value(); + } else if (table.internalProps.layoutMap.isBottomAggregation(col, row)) { + const aggregator = table.internalProps.layoutMap.getAggregatorOnBottom(col, row); + return aggregator?.value(); + } } const { field } = table.internalProps.layoutMap.getBody(col, row); return table.getFieldData(field, col, row); @@ -239,10 +267,19 @@ export class ListTable extends BaseTable implements ListTableAPI { /** 获取当前单元格在body部分的展示索引 即(row / col)-headerLevelCount。注:ListTable特有接口 */ getRecordShowIndexByCell(col: number, row: number): number { const { layoutMap } = this.internalProps; - return layoutMap.getRecordIndexByCell(col, row); + return layoutMap.getRecordShowIndexByCell(col, row); } - getTableIndexByRecordIndex(recordIndex: number) { + /** 获取当前单元格的数据是数据源中的第几条。 + * 如果是树形模式的表格,将返回数组,如[1,2] 数据源中第2条数据中children中的第3条 + * 注:ListTable特有接口 */ + getRecordIndexByCell(col: number, row: number): number | number[] { + const { layoutMap } = this.internalProps; + const recordShowIndex = layoutMap.getRecordShowIndexByCell(col, row); + return this.dataSource.currentPagerIndexedData[recordShowIndex]; + } + + getTableIndexByRecordIndex(recordIndex: number | number[]) { if (this.transpose) { return this.dataSource.getTableIndex(recordIndex) + this.rowHeaderLevelCount; } @@ -314,12 +351,13 @@ export class ListTable extends BaseTable implements ListTableAPI { } return ifCan; } - updateOption(options: ListTableConstructorOptions, accelerateFirstScreen = false) { + updateOption(options: ListTableConstructorOptions & { restoreHierarchyState?: boolean }) { const internalProps = this.internalProps; super.updateOption(options); internalProps.frozenColDragHeaderMode = options.frozenColDragHeaderMode; //分页配置 this.pagination = options.pagination; + internalProps.dataConfig = {}; // cloneDeep(options.dataConfig ?? {}); //更新protectedSpace this.showHeader = options.showHeader ?? true; internalProps.columns = options.columns @@ -350,7 +388,10 @@ export class ListTable extends BaseTable implements ListTableAPI { if (options.dataSource) { _setDataSource(this, options.dataSource); } else if (options.records) { - this.setRecords(options.records as any, options.sortState); + this.setRecords(options.records as any, { + restoreHierarchyState: options.restoreHierarchyState, + sortState: options.sortState + }); } else { this._resetFrozenColCount(); // 生成单元格场景树 @@ -427,11 +468,14 @@ export class ListTable extends BaseTable implements ListTableAPI { if (!layoutMap) { return; } - layoutMap.recordsCount = table.internalProps.dataSource?.length ?? 0; + layoutMap.recordsCount = + (table.internalProps.dataSource?.length ?? 0) + + layoutMap.hasAggregationOnTopCount + + layoutMap.hasAggregationOnBottomCount; + if (table.transpose) { table.rowCount = layoutMap.rowCount ?? 0; - table.colCount = - (table.internalProps.dataSource?.length ?? 0) * layoutMap.bodyRowSpanCount + layoutMap.headerLevelCount; + table.colCount = layoutMap.recordsCount * layoutMap.bodyRowSpanCount + layoutMap.headerLevelCount; table.frozenRowCount = 0; // table.frozenColCount = layoutMap.headerLevelCount; //这里不要这样写 这个setter会检查扁头宽度 可能将frozenColCount置为0 this.internalProps.frozenColCount = layoutMap.headerLevelCount ?? 0; @@ -443,8 +487,7 @@ export class ListTable extends BaseTable implements ListTableAPI { } } else { table.colCount = layoutMap.colCount ?? 0; - table.rowCount = - (table.internalProps.dataSource?.length ?? 0) * layoutMap.bodyRowSpanCount + layoutMap.headerLevelCount; + table.rowCount = layoutMap.recordsCount * layoutMap.bodyRowSpanCount + layoutMap.headerLevelCount; // table.frozenColCount = table.options.frozenColCount ?? 0; //这里不要这样写 这个setter会检查扁头宽度 可能将frozenColCount置为0 this.internalProps.frozenColCount = this.options.frozenColCount ?? 0; table.frozenRowCount = layoutMap.headerLevelCount; @@ -704,7 +747,8 @@ export class ListTable extends BaseTable implements ListTableAPI { const result: DropDownMenuEventInfo = { field: this.getHeaderField(col, row), value: this.getCellValue(col, row), - cellLocation: this.getCellLocation(col, row) + cellLocation: this.getCellLocation(col, row), + event: undefined }; return result; } @@ -760,20 +804,15 @@ export class ListTable extends BaseTable implements ListTableAPI { } let order: any; let field: any; - let fieldKey: any; if (Array.isArray(this.internalProps.sortState)) { - ({ order, field, fieldKey } = this.internalProps.sortState?.[0]); + ({ order, field } = this.internalProps.sortState?.[0]); } else { - ({ order, field, fieldKey } = this.internalProps.sortState as SortState); + ({ order, field } = this.internalProps.sortState as SortState); } if (field && executeSort) { - const sortFunc = this._getSortFuncFromHeaderOption(this.internalProps.columns, field, fieldKey); - let hd; - if (fieldKey) { - hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.fieldKey === fieldKey); - } else { - hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); - } + const sortFunc = this._getSortFuncFromHeaderOption(this.internalProps.columns, field); + const hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); + if (hd.define.sort !== false) { this.dataSource.sort(hd.field, order, sortFunc); @@ -784,6 +823,18 @@ export class ListTable extends BaseTable implements ListTableAPI { } this.stateManager.updateSortState(sortState as SortState); } + updateFilterRules(filterRules: FilterRules) { + this.scenegraph.clearCells(); + this.internalProps.dataConfig.filterRules = filterRules; + if (this.sortState) { + this.dataSource.updateFilterRulesForSorted(filterRules); + sortRecords(this); + } else { + this.dataSource.updateFilterRules(filterRules); + } + this.refreshRowColCount(); + this.scenegraph.createSceneGraph(); + } /** 获取某个字段下checkbox 全部数据的选中状态 顺序对应原始传入数据records 不是对应表格展示row的状态值 */ getCheckboxState(field?: string | number) { if (this.stateManager.checkedState.length < this.rowCount - this.columnHeaderLevelCount) { @@ -812,7 +863,17 @@ export class ListTable extends BaseTable implements ListTableAPI { * @param records * @param sort */ - setRecords(records: Array, sort?: SortState | SortState[]): void { + setRecords( + records: Array, + option?: { restoreHierarchyState?: boolean; sortState?: SortState | SortState[] } + ): void { + let sort: SortState | SortState[]; + if (Array.isArray(option) || (option as any)?.order) { + //兼容之前第二个参数为sort的情况 + sort = option; + } else { + sort = option?.sortState; + } const time = typeof window !== 'undefined' ? window.performance.now() : 0; const oldHoverState = { col: this.stateManager.hover.cellPos.col, row: this.stateManager.hover.cellPos.row }; // 清空单元格内容 @@ -823,39 +884,51 @@ export class ListTable extends BaseTable implements ListTableAPI { this.internalProps.sortState = sort; this.stateManager.setSortState((this as any).sortState as SortState); } + // restoreHierarchyState逻辑,保留树形结构展开收起的状态 + const currentPagerIndexedData = this.dataSource?._currentPagerIndexedData; + const currentIndexedData = this.dataSource?.currentIndexedData; + const treeDataHierarchyState = this.dataSource?.treeDataHierarchyState; + const oldRecordLength = this.records?.length ?? 0; if (records) { _setRecords(this, records); if ((this as any).sortState) { let order: any; let field: any; - let fieldKey: any; if (Array.isArray((this as any).sortState)) { if ((this as any).sortState.length !== 0) { - ({ order, field, fieldKey } = (this as any).sortState?.[0]); + ({ order, field } = (this as any).sortState?.[0]); } } else { - ({ order, field, fieldKey } = (this as any).sortState as SortState); + ({ order, field } = (this as any).sortState as SortState); } // 根据sort规则进行排序 if (order && field && order !== 'normal') { - const sortFunc = this._getSortFuncFromHeaderOption(undefined, field, fieldKey); + const sortFunc = this._getSortFuncFromHeaderOption(undefined, field); // 如果sort传入的信息不能生成正确的sortFunc,直接更新表格,避免首次加载无法正常显示内容 - let hd; - if (fieldKey) { - hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.fieldKey === fieldKey); - } else { - hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); - } + const hd = this.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); // hd?.define?.sort && //如果这里也判断 那想要利用sortState来排序 但不显示排序图标就实现不了 if (hd.define.sort !== false) { this.dataSource.sort(hd.field, order, sortFunc ?? defaultOrderFn); } } } + if (option?.restoreHierarchyState && oldRecordLength === this.records?.length) { + // restoreHierarchyState逻辑,保留树形结构展开收起的状态 + this.dataSource._currentPagerIndexedData = currentPagerIndexedData; + this.dataSource.currentIndexedData = currentIndexedData; + this.dataSource.treeDataHierarchyState = treeDataHierarchyState; + } this.refreshRowColCount(); } else { _setRecords(this, records); + if (option?.restoreHierarchyState && oldRecordLength === this.records?.length) { + // restoreHierarchyState逻辑,保留树形结构展开收起的状态 + this.dataSource._currentPagerIndexedData = currentPagerIndexedData; + this.dataSource.currentIndexedData = currentIndexedData; + this.dataSource.treeDataHierarchyState = treeDataHierarchyState; + } } + this.stateManager.initCheckedState(records); // this.internalProps.frozenColCount = this.options.frozenColCount || this.rowHeaderLevelCount; // 生成单元格场景树 @@ -931,6 +1004,27 @@ export class ListTable extends BaseTable implements ListTableAPI { } else { this.dataSource.changeFieldValue(value, recordIndex, field, col, row, this); } + //改变单元格的值后 聚合值做重新计算 + const aggregators = this.internalProps.layoutMap.getAggregators(col, row); + if (aggregators) { + if (Array.isArray(aggregators)) { + for (let i = 0; i < aggregators?.length; i++) { + aggregators[i].recalculate(); + } + } else { + aggregators.recalculate(); + } + const aggregatorCells = this.internalProps.layoutMap.getCellAddressHasAggregator(col, row); + for (let i = 0; i < aggregatorCells.length; i++) { + const range = this.getCellRange(aggregatorCells[i].col, aggregatorCells[i].row); + for (let sCol = range.start.col; sCol <= range.end.col; sCol++) { + for (let sRow = range.start.row; sRow <= range.end.row; sRow++) { + this.scenegraph.updateCellContent(sCol, sRow); + } + } + } + } + // const cell_value = this.getCellValue(col, row); const range = this.getCellRange(col, row); for (let sCol = range.start.col; sCol <= range.end.col; sCol++) { @@ -1381,7 +1475,7 @@ export class ListTable extends BaseTable implements ListTableAPI { } } - hasCustomRenderOrLayout() { + _hasCustomRenderOrLayout() { const { headerObjects } = this.internalProps.layoutMap; if (this.options.customRender) { return true; @@ -1400,4 +1494,49 @@ export class ListTable extends BaseTable implements ListTableAPI { } return false; } + /** + * 根据字段获取聚合值 + * @param field 字段名 + * 返回数组,包括列号和每一列的聚合值数组 + */ + getAggregateValuesByField(field: string | number): { + col: number; + aggregateValue: { aggregationType: AggregationType; value: number | string }[]; + }[] { + const columns = this.internalProps.layoutMap.getColumnByField(field); + const results: { + col: number; + aggregateValue: { aggregationType: AggregationType; value: number | string }[]; + }[] = []; + for (let i = 0; i < columns.length; i++) { + const aggregator = columns[i].columnDefine.aggregator; + delete columns[i].columnDefine; + if (aggregator) { + const columnAggregateValue: { + col: number; + aggregateValue: { aggregationType: AggregationType; value: number | string }[]; + } = { + col: columns[i].col, + aggregateValue: null + }; + columnAggregateValue.aggregateValue = []; + if (Array.isArray(aggregator)) { + for (let j = 0; j < aggregator.length; j++) { + columnAggregateValue.aggregateValue.push({ + aggregationType: aggregator[j].type as AggregationType, + value: aggregator[j].value() + }); + } + } else { + columnAggregateValue.aggregateValue.push({ + aggregationType: aggregator.type as AggregationType, + value: aggregator.value() + }); + } + + results.push(columnAggregateValue); + } + } + return results; + } } diff --git a/packages/vtable/src/PivotChart.ts b/packages/vtable/src/PivotChart.ts index d4ec76945..0d6781154 100644 --- a/packages/vtable/src/PivotChart.ts +++ b/packages/vtable/src/PivotChart.ts @@ -150,6 +150,9 @@ export class PivotChart extends BaseTable implements PivotChartAPI { get pivotChartAxes() { return this._axes; } + get recordsCount() { + return this.records?.length; + } isListTable(): false { return false; @@ -173,7 +176,7 @@ export class PivotChart extends BaseTable implements PivotChartAPI { } return ifCan; } - updateOption(options: PivotChartConstructorOptions, accelerateFirstScreen = false) { + updateOption(options: PivotChartConstructorOptions) { const internalProps = this.internalProps; //维护选中状态 // const range = internalProps.selection.range; //保留原有单元格选中状态 @@ -719,7 +722,8 @@ export class PivotChart extends BaseTable implements PivotChartAPI { dimensionKey: dimensionInfos[dimensionInfos.length - 1].dimensionKey, value: this.getCellValue(col, row), cellLocation: this.getCellLocation(col, row), - isPivotCorner: this.isCornerHeader(col, row) + isPivotCorner: this.isCornerHeader(col, row), + event: undefined }; return result; } @@ -1221,7 +1225,7 @@ export class PivotChart extends BaseTable implements PivotChartAPI { this.eventManager.updateEventBinder(); } - hasCustomRenderOrLayout() { + _hasCustomRenderOrLayout() { if (this.options.customRender) { return true; } diff --git a/packages/vtable/src/PivotTable.ts b/packages/vtable/src/PivotTable.ts index f6806ddc4..69114ce9f 100644 --- a/packages/vtable/src/PivotTable.ts +++ b/packages/vtable/src/PivotTable.ts @@ -28,7 +28,7 @@ import type { BaseTableAPI, PivotTableProtected } from './ts-types/base-table'; import { Title } from './components/title/title'; import { cloneDeep } from '@visactor/vutils'; import { Env } from './tools/env'; -import type { LayouTreeNode } from './layout/layout-helper'; +import type { LayouTreeNode } from './layout/tree-helper'; import { TABLE_EVENT_TYPE } from './core/TABLE_EVENT_TYPE'; import { EditManeger } from './edit/edit-manager'; import * as editors from './edit/editors'; @@ -84,9 +84,11 @@ export class PivotTable extends BaseTable implements PivotTableAPI { this.internalProps.columnResizeType = options.columnResizeType ?? 'column'; this.internalProps.dataConfig = cloneDeep(options.dataConfig); - this.internalProps.enableDataAnalysis = options.enableDataAnalysis; + // this.internalProps.enableDataAnalysis = options.enableDataAnalysis; if (!options.rowTree && !options.columnTree) { this.internalProps.enableDataAnalysis = true; + } else { + this.internalProps.enableDataAnalysis = false; } const records = this.internalProps.records; if (this.internalProps.enableDataAnalysis && (options.rows || options.columns)) { @@ -188,6 +190,9 @@ export class PivotTable extends BaseTable implements PivotTableAPI { isPivotChart(): false { return false; } + get recordsCount() { + return this.records?.length; + } _canResizeColumn(col: number, row: number): boolean { const ifCan = super._canResizeColumn(col, row); if (ifCan) { @@ -201,7 +206,7 @@ export class PivotTable extends BaseTable implements PivotTableAPI { } return ifCan; } - updateOption(options: PivotTableConstructorOptions, accelerateFirstScreen = false) { + updateOption(options: PivotTableConstructorOptions) { const internalProps = this.internalProps; //维护选中状态 // const range = internalProps.selection.range; //保留原有单元格选中状态 @@ -226,9 +231,11 @@ export class PivotTable extends BaseTable implements PivotTableAPI { // 更新protectedSpace internalProps.columnResizeType = options.columnResizeType ?? 'column'; internalProps.dataConfig = cloneDeep(options.dataConfig); - internalProps.enableDataAnalysis = options.enableDataAnalysis; + // internalProps.enableDataAnalysis = options.enableDataAnalysis; if (!options.rowTree && !options.columnTree) { internalProps.enableDataAnalysis = true; + } else { + internalProps.enableDataAnalysis = false; } //维护tree树形结构的展开状态 if ( @@ -947,6 +954,16 @@ export class PivotTable extends BaseTable implements PivotTableAPI { getHierarchyState(col: number, row: number): HierarchyState { return this._getHeaderLayoutMap(col, row)?.hierarchyState; } + /** 获取列头树结构 */ + getLayoutColumnTree(): LayouTreeNode[] { + const layoutMap = this.internalProps.layoutMap; + return layoutMap.getLayoutColumnTree(); + } + /** 获取表格列头树形结构的占位的总节点数 */ + getLayoutColumnTreeCount(): number { + const layoutMap = this.internalProps.layoutMap; + return layoutMap.getLayoutColumnTreeCount(); + } /** 获取行头树结构 */ getLayoutRowTree(): LayouTreeNode[] { const layoutMap = this.internalProps.layoutMap; @@ -978,7 +995,8 @@ export class PivotTable extends BaseTable implements PivotTableAPI { dimensionKey: dimensionInfos[dimensionInfos.length - 1].dimensionKey, value: this.getCellValue(col, row), cellLocation: this.getCellLocation(col, row), - isPivotCorner: this.isCornerHeader(col, row) + isPivotCorner: this.isCornerHeader(col, row), + event: undefined }; return result; } @@ -1218,7 +1236,7 @@ export class PivotTable extends BaseTable implements PivotTableAPI { this.records[rowIndex][colIndex] = newValue; } } - hasCustomRenderOrLayout() { + _hasCustomRenderOrLayout() { if (this.options.customRender) { return true; } diff --git a/packages/vtable/src/components/axis/axis.ts b/packages/vtable/src/components/axis/axis.ts index 064e7098e..ddca94e1e 100644 --- a/packages/vtable/src/components/axis/axis.ts +++ b/packages/vtable/src/components/axis/axis.ts @@ -57,14 +57,18 @@ export class CartesianAxis { ); if (this.orient === 'left' || this.orient === 'right') { - const innerOffsetTop = this.option.innerOffset?.top ?? 0; - const innerOffsetBottom = this.option.innerOffset?.bottom ?? 0; + // const innerOffsetTop = this.option.innerOffset?.top ?? 0; + // const innerOffsetBottom = this.option.innerOffset?.bottom ?? 0; + const innerOffsetTop = 0; + const innerOffsetBottom = 0; this.width = width; this.height = height - padding[2] - innerOffsetBottom; this.y = padding[0] + innerOffsetTop; } else if (this.orient === 'top' || this.orient === 'bottom') { - const innerOffsetLeft = this.option.innerOffset?.left ?? 0; - const innerOffsetRight = this.option.innerOffset?.right ?? 0; + // const innerOffsetLeft = this.option.innerOffset?.left ?? 0; + // const innerOffsetRight = this.option.innerOffset?.right ?? 0; + const innerOffsetLeft = 0; + const innerOffsetRight = 0; this.width = width - padding[1] - innerOffsetRight; this.height = height; this.x = padding[3] + innerOffsetLeft; @@ -247,16 +251,23 @@ export class CartesianAxis { } updateScaleRange() { + const right = this.option.innerOffset?.right ?? 0; + const left = this.option.innerOffset?.left ?? 0; + const top = this.option.innerOffset?.top ?? 0; + const bottom = this.option.innerOffset?.bottom ?? 0; + const { width, height } = this.getLayoutRect(); const inverse = (this.option as any).inverse || false; let newRange: [number, number] = [0, 0]; if (isXAxis(this.orient)) { if (isValidNumber(width)) { - newRange = inverse ? [width, 0] : [0, width]; + // newRange = inverse ? [width, 0] : [0, width]; + newRange = inverse ? [width - right, left] : [left, width - right]; } } else { if (isValidNumber(height)) { - newRange = inverse ? [0, height] : [height, 0]; + // newRange = inverse ? [0, height] : [height, 0]; + newRange = inverse ? [top, height - bottom] : [height - bottom, top]; } } diff --git a/packages/vtable/src/components/menu/dom/MenuHandler.ts b/packages/vtable/src/components/menu/dom/MenuHandler.ts index bcf50a26f..f720a7488 100644 --- a/packages/vtable/src/components/menu/dom/MenuHandler.ts +++ b/packages/vtable/src/components/menu/dom/MenuHandler.ts @@ -203,7 +203,7 @@ export class MenuHandler { const abstractPos = table._getMouseAbstractPoint(e.event, false); let menu = null; if (abstractPos.inTable && typeof table.internalProps.menu?.contextMenuItems === 'function') { - menu = table.internalProps.menu.contextMenuItems(table.getHeaderField(e.col, e.row) as string, e.row); + menu = table.internalProps.menu.contextMenuItems(table.getHeaderField(e.col, e.row) as string, e.row, e.col); } else if (abstractPos.inTable && Array.isArray(table.internalProps.menu?.contextMenuItems)) { menu = table.internalProps.menu?.contextMenuItems; } diff --git a/packages/vtable/src/components/menu/dom/logic/MenuContainer.ts b/packages/vtable/src/components/menu/dom/logic/MenuContainer.ts index 62306bbee..b7b152d88 100644 --- a/packages/vtable/src/components/menu/dom/logic/MenuContainer.ts +++ b/packages/vtable/src/components/menu/dom/logic/MenuContainer.ts @@ -59,7 +59,8 @@ export class MenuContainer { // dropDownIndex, text, highlight, - cellLocation: table.getCellLocation(col, row) + cellLocation: table.getCellLocation(col, row), + event: e }); table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLEAR, null); // 清除菜单 diff --git a/packages/vtable/src/components/menu/dom/logic/MenuElement.ts b/packages/vtable/src/components/menu/dom/logic/MenuElement.ts index 6e82511c2..682c78101 100644 --- a/packages/vtable/src/components/menu/dom/logic/MenuElement.ts +++ b/packages/vtable/src/components/menu/dom/logic/MenuElement.ts @@ -139,7 +139,8 @@ export class MenuElement { // dropDownIndex, text, highlight, - cellLocation: table.getCellLocation(col, row) + cellLocation: table.getCellLocation(col, row), + event: e }); table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLEAR, null); // 清除菜单 @@ -175,7 +176,8 @@ export class MenuElement { // dropDownIndex, text, highlight, - cellLocation: table.getCellLocation(col, row) + cellLocation: table.getCellLocation(col, row), + event: e }); table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLEAR, null); // 清除菜单 @@ -321,7 +323,8 @@ export class MenuElement { menuKey, text, highlight, - cellLocation: table.getCellLocation(col, row) + cellLocation: table.getCellLocation(col, row), + event: e }); table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLEAR, null); // 清除菜单 diff --git a/packages/vtable/src/components/tooltip/TooltipHandler.ts b/packages/vtable/src/components/tooltip/TooltipHandler.ts index a21d32a45..d8a99022d 100644 --- a/packages/vtable/src/components/tooltip/TooltipHandler.ts +++ b/packages/vtable/src/components/tooltip/TooltipHandler.ts @@ -204,13 +204,14 @@ export class TooltipHandler { this._unbindFromCell(); }); table.on(TABLE_EVENT_TYPE.SCROLL, e => { - const info = this._attachInfo; - if (info?.tooltipOptions && info?.range?.start) { - const { col, row } = info.range.start; - const rect = table.getCellRangeRelativeRect({ col, row }); - info.tooltipOptions.referencePosition.rect = rect; - this._move(info.range.start.col, info.range.start.row, info.tooltipOptions); - } + this._unbindFromCell(); + // const info = this._attachInfo; + // if (info?.tooltipOptions && info?.range?.start) { + // const { col, row } = info.range.start; + // const rect = table.getCellRangeRelativeRect({ col, row }); + // info.tooltipOptions.referencePosition.rect = rect; + // this._move(info.range.start.col, info.range.start.row, info.tooltipOptions); + // } }); } _getTooltipInstanceInfo(col: number, row: number): BaseTooltip | null { diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 0bcc68b22..0451b1d69 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -2427,7 +2427,10 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { field: FieldDef, fieldKey?: FieldKeyDef ): ((v1: any, v2: any, order: string) => 0 | 1 | -1) | undefined; - abstract setRecords(records: Array, sort?: SortState | SortState[]): void; + abstract setRecords( + records: Array, + option?: { restoreHierarchyState: boolean; sort?: SortState | SortState[] } + ): void; abstract refreshHeader(): void; abstract refreshRowColCount(): void; abstract getHierarchyState(col: number, row: number): HierarchyState | null; @@ -2453,8 +2456,11 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { */ abstract updatePagination(pagination: IPagination): void; - abstract hasCustomRenderOrLayout(): boolean; + abstract _hasCustomRenderOrLayout(): boolean; + get recordsCount() { + return this.records?.length; + } get allowFrozenColCount(): number { return this.internalProps.allowFrozenColCount; } @@ -2591,9 +2597,6 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { getHeaderField(col: number, row: number): FieldDef { return this.internalProps.layoutMap.getHeaderField(col, row); } - getHeaderFieldKey(col: number, row: number): FieldDef { - return this.internalProps.layoutMap.getHeaderFieldKey(col, row); - } /** * 根据行列号获取配置 * @param {number} col column index. @@ -2682,12 +2685,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { */ _getHeaderCellBySortState(sortState: SortState): CellAddress | undefined { const { layoutMap } = this.internalProps; - let hd; - if (sortState.fieldKey) { - hd = layoutMap.headerObjects.find((col: any) => col && col.fieldKey === sortState.fieldKey); - } else { - hd = layoutMap.headerObjects.find((col: any) => col && col.field === sortState.field); - } + const hd = layoutMap.headerObjects.find((col: any) => col && col.field === sortState.field); if (hd) { const headercell = layoutMap.getHeaderCellAdressById(hd.id as number); return headercell; @@ -2986,16 +2984,16 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.options.autoWrapText ); } else { - let defaultStyle; - if (layoutMap.isColumnHeader(col, row) || layoutMap.isBottomFrozenRow(col, row)) { - defaultStyle = this.theme.headerStyle; - } else if (this.internalProps.transpose && layoutMap.isRowHeader(col, row)) { - defaultStyle = this.theme.headerStyle; - } else if (layoutMap.isRowHeader(col, row) || layoutMap.isRightFrozenColumn(col, row)) { - defaultStyle = this.theme.rowHeaderStyle; - } else { - defaultStyle = this.theme.cornerHeaderStyle; - } + // let defaultStyle; + // if (layoutMap.isColumnHeader(col, row) || layoutMap.isBottomFrozenRow(col, row)) { + // defaultStyle = this.theme.headerStyle; + // } else if (this.internalProps.transpose && layoutMap.isRowHeader(col, row)) { + // defaultStyle = this.theme.headerStyle; + // } else if (layoutMap.isRowHeader(col, row) || layoutMap.isRightFrozenColumn(col, row)) { + // defaultStyle = this.theme.rowHeaderStyle; + // } else { + // defaultStyle = this.theme.cornerHeaderStyle; + // } // const styleClass = hd.headerType.StyleClass; //BaseHeader文件 // const { style } = hd; const style = hd?.style || {}; @@ -3004,12 +3002,12 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { } cacheStyle = headerStyleContents.of( style, - defaultStyle, - // layoutMap.isColumnHeader(col, row) || layoutMap.isBottomFrozenRow(col, row) - // ? this.theme.headerStyle - // : layoutMap.isRowHeader(col, row) || layoutMap.isRightFrozenColumn(col, row) - // ? this.theme.rowHeaderStyle - // : this.theme.cornerHeaderStyle, + // defaultStyle, + layoutMap.isColumnHeader(col, row) || layoutMap.isBottomFrozenRow(col, row) + ? this.theme.headerStyle + : layoutMap.isRowHeader(col, row) || layoutMap.isRightFrozenColumn(col, row) + ? this.theme.rowHeaderStyle + : this.theme.cornerHeaderStyle, { col, row, diff --git a/packages/vtable/src/core/tableHelper.ts b/packages/vtable/src/core/tableHelper.ts index a5e126783..b534bb3e6 100644 --- a/packages/vtable/src/core/tableHelper.ts +++ b/packages/vtable/src/core/tableHelper.ts @@ -5,7 +5,7 @@ import { parseFont } from '../scenegraph/utils/font'; import { getQuadProps } from '../scenegraph/utils/padding'; import { Rect } from '../tools/Rect'; import * as calc from '../tools/calc'; -import type { FullExtendStyle, SortState } from '../ts-types'; +import type { FullExtendStyle, ListTableAPI, SortState } from '../ts-types'; import type { BaseTableAPI } from '../ts-types/base-table'; import { defaultOrderFn } from '../tools/util'; import type { ListTable } from '../ListTable'; @@ -43,7 +43,7 @@ export function _dealWithUpdateDataSource(table: BaseTableAPI, fn: (table: BaseT table.internalProps.dataSourceEventIds = [ table.internalProps.handler.on(table.internalProps.dataSource, DataSource.EVENT_TYPE.CHANGE_ORDER, () => { - if (table.dataSource.enableHierarchyState) { + if (table.dataSource.hierarchyExpandLevel) { table.refreshRowColCount(); } table.render(); @@ -51,13 +51,15 @@ export function _dealWithUpdateDataSource(table: BaseTableAPI, fn: (table: BaseT ]; } /** @private */ -export function _setRecords(table: BaseTableAPI, records: any[] = []): void { +export function _setRecords(table: ListTableAPI, records: any[] = []): void { _dealWithUpdateDataSource(table, () => { const data = records; table.internalProps.records = records; const newDataSource = (table.internalProps.dataSource = CachedDataSource.ofArray( data, + table.internalProps.dataConfig, table.pagination, + table.internalProps.layoutMap.columnObjects, (table.options as any).hierarchyExpandLevel ?? (table._hasHierarchyTreeHeader?.() ? 1 : undefined) )); table.addReleaseObj(newDataSource); @@ -322,24 +324,19 @@ export function sortRecords(table: ListTable) { if ((table as any).sortState) { let order: any; let field: any; - let fieldKey: any; if (Array.isArray((table as any).sortState)) { if ((table as any).sortState.length !== 0) { - ({ order, field, fieldKey } = (table as any).sortState?.[0]); + ({ order, field } = (table as any).sortState?.[0]); } } else { - ({ order, field, fieldKey } = (table as any).sortState as SortState); + ({ order, field } = (table as any).sortState as SortState); } // 根据sort规则进行排序 if (order && field && order !== 'normal') { - const sortFunc = table._getSortFuncFromHeaderOption(undefined, field, fieldKey); + const sortFunc = table._getSortFuncFromHeaderOption(undefined, field); // 如果sort传入的信息不能生成正确的sortFunc,直接更新表格,避免首次加载无法正常显示内容 - let hd; - if (fieldKey) { - hd = table.internalProps.layoutMap.headerObjects.find((col: any) => col && col.fieldKey === fieldKey); - } else { - hd = table.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); - } + const hd = table.internalProps.layoutMap.headerObjects.find((col: any) => col && col.field === field); + // hd?.define?.sort && //如果这里也判断 那想要利用sortState来排序 但不显示排序图标就实现不了 table.dataSource.sort(hd.field, order, sortFunc ?? defaultOrderFn); } diff --git a/packages/vtable/src/data/CachedDataSource.ts b/packages/vtable/src/data/CachedDataSource.ts index 1fa6e60af..0aad18a2c 100644 --- a/packages/vtable/src/data/CachedDataSource.ts +++ b/packages/vtable/src/data/CachedDataSource.ts @@ -1,6 +1,14 @@ import { getValueFromDeepArray } from '../tools/util'; -import type { FieldData, FieldDef, IPagination, MaybePromise, MaybePromiseOrUndefined } from '../ts-types'; +import type { + FieldData, + FieldDef, + IListTableDataConfig, + IPagination, + MaybePromise, + MaybePromiseOrUndefined +} from '../ts-types'; import type { BaseTableAPI } from '../ts-types/base-table'; +import type { ColumnData } from '../ts-types/list-table/layout-map/api'; import type { DataSourceParam } from './DataSource'; import { DataSource } from './DataSource'; @@ -33,7 +41,13 @@ export class CachedDataSource extends DataSource { static get EVENT_TYPE(): typeof DataSource.EVENT_TYPE { return DataSource.EVENT_TYPE; } - static ofArray(array: any[], pagination?: IPagination, hierarchyExpandLevel?: number): CachedDataSource { + static ofArray( + array: any[], + dataConfig?: IListTableDataConfig, + pagination?: IPagination, + columnObjs?: ColumnData[], + hierarchyExpandLevel?: number + ): CachedDataSource { return new CachedDataSource( { get: (index: number): any => { @@ -45,12 +59,20 @@ export class CachedDataSource extends DataSource { length: array.length, source: array }, + dataConfig, pagination, + columnObjs, hierarchyExpandLevel ); } - constructor(opt?: DataSourceParam, pagination?: IPagination, hierarchyExpandLevel?: number) { - super(opt, pagination, hierarchyExpandLevel); + constructor( + opt?: DataSourceParam, + dataConfig?: IListTableDataConfig, + pagination?: IPagination, + columnObjs?: ColumnData[], + hierarchyExpandLevel?: number + ) { + super(opt, dataConfig, pagination, columnObjs, hierarchyExpandLevel); this._recordCache = {}; this._fieldCache = {}; } diff --git a/packages/vtable/src/data/DataSource.ts b/packages/vtable/src/data/DataSource.ts index be266a00d..45e2c8ab8 100644 --- a/packages/vtable/src/data/DataSource.ts +++ b/packages/vtable/src/data/DataSource.ts @@ -1,23 +1,37 @@ import * as sort from '../tools/sort'; import type { + CustomAggregation, DataSourceAPI, FieldAssessor, FieldData, FieldDef, FieldFormat, + FilterRules, + IListTableDataConfig, IPagination, MaybePromiseOrCallOrUndefined, MaybePromiseOrUndefined, SortOrder } from '../ts-types'; -import { HierarchyState } from '../ts-types'; +import { AggregationType, HierarchyState } from '../ts-types'; import { applyChainSafe, getOrApply, obj, isPromise, emptyFn } from '../tools/helper'; import { EventTarget } from '../event/EventTarget'; import { getValueByPath, isAllDigits } from '../tools/util'; import { calculateArrayDiff } from '../tools/diff-cell'; -import { cloneDeep, isValid } from '@visactor/vutils'; +import { arrayEqual, cloneDeep, isValid } from '@visactor/vutils'; import type { BaseTableAPI } from '../ts-types/base-table'; -import { TABLE_EVENT_TYPE } from '../core/TABLE_EVENT_TYPE'; +import { + RecordAggregator, + type Aggregator, + SumAggregator, + CountAggregator, + MaxAggregator, + MinAggregator, + AvgAggregator, + NoneAggregator, + CustomAggregator +} from '../dataset/statistics-helper'; +import type { ColumnData } from '../ts-types/list-table/layout-map/api'; /** * 判断字段数据是否为访问器的格式 @@ -126,11 +140,13 @@ export interface ISortedMapItem { } export class DataSource extends EventTarget implements DataSourceAPI { + dataConfig: IListTableDataConfig; + dataSourceObj: DataSourceParam | DataSource; private _get: (index: number | number[]) => any; /** 数据条目数 如果是树形结构的数据 则是第一层父节点的数量 */ private _sourceLength: number; - private readonly _source: any; + private _source: any; /** * 缓存按字段进行排序的结果 */ @@ -142,24 +158,43 @@ export class DataSource extends EventTarget implements DataSourceAPI { private lastOrderFn: (a: any, b: any, order: string) => number; private lastOrderField: FieldDef; /** 每一行对应源数据的索引 */ - protected currentIndexedData: (number | number[])[] | null = []; + currentIndexedData: (number | number[])[] | null = []; protected userPagination: IPagination; protected pagination: IPagination; /** 当前页每一行对应源数据的索引 */ - protected _currentPagerIndexedData: (number | number[])[]; + _currentPagerIndexedData: (number | number[])[]; // 当前是否为层级的树形结构 排序时判断该值确实是否继续进行子节点排序 - enableHierarchyState = false; + hierarchyExpandLevel: number = 0; static get EVENT_TYPE(): typeof EVENT_TYPE { return EVENT_TYPE; } - protected treeDataHierarchyState: Map = new Map(); + treeDataHierarchyState: Map = new Map(); beforeChangedRecordsMap: Record[] = []; - constructor(obj?: DataSourceParam | DataSource, pagination?: IPagination, hierarchyExpandLevel?: number) { - super(); - this._get = obj?.get.bind(obj) || (undefined as any); - this._sourceLength = obj?.length || 0; - this._source = obj?.source ?? obj; + // 注册聚合类型 + registedAggregators: { + [key: string]: { + new (dimension: string | string[], formatFun?: any, isRecord?: boolean, aggregationFun?: Function): Aggregator; + }; + } = {}; + // columns对应各个字段的聚合类对象 + fieldAggregators: Aggregator[] = []; + layoutColumnObjects: ColumnData[] = []; + constructor( + dataSourceObj?: DataSourceParam | DataSource, + dataConfig?: IListTableDataConfig, + pagination?: IPagination, + columnObjs?: ColumnData[], + hierarchyExpandLevel?: number + ) { + super(); + this.registerAggregators(); + this.dataSourceObj = dataSourceObj; + this.dataConfig = dataConfig; + this._get = dataSourceObj?.get.bind(dataSourceObj) || (undefined as any); + this.layoutColumnObjects = columnObjs; + this._source = this.processRecords(dataSourceObj?.source ?? dataSourceObj); + this._sourceLength = this._source?.length || 0; this.sortedIndexMap = new Map(); this._currentPagerIndexedData = []; @@ -170,34 +205,121 @@ export class DataSource extends EventTarget implements DataSourceAPI { currentPage: 0 }; if (hierarchyExpandLevel >= 1) { - this.enableHierarchyState = true; + this.hierarchyExpandLevel = hierarchyExpandLevel; } - // 初始化currentIndexedData 正常未排序。设置其状态 this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i); - if (this.enableHierarchyState) { + // 初始化currentIndexedData 正常未排序。设置其状态 + this.initTreeHierarchyState(); + this.updatePagerData(); + } + initTreeHierarchyState() { + if (this.hierarchyExpandLevel) { + this.treeDataHierarchyState = new Map(); for (let i = 0; i < this._sourceLength; i++) { //expandLevel为有效值即需要按tree分析展示数据 const nodeData = this.getOriginalRecord(i); (nodeData as any).children && this.treeDataHierarchyState.set(i, HierarchyState.collapse); } + + this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i); + if (this.hierarchyExpandLevel > 1) { + let nodeLength = this._sourceLength; + for (let i = 0; i < nodeLength; i++) { + const indexKey = this.currentIndexedData[i]; + const nodeData = this.getOriginalRecord(indexKey); + if ((nodeData as any).children?.length > 0) { + this.treeDataHierarchyState.set( + Array.isArray(indexKey) ? indexKey.join(',') : indexKey, + HierarchyState.expand + ); + const childrenLength = this.initChildrenNodeHierarchy(indexKey, this.hierarchyExpandLevel, 2, nodeData); + i += childrenLength; + nodeLength += childrenLength; + } + } + } } - if (hierarchyExpandLevel > 1) { - let nodeLength = this._sourceLength; - for (let i = 0; i < nodeLength; i++) { - const indexKey = this.currentIndexedData[i]; - const nodeData = this.getOriginalRecord(indexKey); - if ((nodeData as any).children?.length > 0) { - this.treeDataHierarchyState.set( - Array.isArray(indexKey) ? indexKey.join(',') : indexKey, - HierarchyState.expand + } + //将聚合类型注册 收集到aggregators + registerAggregator(type: string, aggregator: any) { + this.registedAggregators[type] = aggregator; + } + //将聚合类型注册 + registerAggregators() { + this.registerAggregator(AggregationType.RECORD, RecordAggregator); + this.registerAggregator(AggregationType.SUM, SumAggregator); + this.registerAggregator(AggregationType.COUNT, CountAggregator); + this.registerAggregator(AggregationType.MAX, MaxAggregator); + this.registerAggregator(AggregationType.MIN, MinAggregator); + this.registerAggregator(AggregationType.AVG, AvgAggregator); + this.registerAggregator(AggregationType.NONE, NoneAggregator); + this.registerAggregator(AggregationType.CUSTOM, CustomAggregator); + } + _generateFieldAggragations() { + const columnObjs = this.layoutColumnObjects; + for (let i = 0; i < columnObjs?.length; i++) { + columnObjs[i].aggregator = null; //重置聚合器 如更新了过滤条件都需要重新计算 + const field = columnObjs[i].field; + const aggragation = columnObjs[i].aggregation; + if (!aggragation) { + continue; + } + if (Array.isArray(aggragation)) { + for (let j = 0; j < aggragation.length; j++) { + const item = aggragation[j]; + const aggregator = new this.registedAggregators[item.aggregationType]( + field as string, + item.formatFun, + true, + (item as CustomAggregation).aggregationFun ); - const childrenLength = this.initChildrenNodeHierarchy(indexKey, hierarchyExpandLevel, 2, nodeData); - i += childrenLength; - nodeLength += childrenLength; + this.fieldAggregators.push(aggregator); + if (!columnObjs[i].aggregator) { + columnObjs[i].aggregator = []; + } + columnObjs[i].aggregator.push(aggregator); } + } else { + const aggregator = new this.registedAggregators[aggragation.aggregationType]( + field as string, + aggragation.formatFun, + true, + (aggragation as CustomAggregation).aggregationFun + ); + this.fieldAggregators.push(aggregator); + columnObjs[i].aggregator = aggregator; } } - this.updatePagerData(); + } + processRecords(records: any[]) { + this._generateFieldAggragations(); + const filteredRecords = []; + const isHasAggregation = this.fieldAggregators.length >= 1; + const isHasFilterRule = this.dataConfig?.filterRules?.length >= 1; + if (isHasFilterRule || isHasAggregation) { + for (let i = 0, len = records.length; i < len; i++) { + const record = records[i]; + if (isHasFilterRule) { + if (this.filterRecord(record)) { + filteredRecords.push(record); + isHasAggregation && this.processRecord(record); + } + } else if (isHasAggregation) { + this.processRecord(record); + } + } + if (isHasFilterRule) { + return filteredRecords; + } + } + return records; + } + + processRecord(record: any) { + for (let i = 0; i < this.fieldAggregators.length; i++) { + const aggregator = this.fieldAggregators[i]; + aggregator.push(record); + } } /** * 初始化子节点的层次信息 @@ -300,7 +422,10 @@ export class DataSource extends EventTarget implements DataSourceAPI { getIndexKey(index: number): number | number[] { return _getIndex(this.currentPagerIndexedData, index); } - getTableIndex(colOrRow: number): number { + getTableIndex(colOrRow: number | number[]): number { + if (Array.isArray(colOrRow)) { + return this.currentPagerIndexedData.findIndex(value => arrayEqual(value, colOrRow)); + } return this.currentPagerIndexedData.findIndex(value => value === colOrRow); } /** 获取数据源中第index位置的field字段数据。传入col row是因为后面的format函数参数使用*/ @@ -512,7 +637,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { this.source.splice(index, 0, record); this.currentIndexedData.push(this.currentIndexedData.length); this._sourceLength += 1; - + this.initTreeHierarchyState(); if (this.userPagination) { //如果用户配置了分页 this.pagination.totalCount = this._sourceLength; @@ -720,7 +845,7 @@ export class DataSource extends EventTarget implements DataSourceAPI { ); } this.currentIndexedData = sortedIndexArray; - if (this.enableHierarchyState) { + if (this.hierarchyExpandLevel) { let nodeLength = sortedIndexArray.length; const t0 = window.performance.now(); for (let i = 0; i < nodeLength; i++) { @@ -741,6 +866,52 @@ export class DataSource extends EventTarget implements DataSourceAPI { this.updatePagerData(); this.fireListeners(EVENT_TYPE.CHANGE_ORDER, null); } + + private filterRecord(record: any) { + let isReserved = true; + for (let i = 0; i < this.dataConfig.filterRules.length; i++) { + const filterRule = this.dataConfig?.filterRules[i]; + if (filterRule.filterKey) { + const filterValue = record[filterRule.filterKey]; + if (filterRule.filteredValues.indexOf(filterValue) === -1) { + isReserved = false; + break; + } + } else if (!filterRule.filterFunc?.(record)) { + isReserved = false; + break; + } + } + return isReserved; + } + + updateFilterRulesForSorted(filterRules?: FilterRules): void { + this.dataConfig.filterRules = filterRules; + this._source = this.processRecords(this.dataSourceObj?.source ?? this.dataSourceObj); + this._sourceLength = this._source?.length || 0; + this.sortedIndexMap.clear(); + this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i); + if (!this.userPagination) { + this.pagination.perPageCount = this._sourceLength; + this.pagination.totalCount = this._sourceLength; + } + } + + updateFilterRules(filterRules?: FilterRules): void { + this.dataConfig.filterRules = filterRules; + this._source = this.processRecords(this.dataSourceObj?.source ?? this.dataSourceObj); + this._sourceLength = this._source?.length || 0; + // 初始化currentIndexedData 正常未排序。设置其状态 + this.currentIndexedData = Array.from({ length: this._sourceLength }, (_, i) => i); + if (this.userPagination) { + // 如果用户配置了分页 + this.updatePagerData(); + } else { + this.pagination.perPageCount = this._sourceLength; + this.pagination.totalCount = this._sourceLength; + this.updatePagerData(); + } + } /** * 当节点折叠或者展开时 将排序缓存清空(非当前排序规则的缓存) */ @@ -801,6 +972,9 @@ export class DataSource extends EventTarget implements DataSourceAPI { this.currentPagerIndexedData.length = 0; } protected getOriginalRecord(dataIndex: number | number[]): MaybePromiseOrUndefined { + if (this.dataConfig?.filterRules) { + return (this.source as Array)[dataIndex as number]; + } return getValue(this._get(dataIndex), (val: MaybePromiseOrUndefined) => { this.recordPromiseCallBack(dataIndex, val); }); diff --git a/packages/vtable/src/data/FilterDataSource.ts b/packages/vtable/src/data/FilterDataSource.ts deleted file mode 100644 index c18286246..000000000 --- a/packages/vtable/src/data/FilterDataSource.ts +++ /dev/null @@ -1,220 +0,0 @@ -// import type { FieldDef, MaybePromise, MaybePromiseOrUndefined } from "../ts-types"; -// import { each, isPromise } from "../tools/utils"; -// import { DataSource } from "./DataSource"; -// import { EventHandler } from "../tools/EventHandler"; - -// /** @private */ -// type Filter = (record: T | undefined) => boolean; - -// /** @private */ -// class DataSourceIterator { -// _dataSource: DataSource; -// _curIndex: number; -// _data: MaybePromiseOrUndefined[]; -// constructor(dataSource: DataSource) { -// this._dataSource = dataSource; -// this._curIndex = -1; -// this._data = []; -// } -// hasNext(): boolean { -// const next = this._curIndex + 1; -// return this._dataSource.length > next; -// } -// next(): MaybePromiseOrUndefined { -// const next = this._curIndex + 1; -// const data = this._getIndexData(next); -// this._curIndex = next; -// return data; -// } -// movePrev(): void { -// this._curIndex--; -// } -// _getIndexData(index: number, nest?: boolean): MaybePromiseOrUndefined { -// const dataSource = this._dataSource; -// const data = this._data; -// if (index < data.length) { -// return data[index]; -// } - -// if (dataSource.length <= index) { -// return undefined; -// } -// const record = this._dataSource.get(index); -// data[index] = record; -// if (isPromise(record)) { -// record.then((val) => { -// data[index] = val; -// }); -// if (!nest) { -// for (let i = 1; i <= 100; i++) { -// this._getIndexData(index + i, true); -// } -// } -// } -// return record; -// } -// } -// /** @private */ -// class FilterData { -// _owner: FilterDataSource; -// _dataSourceItr: DataSourceIterator; -// _filter: Filter; -// _filterdList: (T | undefined)[]; -// _queues: (Promise | null)[]; -// _cancel = false; -// constructor( -// dc: FilterDataSource, -// original: DataSource, -// filter: Filter -// ) { -// this._owner = dc; -// this._dataSourceItr = new DataSourceIterator(original); -// this._filter = filter; -// this._filterdList = []; -// this._queues = []; -// } -// get(index: number): MaybePromiseOrUndefined { -// if (this._cancel) { -// return undefined; -// } -// const filterdList = this._filterdList; -// if (index < filterdList.length) { -// return filterdList[index]; -// } -// const queues = this._queues; -// const indexQueue = queues[index]; -// if (indexQueue) { -// return indexQueue; -// } -// return queues[index] || this._findIndex(index); -// } -// cancel(): void { -// this._cancel = true; -// } -// _findIndex(index: number): MaybePromiseOrUndefined { -// if (window.Promise) { -// const timeout = Date.now() + 100; -// let count = 0; -// return this._findIndexWithTimeout(index, () => { -// count++; -// if (count >= 100) { -// count = 0; -// return timeout < Date.now(); -// } -// return false; -// }); -// } -// return this._findIndexWithTimeout(index, () => false); -// } -// _findIndexWithTimeout( -// index: number, -// testTimeout: () => boolean -// ): MaybePromiseOrUndefined { -// const filterdList = this._filterdList; -// const filter = this._filter; -// const dataSourceItr = this._dataSourceItr; - -// const queues = this._queues; - -// while (dataSourceItr.hasNext()) { -// if (this._cancel) { -// return undefined; -// } -// const record = dataSourceItr.next(); -// if (isPromise(record)) { -// dataSourceItr.movePrev(); -// const queue = record.then((_value) => { -// queues[index] = null; -// return this.get(index); -// }); -// queues[index] = queue; -// return queue; -// } -// if (filter(record)) { -// filterdList.push(record); -// if (index < filterdList.length) { -// return filterdList[index]; -// } -// } -// if (testTimeout()) { -// const promise = new Promise((resolve) => { -// setTimeout(() => { -// resolve(); -// }, 300); -// }); -// const queue = promise.then(() => { -// queues[index] = null; -// return this.get(index); -// }); -// queues[index] = queue; -// return queue; -// } -// } -// const dc = this._owner; -// dc.length = filterdList.length; -// return undefined; -// } -// } - -// /** -// * table data source for filter -// * -// * @classdesc VTable.data.FilterDataSource -// * @memberof VTable.data -// */ -// export class FilterDataSource extends DataSource { -// private _dataSource: DataSource; -// private _handler: EventHandler; -// private _filterData: FilterData | null = null; -// static get EVENT_TYPE(): typeof DataSource.EVENT_TYPE { -// return DataSource.EVENT_TYPE; -// } -// constructor(dataSource: DataSource, filter: Filter) { -// super(dataSource); -// this._dataSource = dataSource; -// this.filter = filter; -// const handler = (this._handler = new EventHandler()); -// handler.on(dataSource, DataSource.EVENT_TYPE.UPDATED_ORDER, () => { -// // reset -// // eslint-disable-next-line no-self-assign -// this.filter = this.filter; -// }); -// each(DataSource.EVENT_TYPE, (type) => { -// handler.on(dataSource, type, (...args) => -// this.fireListeners(type, ...args) -// ); -// }); -// } -// get filter(): Filter | null { -// return this._filterData?._filter || null; -// } -// set filter(filter: Filter | null) { -// if (this._filterData) { -// this._filterData.cancel(); -// } -// this._filterData = filter -// ? new FilterData(this, this._dataSource, filter) -// : null; -// this.length = this._dataSource.length; -// } -// protected getOriginal(index: number): MaybePromiseOrUndefined { -// if (!this._filterData) { -// return super.getOriginal(index); -// } -// return this._filterData.get(index); -// } -// sort(field: FieldDef, order: "desc" | "asc",orderFn: (v1: T, v2: T, order: string) => -1 | 0 | 1): void { -// this._dataSource.sort(field, order,orderFn); -// } -// -// get source(): any { -// return this._dataSource.source; -// } -// get dataSource(): DataSource { -// return this._dataSource; -// } -// release(): void { -// this._handler.release?.(); -// super.release?.(); -// } -// } diff --git a/packages/vtable/src/dataset/dataset-pivot-table.ts b/packages/vtable/src/dataset/dataset-pivot-table.ts index bf3b6d6db..49945decf 100644 --- a/packages/vtable/src/dataset/dataset-pivot-table.ts +++ b/packages/vtable/src/dataset/dataset-pivot-table.ts @@ -1,6 +1,6 @@ import type { FilterRules, - IDataConfig, + IPivotTableDataConfig, SortRule, AggregationRules, AggregationRule, @@ -35,7 +35,7 @@ export class DatasetForPivotTable { /** * 用户配置 */ - dataConfig: IDataConfig; + dataConfig: IPivotTableDataConfig; /** * 明细数据 */ @@ -94,7 +94,7 @@ export class DatasetForPivotTable { columns: string[]; indicatorKeys: string[]; constructor( - dataConfig: IDataConfig, + dataConfig: IPivotTableDataConfig, rows: string[], columns: string[], indicators: string[], @@ -415,6 +415,9 @@ export class DatasetForPivotTable { push() { // do nothing }, + recalculate() { + // do nothing + }, value(): any { return null; }, diff --git a/packages/vtable/src/dataset/dataset.ts b/packages/vtable/src/dataset/dataset.ts index 9afaeb07f..016ce82c1 100644 --- a/packages/vtable/src/dataset/dataset.ts +++ b/packages/vtable/src/dataset/dataset.ts @@ -1,7 +1,7 @@ import { isArray, isValid } from '@visactor/vutils'; import type { FilterRules, - IDataConfig, + IPivotTableDataConfig, SortRule, AggregationRules, AggregationRule, @@ -18,7 +18,8 @@ import type { IHeaderTreeDefine, CollectValueBy, CollectedValue, - IIndicator + IIndicator, + IPivotChartDataConfig } from '../ts-types'; import { AggregationType, SortType } from '../ts-types'; import type { Aggregator } from './statistics-helper'; @@ -41,7 +42,7 @@ export class Dataset { /** * 用户配置 */ - dataConfig: IDataConfig; + dataConfig: IPivotTableDataConfig | IPivotChartDataConfig; // /** // * 分页配置 // */ @@ -127,7 +128,7 @@ export class Dataset { // 记录用户传入的汇总数据 totalRecordsTree: Record> = {}; constructor( - dataConfig: IDataConfig, + dataConfig: IPivotTableDataConfig | IPivotChartDataConfig, // pagination: IPagination, rows: string[], columns: string[], @@ -160,7 +161,7 @@ export class Dataset { this.colSubTotalLabel = this.totals?.column?.subTotalLabel ?? '小计'; this.rowGrandTotalLabel = this.totals?.row?.grandTotalLabel ?? '总计'; this.rowSubTotalLabel = this.totals?.row?.subTotalLabel ?? '小计'; - this.collectValuesBy = this.dataConfig?.collectValuesBy; + this.collectValuesBy = (this.dataConfig as IPivotChartDataConfig)?.collectValuesBy; this.needSplitPositiveAndNegative = needSplitPositiveAndNegative ?? false; this.rowsIsTotal = new Array(this.rows?.length ?? 0).fill(false); this.colsIsTotal = new Array(this.columns?.length ?? 0).fill(false); @@ -280,7 +281,7 @@ export class Dataset { const t8 = typeof window !== 'undefined' ? window.performance.now() : 0; console.log('TreeToArr:', t8 - t7); - if (this.dataConfig?.isPivotChart) { + if ((this.dataConfig as IPivotChartDataConfig)?.isPivotChart) { // 处理PivotChart双轴图0值对齐 // this.dealWithZeroAlign(); @@ -794,7 +795,7 @@ export class Dataset { this.processCollectedValuesWithSumBy(); this.processCollectedValuesWithSortBy(); - if (this.dataConfig?.isPivotChart) { + if ((this.dataConfig as IPivotChartDataConfig)?.isPivotChart) { // 处理PivotChart双轴图0值对齐 // this.dealWithZeroAlign(); // 记录PivotChart维度对应的数据 @@ -856,6 +857,9 @@ export class Dataset { formatFun: agg.formatFun, records: agg.records, className: '', + recalculate() { + // do nothing + }, push() { // do nothing }, @@ -879,6 +883,9 @@ export class Dataset { push() { // do nothing }, + recalculate() { + // do nothing + }, formatValue() { return changeValue; }, @@ -899,6 +906,9 @@ export class Dataset { push() { // do nothing }, + recalculate() { + // do nothing + }, value(): any { return null; }, @@ -1489,10 +1499,10 @@ export class Dataset { private cacheDeminsionCollectedValues() { for (const key in this.collectValuesBy) { if (this.collectValuesBy[key].type === 'xField' || this.collectValuesBy[key].type === 'yField') { - if (this.dataConfig.dimensionSortArray) { + if ((this.dataConfig as IPivotChartDataConfig).dimensionSortArray) { this.cacheCollectedValues[key] = arraySortByAnotherArray( this.collectedValues[key] as unknown as string[], - this.dataConfig.dimensionSortArray + (this.dataConfig as IPivotChartDataConfig).dimensionSortArray ) as unknown as Record; } else { this.cacheCollectedValues[key] = this.collectedValues[key]; diff --git a/packages/vtable/src/dataset/statistics-helper.ts b/packages/vtable/src/dataset/statistics-helper.ts index 4359bf24f..1a718aed8 100644 --- a/packages/vtable/src/dataset/statistics-helper.ts +++ b/packages/vtable/src/dataset/statistics-helper.ts @@ -10,26 +10,15 @@ export abstract class Aggregator { field?: string | string[]; formatFun?: any; _formatedValue?: any; - needSplitPositiveAndNegativeForSum?: boolean = false; - constructor( - dimension: string | string[], - formatFun?: any, - isRecord?: boolean, - needSplitPositiveAndNegative?: boolean - ) { + + constructor(dimension: string, formatFun?: any, isRecord?: boolean) { this.field = dimension; - this.needSplitPositiveAndNegativeForSum = needSplitPositiveAndNegative ?? false; this.formatFun = formatFun; this.isRecord = isRecord ?? this.isRecord; } - // push(record: any) { - // if (this.isRecord) { - // if (record.className === 'Aggregator') this.records.push(...record.records); - // else this.records.push(record); - // } - // } abstract push(record: any): void; abstract value(): any; + abstract recalculate(): any; clearCacheValue() { this._formatedValue = undefined; } @@ -65,6 +54,9 @@ export class RecordAggregator extends Aggregator { reset() { this.records = []; } + recalculate() { + // do nothing + } } export class NoneAggregator extends Aggregator { @@ -85,6 +77,44 @@ export class NoneAggregator extends Aggregator { this.records = []; this.fieldValue = undefined; } + recalculate() { + // do nothing + } +} +export class CustomAggregator extends Aggregator { + type: string = AggregationType.CUSTOM; //仅获取其中一条数据 不做聚合 其fieldValue可以是number或者string类型 + isRecord?: boolean = true; + declare field?: string; + aggregationFun: Function; + values: (string | number)[] = []; + fieldValue?: any; + constructor(dimension: string, formatFun?: any, isRecord?: boolean, aggregationFun?: Function) { + super(dimension, formatFun, isRecord); + this.aggregationFun = aggregationFun; + } + push(record: any): void { + if (this.isRecord) { + if (record.className === 'Aggregator') { + this.records.push(...record.records); + } else { + this.records.push(record); + } + } + this.values.push(record[this.field]); + } + value() { + if (!this.fieldValue) { + this.fieldValue = this.aggregationFun?.(this.values, this.records, this.field); + } + return this.fieldValue; + } + reset() { + this.records = []; + this.fieldValue = undefined; + } + recalculate() { + // do nothing + } } export class SumAggregator extends Aggregator { type: string = AggregationType.SUM; @@ -92,6 +122,11 @@ export class SumAggregator extends Aggregator { positiveSum = 0; nagetiveSum = 0; declare field?: string; + needSplitPositiveAndNegativeForSum?: boolean = false; + constructor(dimension: string, formatFun?: any, isRecord?: boolean, needSplitPositiveAndNegative?: boolean) { + super(dimension, formatFun, isRecord); + this.needSplitPositiveAndNegativeForSum = needSplitPositiveAndNegative ?? false; + } push(record: any): void { if (this.isRecord) { if (record.className === 'Aggregator') { @@ -135,6 +170,34 @@ export class SumAggregator extends Aggregator { this.records = []; this.sum = 0; } + recalculate() { + this.sum = 0; + this._formatedValue = undefined; + for (let i = 0; i < this.records.length; i++) { + const record = this.records[i]; + if (record.className === 'Aggregator') { + const value = record.value(); + this.sum += value; + if (this.needSplitPositiveAndNegativeForSum) { + if (value > 0) { + this.positiveSum += value; + } else if (value < 0) { + this.nagetiveSum += value; + } + } + } else if (!isNaN(parseFloat(record[this.field]))) { + const value = parseFloat(record[this.field]); + this.sum += value; + if (this.needSplitPositiveAndNegativeForSum) { + if (value > 0) { + this.positiveSum += value; + } else if (value < 0) { + this.nagetiveSum += value; + } + } + } + } + } } export class CountAggregator extends Aggregator { @@ -162,6 +225,18 @@ export class CountAggregator extends Aggregator { this.records = []; this.count = 0; } + recalculate() { + this.count = 0; + this._formatedValue = undefined; + for (let i = 0; i < this.records.length; i++) { + const record = this.records[i]; + if (record.className === 'Aggregator') { + this.count += record.value(); + } else { + this.count++; + } + } + } } export class AvgAggregator extends Aggregator { type: string = AggregationType.AVG; @@ -192,6 +267,21 @@ export class AvgAggregator extends Aggregator { this.sum = 0; this.count = 0; } + recalculate() { + this.sum = 0; + this.count = 0; + this._formatedValue = undefined; + for (let i = 0; i < this.records.length; i++) { + const record = this.records[i]; + if (record.className === 'Aggregator' && record.type === AggregationType.AVG) { + this.sum += record.sum; + this.count += record.count; + } else if (!isNaN(parseFloat(record[this.field]))) { + this.sum += parseFloat(record[this.field]); + this.count++; + } + } + } } export class MaxAggregator extends Aggregator { type: string = AggregationType.MAX; @@ -222,11 +312,26 @@ export class MaxAggregator extends Aggregator { this.records = []; this.max = Number.MIN_SAFE_INTEGER; } + recalculate() { + this.max = Number.MIN_SAFE_INTEGER; + this._formatedValue = undefined; + for (let i = 0; i < this.records.length; i++) { + const record = this.records[i]; + if (record.className === 'Aggregator') { + this.max = record.max > this.max ? record.max : this.max; + } else if (typeof record === 'number') { + this.max = record > this.max ? record : this.max; + } else if (typeof record[this.field] === 'number') { + this.max = record[this.field] > this.max ? record[this.field] : this.max; + } else if (!isNaN(record[this.field])) { + this.max = parseFloat(record[this.field]) > this.max ? parseFloat(record[this.field]) : this.max; + } + } + } } export class MinAggregator extends Aggregator { type: string = AggregationType.MIN; min: number = Number.MAX_SAFE_INTEGER; - isRecord?: boolean = false; declare field?: string; push(record: any): void { if (this.isRecord) { @@ -251,6 +356,20 @@ export class MinAggregator extends Aggregator { this.records = []; this.min = Number.MAX_SAFE_INTEGER; } + recalculate() { + this.min = Number.MAX_SAFE_INTEGER; + this._formatedValue = undefined; + for (let i = 0; i < this.records.length; i++) { + const record = this.records[i]; + if (record.className === 'Aggregator') { + this.min = record.min < this.min ? record.min : this.min; + } else if (typeof record === 'number') { + this.min = record < this.min ? record : this.min; + } else if (typeof record[this.field] === 'number') { + this.min = record[this.field] < this.min ? record[this.field] : this.min; + } + } + } } export function indicatorSort(a: any, b: any) { if (a && b) { diff --git a/packages/vtable/src/edit/edit-manager.ts b/packages/vtable/src/edit/edit-manager.ts index 81ba891b8..1a7a5462f 100644 --- a/packages/vtable/src/edit/edit-manager.ts +++ b/packages/vtable/src/edit/edit-manager.ts @@ -3,6 +3,7 @@ import { TABLE_EVENT_TYPE } from '../core/TABLE_EVENT_TYPE'; import type { BaseTableAPI } from '../ts-types/base-table'; import type { ListTableAPI, ListTableConstructorOptions } from '../ts-types'; import { getCellEventArgsSet } from '../event/util'; +import type { SimpleHeaderLayoutMap } from '../layout'; export class EditManeger { table: BaseTableAPI; @@ -71,6 +72,10 @@ export class EditManeger { return; } } + if ((this.table.internalProps.layoutMap as SimpleHeaderLayoutMap)?.isAggregation?.(col, row)) { + console.warn("VTable Warn: this is aggregation value, can't be edited"); + return; + } this.editingEditor = editor; this.editCell = { col, row }; const dataValue = this.table.getCellOriginValue(col, row); diff --git a/packages/vtable/src/event/event.ts b/packages/vtable/src/event/event.ts index a708da566..57af2b27f 100644 --- a/packages/vtable/src/event/event.ts +++ b/packages/vtable/src/event/event.ts @@ -29,8 +29,8 @@ export class EventManager { table: BaseTableAPI; // _col: number; // _resizing: boolean = false; - /** 为了能够判断canvas mousedown 事件 以阻止事件冒泡 */ - isPointerDownOnTable: boolean = false; + // /** 为了能够判断canvas mousedown 事件 以阻止事件冒泡 */ + // isPointerDownOnTable: boolean = false; isTouchdown: boolean; // touch scrolling mode on touchMovePoints: { x: number; @@ -90,12 +90,12 @@ export class EventManager { // 图标点击 this.table.on(TABLE_EVENT_TYPE.ICON_CLICK, iconInfo => { - const { col, row, x, y, funcType, icon } = iconInfo; + const { col, row, x, y, funcType, icon, event } = iconInfo; // 下拉菜单按钮点击 if (funcType === IconFuncTypeEnum.dropDown) { - stateManager.triggerDropDownMenu(col, row, x, y); + stateManager.triggerDropDownMenu(col, row, x, y, event); } else if (funcType === IconFuncTypeEnum.sort) { - stateManager.triggerSort(col, row, icon); + stateManager.triggerSort(col, row, icon, event); } else if (funcType === IconFuncTypeEnum.frozen) { stateManager.triggerFreeze(col, row, icon); } else if (funcType === IconFuncTypeEnum.drillDown) { @@ -361,7 +361,8 @@ export class EventManager { col, row, funcType: icon.attribute.funcType, - icon + icon, + event }); return true; @@ -376,7 +377,8 @@ export class EventManager { col, row, funcType: (icon.attribute as any).funcType, - icon: icon as unknown as Icon + icon: icon as unknown as Icon, + event }); return true; } diff --git a/packages/vtable/src/event/listener/container-dom.ts b/packages/vtable/src/event/listener/container-dom.ts index 3cbcd075f..daa87a9cb 100644 --- a/packages/vtable/src/event/listener/container-dom.ts +++ b/packages/vtable/src/event/listener/container-dom.ts @@ -11,11 +11,11 @@ export function bindContainerDomListener(eventManager: EventManager) { const stateManager = table.stateManager; const handler: EventHandler = table.internalProps.handler; - handler.on(table.getElement(), 'mousedown', (e: MouseEvent) => { - if (table.eventManager.isPointerDownOnTable) { - e.stopPropagation(); - } - }); + // handler.on(table.getElement(), 'mousedown', (e: MouseEvent) => { + // if (table.eventManager.isPointerDownOnTable) { + // e.stopPropagation(); + // } + // }); handler.on(table.getElement(), 'blur', (e: MouseEvent) => { eventManager.dealTableHover(); diff --git a/packages/vtable/src/event/listener/table-group.ts b/packages/vtable/src/event/listener/table-group.ts index 8f4024f3c..955203b3b 100644 --- a/packages/vtable/src/event/listener/table-group.ts +++ b/packages/vtable/src/event/listener/table-group.ts @@ -360,15 +360,16 @@ export function bindTableGroupListener(eventManager: EventManager) { table.scenegraph.tableGroup.addEventListener('pointerdown', (e: FederatedPointerEvent) => { console.log('tableGroup pointerdown'); - table.eventManager.isPointerDownOnTable = true; - setTimeout(() => { - table.eventManager.isPointerDownOnTable = false; - }, 0); + // table.eventManager.isPointerDownOnTable = true; + // setTimeout(() => { + // table.eventManager.isPointerDownOnTable = false; + // }, 0); table.eventManager.isDown = true; table.eventManager.LastBodyPointerXY = { x: e.x, y: e.y }; - // 避免在调整列宽等拖拽操作触发外层组件的拖拽逻辑 - // 如果鼠标位置在表格内(加调整列宽的热区),将mousedown事件阻止冒泡 - e.stopPropagation(); + // // 避免在调整列宽等拖拽操作触发外层组件的拖拽逻辑; + // // 如果鼠标位置在表格内(加调整列宽的热区),将pointerdown事件阻止冒泡(如果阻止mousedown需要结合isPointerDownOnTable来判断) + // e.stopPropagation(); + // e.preventDefault(); //为了阻止mousedown事件的触发,后续:不能这样写,会阻止table聚焦 table.eventManager.LastPointerXY = { x: e.x, y: e.y }; if (e.button !== 0) { @@ -750,7 +751,7 @@ function endResizeCol(table: BaseTableAPI) { } table.fireListeners(TABLE_EVENT_TYPE.RESIZE_COLUMN_END, { col: table.stateManager.columnResize.col, - columns + colWidths: columns }); } } diff --git a/packages/vtable/src/layout/chart-helper/get-chart-spec.ts b/packages/vtable/src/layout/chart-helper/get-chart-spec.ts index bdfd7e117..29551db9f 100644 --- a/packages/vtable/src/layout/chart-helper/get-chart-spec.ts +++ b/packages/vtable/src/layout/chart-helper/get-chart-spec.ts @@ -20,8 +20,37 @@ export function getRawChartSpec(col: number, row: number, layout: PivotHeaderLay } const chartSpec = indicatorObj?.chartSpec; + if (typeof chartSpec === 'function') { + // 动态组织spec + const arg = { + col, + row, + dataValue: layout._table.getCellOriginValue(col, row) || '', + value: layout._table.getCellValue(col, row) || '', + rect: layout._table.getCellRangeRelativeRect(layout._table.getCellRange(col, row)), + table: layout._table + }; + return chartSpec(arg); + } return chartSpec; } +export function isShareChartSpec(col: number, row: number, layout: PivotHeaderLayoutMap): any { + const paths = layout.getCellHeaderPaths(col, row); + let indicatorObj; + if (layout.indicatorsAsCol) { + const indicatorKey = paths.colHeaderPaths.find(colPath => colPath.indicatorKey)?.indicatorKey; + indicatorObj = layout.columnObjects.find(indicator => indicator.indicatorKey === indicatorKey); + } else { + const indicatorKey = paths.rowHeaderPaths.find(rowPath => rowPath.indicatorKey)?.indicatorKey; + indicatorObj = layout.columnObjects.find(indicator => indicator.indicatorKey === indicatorKey); + } + const chartSpec = indicatorObj?.chartSpec; + + if (typeof chartSpec === 'function') { + return false; + } + return true; +} /** 检查是否有直角坐标系的图表 */ export function checkHasCartesianChart(layout: PivotHeaderLayoutMap) { let isHasCartesianChart = false; @@ -95,16 +124,19 @@ export function isHasCartesianChartInline( export function getChartSpec(col: number, row: number, layout: PivotHeaderLayoutMap): any { let chartSpec = layout.getRawChartSpec(col, row); if (chartSpec) { - chartSpec = cloneDeep(chartSpec); - chartSpec.sortDataByAxis = true; - if (isArray(chartSpec.series)) { - chartSpec.series.forEach((serie: any) => { - serie.sortDataByAxis = true; - }); + if (layout._table.isPivotChart()) { + chartSpec = cloneDeep(chartSpec); + chartSpec.sortDataByAxis = true; + if (isArray(chartSpec.series)) { + chartSpec.series.forEach((serie: any) => { + serie.sortDataByAxis = true; + }); + } + chartSpec.axes = layout.getChartAxes(col, row); + chartSpec.padding = 0; + chartSpec.dataZoom = []; // Do not support datazoom temply + return chartSpec; } - chartSpec.axes = layout.getChartAxes(col, row); - chartSpec.padding = 0; - chartSpec.dataZoom = []; // Do not support datazoom temply return chartSpec; } return null; diff --git a/packages/vtable/src/layout/layout-helper.ts b/packages/vtable/src/layout/layout-helper.ts index c4cb04c5a..830f627ec 100644 --- a/packages/vtable/src/layout/layout-helper.ts +++ b/packages/vtable/src/layout/layout-helper.ts @@ -1,628 +1,52 @@ -import { cloneDeep, isValid } from '@visactor/vutils'; -import { NumberMap } from '../tools/NumberMap'; -import { IndicatorDimensionKeyPlaceholder } from '../tools/global'; -import type { Either } from '../tools/helper'; -import type { - CellInfo, - ColumnIconOption, - FieldData, - HeaderData, - ICustomRender, - IDimension, - IIndicator, - IRowDimension, - LayoutObjectId -} from '../ts-types'; -import { HierarchyState } from '../ts-types'; -import type { PivotHeaderLayoutMap } from './pivot-header-layout'; -import type { ILinkDimension } from '../ts-types/pivot-table/dimension/link-dimension'; -import type { IImageDimension } from '../ts-types/pivot-table/dimension/image-dimension'; -// import { sharedVar } from './pivot-header-layout'; +import type { Aggregation } from '../ts-types'; +import type { ColumnData } from '../ts-types/list-table/layout-map/api'; +import type { SimpleHeaderLayoutMap } from './simple-header-layout'; -interface ITreeLayoutBaseHeadNode { - id: number; - // dimensionKey: string; - // // title: string; - // indicatorKey?: string; - value: string; - children: ITreeLayoutHeadNode[] | undefined; - columns?: any; //兼容ListTable情况 simple-header-layout中增加了columnTree - level: number; - /** 节点跨占层数 如汇总节点跨几层维度 */ - levelSpan: number; - startIndex: number; - size: number; //对应到colSpan或者rowSpan - // parsing?: 'img' | 'link' | 'video' | 'templateLink'; - startInTotal: number; - // headerStyle:HeaderStyleOption| null; - customRender?: ICustomRender; - - hierarchyState: HierarchyState; - headerIcon?: (string | ColumnIconOption)[] | ((args: CellInfo) => (string | ColumnIconOption)[]); -} - -interface ITreeLayoutDimensionHeadNode extends ITreeLayoutBaseHeadNode { - dimensionKey: string; -} -interface ITreeLayoutIndicatorHeadNode extends ITreeLayoutBaseHeadNode { - indicatorKey: string; -} -export type ITreeLayoutHeadNode = Either; -export class DimensionTree { - sharedVar: { seqId: number }; - // 每一个值对应的序号 结果缓存 - // cache: { - // [propName: string]: any; - // }; - //树形展示 会将非叶子节点单独展示一行 所以size会增加非叶子节点的个数 - sizeIncludeParent = false; - rowExpandLevel: number; - hierarchyType: 'grid' | 'tree'; - tree: ITreeLayoutHeadNode = { - id: 0, - dimensionKey: '', - // title: '', - value: '', - children: [], - level: -1, - levelSpan: 1, - startIndex: 0, - size: 0, - startInTotal: 0, - hierarchyState: undefined - }; - - totalLevel = 0; - - // blockLevel: number = 0; - - // blockStartIndexMap: Map = new Map(); - // blockEndIndexMap: Map = new Map(); - dimensionKeys: NumberMap = new NumberMap(); - // dimensions: IDimension[] | undefined;//目前用不到这个 - - cache: Map = new Map(); - constructor( - tree: ITreeLayoutHeadNode[], - sharedVar: { seqId: number }, - hierarchyType: 'grid' | 'tree' = 'grid', - rowExpandLevel: number = undefined - ) { - this.sizeIncludeParent = rowExpandLevel !== null && rowExpandLevel !== undefined; - this.rowExpandLevel = rowExpandLevel; - this.hierarchyType = hierarchyType; - this.sharedVar = sharedVar; - this.reset(tree); - } - - reset(tree: ITreeLayoutHeadNode[], updateTreeNode = false) { - // 清空缓存的计算 - // this.cache = {}; - // this.dimensions = dimensions; - this.cache.clear(); - this.dimensionKeys = new NumberMap(); - this.tree.children = tree as ITreeLayoutHeadNode[]; - // const re = { totalLevel: 0 }; - // if (updateTreeNode) this.updateTreeNode(this.tree, 0, re, this.tree); - // else - this.setTreeNode(this.tree, 0, this.tree); - this.totalLevel = this.dimensionKeys.count(); - } - setTreeNode(node: ITreeLayoutHeadNode, startIndex: number, parent: ITreeLayoutHeadNode): number { - node.startIndex = startIndex; - node.startInTotal = (parent.startInTotal ?? 0) + node.startIndex; - // if (node.dimensionKey) { - // !this.dimensionKeys.contain(node.dimensionKey) && - // this.dimensionKeys.put(node.level, node.dimensionKey); - // if (!node.id) node.id = ++seqId; - // } - if (node.dimensionKey ?? node.indicatorKey) { - !this.dimensionKeys.contain(node.indicatorKey ? IndicatorDimensionKeyPlaceholder : node.dimensionKey) && - this.dimensionKeys.put(node.level, node.indicatorKey ? IndicatorDimensionKeyPlaceholder : node.dimensionKey); - if (!node.id) { - node.id = ++this.sharedVar.seqId; - } - } - let size = node.dimensionKey ? (this.sizeIncludeParent ? 1 : 0) : 0; - const children = node.children || node.columns; - //平铺展示 分析所有层级 - if (this.hierarchyType === 'grid') { - if (children?.length >= 1) { - children.forEach((n: any) => { - n.level = (node.level ?? 0) + 1; - size += this.setTreeNode(n, size, node); - }); - } else { - size = 1; - // re.totalLevel = Math.max(re.totalLevel, (node.level ?? -1) + 1); - } - } else if (node.hierarchyState === HierarchyState.expand && children?.length >= 1) { - //树形展示 有子节点 且下一层需要展开 - children.forEach((n: any) => { - n.level = (node.level ?? 0) + 1; - size += this.setTreeNode(n, size, node); - }); - } else if (node.hierarchyState === HierarchyState.collapse && children?.length >= 1) { - //树形展示 有子节点 且下一层不需要展开 - children.forEach((n: any) => { - n.level = (node.level ?? 0) + 1; - this.setTreeNode(n, size, node); - }); - } else if (!node.hierarchyState && node.level + 1 < this.rowExpandLevel && children?.length >= 1) { - //树形展示 有子节点 且下一层需要展开 - node.hierarchyState = HierarchyState.expand; - children.forEach((n: any) => { - n.level = (node.level ?? 0) + 1; - size += this.setTreeNode(n, size, node); - }); - } else if (children?.length >= 1) { - //树形展示 有子节点 且下一层不需要展开 - node.hierarchyState = HierarchyState.collapse; - children.forEach((n: any) => { - n.level = (node.level ?? 0) + 1; - this.setTreeNode(n, size, node); - }); - } else { - //树形展示 无children子节点。但不能确定是最后一层的叶子节点 totalLevel还不能确定是计算完整棵树的整体深度 - node.hierarchyState = HierarchyState.none; - size = 1; - // re.totalLevel = Math.max(re.totalLevel, (node.level ?? -1) + 1); - } - - node.size = size; - // startInTotal = parent.startIndex + prevStartIndex - return size; - } - getTreePath(index: number, maxDeep = 30): Array { - const path: any[] = []; - this.searchPath(index, this.tree, path, maxDeep); - path.shift(); - return path; - } - - getTreePathByCellIds(ids: LayoutObjectId[]): Array { - const path: any[] = []; - let nodes = this.tree.children; - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - const pathNode = this.findNodeById(nodes, id); - if (pathNode) { - path.push(pathNode); - nodes = pathNode.children; - } else { - break; - } - } - // path.shift(); - return path; - } - findNodeById(nodes: ITreeLayoutHeadNode[], id: LayoutObjectId) { - return nodes.find(node => { - return node.id === id; - }); - } - searchPath(index: number, node: ITreeLayoutHeadNode, path: Array, maxDeep: number) { - if (!node) { - return; +export function checkHasAggregation(layoutMap: SimpleHeaderLayoutMap) { + const columnObjects = layoutMap.columnObjects; + for (let i = 0; i < columnObjects.length; i++) { + const column = columnObjects[i]; + if ((column as ColumnData)?.aggregation) { + return true; } - if (index < node.startIndex || index >= node.startIndex + node.size) { - return; - } - path.push(node); - if (!node.children || node.children.length === 0 || node.level >= maxDeep) { - return; - } - - // const cIndex = index - node.startIndex; - // for (let i = 0; i < node.children.length; i++) { - // const element = node.children[i]; - // if (cIndex >= element.startIndex && cIndex < element.startIndex + element.size) { - // this.searchPath(cIndex, element, path, maxDeep); - // break; - // } - // } - - // use dichotomy to optimize search performance - const cIndex = index - node.startIndex; - - if (this.cache.has(node.level + 1)) { - const cacheNode = this.cache.get(node.level + 1); - if (cIndex >= cacheNode.startIndex && cIndex < cacheNode.startIndex + cacheNode.size) { - this.searchPath(cIndex, cacheNode, path, maxDeep); - return; - } - } - - let left = 0; - let right = node.children.length - 1; - - while (left <= right) { - const middle = Math.floor((left + right) / 2); - const element = node.children[middle]; - - if (cIndex >= element.startIndex && cIndex < element.startIndex + element.size) { - this.cache.set(element.level, element); - const deleteLevels: number[] = []; - this.cache.forEach((node, key) => { - if (key > element.level) { - deleteLevels.push(key); - } - }); - deleteLevels.forEach(key => { - this.cache.delete(key); - }); - this.searchPath(cIndex, element, path, maxDeep); - break; - } else if (cIndex < element.startIndex) { - right = middle - 1; - } else { - left = middle + 1; - } - } - return; - } - /** - * 将该树中 层级为level 的sourceIndex处的节点移动到targetIndex位置 - * @param level - * @param sourceIndex - * @param targetIndex - */ - movePosition(level: number, sourceIndex: number, targetIndex: number) { - // let sourceNode: IPivotLayoutHeadNode; - let parNode: ITreeLayoutHeadNode; - let sourceSubIndex: number; - let targetSubIndex: number; - /** - * 对parNode的子节点第subIndex处的node节点 进行判断是否为sourceIndex或者targetIndex - * 如果是 则记录下subIndex 以对parNode中个节点位置进行移位 - * @param node - * @param subIndex - * @returns - */ - const findTargetNode = (node: ITreeLayoutHeadNode, subIndex: number) => { - if (sourceSubIndex !== undefined && targetSubIndex !== undefined) { - return; - } - if (node.level === level) { - if (node.startInTotal === sourceIndex) { - // sourceNode = node; - sourceSubIndex = subIndex; - } - // if (node.startInTotal === targetIndex) targetSubIndex = subIndex; - // 判断targetIndex是否在node的范围内 将当前node的subIndex记为targetSubIndex - if (node.startInTotal <= targetIndex && targetIndex <= node.startInTotal + node.size - 1) { - targetSubIndex = subIndex; - } - } - const children = node.children || node.columns; - if (children && node.level < level) { - parNode = node; - for (let i = 0; i < children.length; i++) { - if ( - (sourceIndex >= children[i].startInTotal && sourceIndex <= children[i].startInTotal + children[i].size) || - (targetIndex >= children[i].startInTotal && targetIndex <= children[i].startInTotal + children[i].size) - ) { - findTargetNode(children[i], i); - } - } - } - }; - findTargetNode(this.tree, 0); - - //对parNode子节点位置进行移位【根据sourceSubIndex和targetSubIndex】 - const children = parNode.children || parNode.columns; - const sourceColumns = children.splice(sourceSubIndex, 1); - sourceColumns.unshift(targetSubIndex as any, 0 as any); - Array.prototype.splice.apply(children, sourceColumns); } - /** 获取纯净树结构 没有level size index这些属性 */ - getCopiedTree() { - const children = cloneDeep(this.tree.children); - clearNode(children); - return children; - } -} - -//#region 为方法getLayoutRowTree提供的类型和工具方法 -export type LayouTreeNode = { - dimensionKey?: string; - indicatorKey?: string; - value: string; - hierarchyState: HierarchyState; - children?: LayouTreeNode[]; -}; - -export function generateLayoutTree(tree: LayouTreeNode[], children: ITreeLayoutHeadNode[]) { - children?.forEach((node: ITreeLayoutHeadNode) => { - const diemnsonNode: { - dimensionKey?: string; - indicatorKey?: string; - value: string; - hierarchyState: HierarchyState; - children: any; - } = { - dimensionKey: node.dimensionKey, - indicatorKey: node.indicatorKey, - value: node.value, - hierarchyState: node.hierarchyState, - children: undefined - }; - tree.push(diemnsonNode); - if (node.children) { - diemnsonNode.children = []; - generateLayoutTree(diemnsonNode.children, node.children); - } - }); + return false; } -//#endregion -//#region 为方法getLayoutRowTreeCount提的工具方法 -export function countLayoutTree(children: { children?: any }[], countParentNode: boolean) { +export function checkHasAggregationOnTop(layoutMap: SimpleHeaderLayoutMap) { + const columnObjects = layoutMap.columnObjects; let count = 0; - children?.forEach((node: ITreeLayoutHeadNode) => { - if (countParentNode) { - count++; - } else { - if (!node.children || node.children.length === 0) { - count++; + for (let i = 0; i < columnObjects.length; i++) { + const column = columnObjects[i]; + if ((column as ColumnData)?.aggregation) { + if (Array.isArray((column as ColumnData)?.aggregation)) { + count = Math.max( + count, + ((column as ColumnData).aggregation as Array).filter(item => item.showOnTop === true).length + ); + } else if (((column as ColumnData).aggregation as Aggregation).showOnTop === true) { + count = Math.max(count, 1); } } - if (node.children) { - count += countLayoutTree(node.children, countParentNode); - } - }); + } return count; } -//#endregion - -export function dealHeader( - hd: ITreeLayoutHeadNode, - _headerCellIds: number[][], - results: HeaderData[], - roots: number[], - row: number, - layoutMap: PivotHeaderLayoutMap -) { - // const col = this._columns.length; - const id = hd.id; - const dimensionInfo: IDimension = - (layoutMap.rowsDefine?.find(dimension => - typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey - ) as IDimension) ?? - (layoutMap.columnsDefine?.find(dimension => - typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey - ) as IDimension); - const indicatorInfo = layoutMap.indicatorsDefine?.find(indicator => { - if (typeof indicator === 'string') { - return false; - } - if (hd.indicatorKey) { - return indicator.indicatorKey === hd.indicatorKey; - } - return indicator.title === hd.value; - }) as IIndicator; - const cell: HeaderData = { - id, - title: hd.value ?? indicatorInfo?.title, - field: hd.dimensionKey, - style: - typeof (indicatorInfo ?? dimensionInfo)?.headerStyle === 'function' - ? (indicatorInfo ?? dimensionInfo)?.headerStyle - : Object.assign({}, (indicatorInfo ?? dimensionInfo)?.headerStyle), - headerType: indicatorInfo?.headerType ?? dimensionInfo?.headerType ?? 'text', - headerIcon: indicatorInfo?.headerIcon ?? dimensionInfo?.headerIcon, - // define: hd, - define: Object.assign({}, hd, indicatorInfo ?? dimensionInfo), - fieldFormat: indicatorInfo?.headerFormat ?? dimensionInfo?.headerFormat, - // iconPositionList:[] - dropDownMenu: indicatorInfo?.dropDownMenu ?? dimensionInfo?.dropDownMenu, - pivotInfo: { - value: hd.value, - dimensionKey: hd.dimensionKey, - isPivotCorner: false - // customInfo: dimensionInfo?.customInfo - }, - width: (dimensionInfo as IRowDimension)?.width, - minWidth: (dimensionInfo as IRowDimension)?.minWidth, - maxWidth: (dimensionInfo as IRowDimension)?.maxWidth, - showSort: indicatorInfo?.showSort ?? dimensionInfo?.showSort, - description: dimensionInfo?.description - }; - - if (indicatorInfo) { - //收集indicatorDimensionKey 提到了构造函数中 - // this.indicatorDimensionKey = dimensionInfo.dimensionKey; - if (indicatorInfo.customRender) { - hd.customRender = indicatorInfo.customRender; - } - if (!isValid(layoutMap._indicators?.find(indicator => indicator.indicatorKey === indicatorInfo.indicatorKey))) { - layoutMap._indicators?.push({ - id: ++layoutMap.sharedVar.seqId, - indicatorKey: indicatorInfo.indicatorKey, - field: indicatorInfo.indicatorKey, - fieldFormat: indicatorInfo?.format, - cellType: indicatorInfo?.cellType ?? (indicatorInfo as any)?.columnType ?? 'text', - chartModule: 'chartModule' in indicatorInfo ? indicatorInfo.chartModule : null, - chartSpec: 'chartSpec' in indicatorInfo ? indicatorInfo.chartSpec : null, - sparklineSpec: 'sparklineSpec' in indicatorInfo ? indicatorInfo.sparklineSpec : null, - style: indicatorInfo?.style, - icon: indicatorInfo?.icon, - define: Object.assign({}, hd, indicatorInfo, { - dragHeader: dimensionInfo?.dragHeader - }), - width: indicatorInfo?.width, - minWidth: indicatorInfo?.minWidth, - maxWidth: indicatorInfo?.maxWidth, - disableColumnResize: indicatorInfo?.disableColumnResize - }); - } - } else if (hd.indicatorKey) { - //兼容当某个指标没有设置在dimension.indicators中 - if (!isValid(layoutMap._indicators?.find(indicator => indicator.indicatorKey === hd.indicatorKey))) { - layoutMap._indicators?.push({ - id: ++layoutMap.sharedVar.seqId, - indicatorKey: hd.indicatorKey, - field: hd.indicatorKey, - cellType: 'text', - define: Object.assign({}, hd) - }); - } - } - // if (dimensionInfo.indicators) { - // layoutMap.hideIndicatorName = dimensionInfo.hideIndicatorName ?? false; - // layoutMap.indicatorsAsCol = dimensionInfo.indicatorsAsCol ?? true; - // } - results[id] = cell; - layoutMap._headerObjects[id] = cell; - _headerCellIds[row][layoutMap.colIndex] = id; - for (let r = row - 1; r >= 0; r--) { - _headerCellIds[r][layoutMap.colIndex] = roots[r]; - } - // 处理汇总小计跨维度层级的情况 - if ((hd as any).levelSpan > 1) { - for (let i = 1; i < (hd as any).levelSpan; i++) { - if (!_headerCellIds[row + i]) { - _headerCellIds[row + i] = []; +export function checkHasAggregationOnBottom(layoutMap: SimpleHeaderLayoutMap) { + const columnObjects = layoutMap.columnObjects; + let count = 0; + for (let i = 0; i < columnObjects.length; i++) { + const column = columnObjects[i]; + if ((column as ColumnData)?.aggregation) { + if (Array.isArray((column as ColumnData)?.aggregation)) { + count = Math.max( + count, + ((column as ColumnData).aggregation as Array).filter(item => item.showOnTop === false).length + ); + } else if (((column as ColumnData).aggregation as Aggregation).showOnTop === false) { + count = Math.max(count, 1); } - _headerCellIds[row + i][layoutMap.colIndex] = id; - } - } - - if ((hd as ITreeLayoutHeadNode).children?.length >= 1) { - layoutMap - ._addHeaders(_headerCellIds, row + ((hd as any).levelSpan ?? 1), (hd as ITreeLayoutHeadNode).children ?? [], [ - ...roots, - ...Array((hd as any).levelSpan ?? 1).fill(id) - ]) - .forEach(c => results.push(c)); - } else { - // columns.push([""])//代码一个路径 - for (let r = row + 1; r < _headerCellIds.length; r++) { - _headerCellIds[r][layoutMap.colIndex] = id; - - // if ((hd as any).levelSpan > 1) { - // for (let i = 1; i < (hd as any).levelSpan; i++) { - // _headerCellIds[r + i][layoutMap.colIndex] = id; - // } - // } - } - layoutMap.colIndex++; - } -} - -export function dealHeaderForTreeMode( - hd: ITreeLayoutHeadNode, - _headerCellIds: number[][], - results: HeaderData[], - roots: number[], - row: number, - totalLevel: number, - show: boolean, - dimensions: (IDimension | string)[], - layoutMap: PivotHeaderLayoutMap -) { - const id = hd.id; - // const dimensionInfo: IDimension = - // (this.rowsDefine?.find(dimension => - // typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey - // ) as IDimension) ?? - // (this.columnsDefine?.find(dimension => - // typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey - // ) as IDimension); - const dimensionInfo: IDimension = dimensions.find(dimension => - typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey - ) as IDimension; - - const cell: HeaderData = { - id, - title: hd.value, - field: hd.dimensionKey as FieldData, - //如果不是整棵树的叶子节点,都靠左显示 - style: - hd.level + 1 === totalLevel || typeof dimensionInfo?.headerStyle === 'function' - ? dimensionInfo?.headerStyle - : Object.assign({}, dimensionInfo?.headerStyle, { textAlign: 'left' }), - headerType: dimensionInfo?.headerType ?? 'text', - headerIcon: dimensionInfo?.headerIcon, - define: Object.assign(hd, { - linkJump: (dimensionInfo as ILinkDimension)?.linkJump, - linkDetect: (dimensionInfo as ILinkDimension)?.linkDetect, - templateLink: (dimensionInfo as ILinkDimension)?.templateLink, - - // image相关 to be fixed - keepAspectRatio: (dimensionInfo as IImageDimension)?.keepAspectRatio ?? false, - imageAutoSizing: (dimensionInfo as IImageDimension)?.imageAutoSizing, - - headerCustomRender: dimensionInfo?.headerCustomRender, - headerCustomLayout: dimensionInfo?.headerCustomLayout, - dragHeader: dimensionInfo?.dragHeader, - disableHeaderHover: !!dimensionInfo?.disableHeaderHover, - disableHeaderSelect: !!dimensionInfo?.disableHeaderSelect - }), //这里不能新建对象,要用hd保持引用关系 - fieldFormat: dimensionInfo?.headerFormat, - // iconPositionList:[] - dropDownMenu: dimensionInfo?.dropDownMenu, - pivotInfo: { - value: hd.value, - dimensionKey: hd.dimensionKey as string, - isPivotCorner: false - // customInfo: dimensionInfo?.customInfo - }, - hierarchyLevel: hd.level, - dimensionTotalLevel: totalLevel, - hierarchyState: hd.level + 1 === totalLevel ? undefined : hd.hierarchyState, - width: (dimensionInfo as IRowDimension)?.width, - minWidth: (dimensionInfo as IRowDimension)?.minWidth, - maxWidth: (dimensionInfo as IRowDimension)?.maxWidth, - parentCellId: roots[roots.length - 1] - }; - - results[id] = cell; - // this._cellIdDiemnsionMap.set(id, { - // dimensionKey: hd.dimensionKey, - // value: hd.value - // }); - layoutMap._headerObjects[id] = cell; - _headerCellIds[row][layoutMap.colIndex] = id; - for (let r = row - 1; r >= 0; r--) { - _headerCellIds[r][layoutMap.colIndex] = roots[r]; - } - if (hd.hierarchyState === HierarchyState.expand && (hd as ITreeLayoutHeadNode).children?.length >= 1) { - //row传值 colIndex++和_addHeaders有区别 - show && layoutMap.colIndex++; - layoutMap - ._addHeadersForTreeMode( - _headerCellIds, - row, - (hd as ITreeLayoutHeadNode).children ?? [], - [...roots, id], - totalLevel, - show && hd.hierarchyState === HierarchyState.expand, //当前节点show 且当前节点状态为展开 则传给子节点为show:true - dimensions - ) - .forEach(c => results.push(c)); - } else { - // columns.push([""])//代码一个路径 - show && layoutMap.colIndex++; - for (let r = row + 1; r < _headerCellIds.length; r++) { - _headerCellIds[r][layoutMap.colIndex] = id; - } - } -} - -function clearNode(children: any) { - for (let i = 0; i < children.length; i++) { - const node = children[i]; - delete node.level; - delete node.startIndex; - delete node.id; - delete node.levelSpan; - delete node.size; - delete node.startInTotal; - const childrenNew = node.children || node.columns; - if (childrenNew) { - clearNode(childrenNew); } } + return count; } diff --git a/packages/vtable/src/layout/pivot-header-layout.ts b/packages/vtable/src/layout/pivot-header-layout.ts index 2c96f8cb3..133e0b58b 100644 --- a/packages/vtable/src/layout/pivot-header-layout.ts +++ b/packages/vtable/src/layout/pivot-header-layout.ts @@ -43,10 +43,11 @@ import { getChartSpec, getRawChartSpec, isCartesianChart, - isHasCartesianChartInline + isHasCartesianChartInline, + isShareChartSpec } from './chart-helper/get-chart-spec'; -import type { LayouTreeNode, ITreeLayoutHeadNode } from './layout-helper'; -import { DimensionTree, countLayoutTree, dealHeader, dealHeaderForTreeMode, generateLayoutTree } from './layout-helper'; +import type { ITreeLayoutHeadNode, LayouTreeNode } from './tree-helper'; +import { DimensionTree, countLayoutTree, dealHeader, dealHeaderForTreeMode, generateLayoutTree } from './tree-helper'; import type { Dataset } from '../dataset/dataset'; import { cloneDeep, isArray, isValid } from '@visactor/vutils'; import type { TextStyle } from '../body-helper/style'; @@ -1462,7 +1463,7 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { getRecordStartRowByRecordIndex(index: number): number { return this.columnHeaderLevelCount + index; } - getRecordIndexByCell(col: number, row: number): number { + getRecordShowIndexByCell(col: number, row: number): number { return undefined; } // getCellRangeTranspose(): CellRange { @@ -2312,6 +2313,9 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { const indicatorKey = paths.rowHeaderPaths.find(rowPath => rowPath.indicatorKey)?.indicatorKey; indicatorObj = this._indicators?.find(indicator => indicator.indicatorKey === indicatorKey); } + if (typeof indicatorObj?.chartSpec === 'function') { + return; + } indicatorObj && (indicatorObj.chartInstance = chartInstance); } @@ -2407,6 +2411,13 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { getRawChartSpec(col: number, row: number): any { return getRawChartSpec(col, row, this); } + + getChartSpec(col: number, row: number): any { + return getChartSpec(col, row, this); + } + isShareChartSpec(col: number, row: number): any { + return isShareChartSpec(col, row, this); + } getChartDataId(col: number, row: number): any { return getChartDataId(col, row, this); } @@ -2540,9 +2551,7 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { } return null; } - getChartSpec(col: number, row: number): any { - return getChartSpec(col, row, this); - } + /** 将_selectedDataItemsInChart保存的数据状态同步到各个图表实例中 */ _generateChartState() { const state = { @@ -2818,13 +2827,25 @@ export class PivotHeaderLayoutMap implements LayoutMapAPI { return indicatorInfo; } /** 获取行头树结构 */ + getLayoutColumnTree() { + const tree: LayouTreeNode[] = []; + const children = this.columnDimensionTree.tree.children; + generateLayoutTree(tree, children); + return tree; + } + /** 获取行头树结构 */ getLayoutRowTree() { const tree: LayouTreeNode[] = []; const children = this.rowDimensionTree.tree.children; generateLayoutTree(tree, children); return tree; } - + /** 获取列头总共的行数(全部展开情况下) */ + getLayoutColumnTreeCount() { + const children = this.columnDimensionTree.tree.children; + const mainTreeCount = countLayoutTree(children, this.rowHierarchyType === 'tree'); + return mainTreeCount; + } /** 获取行头总共的行数(全部展开情况下) */ getLayoutRowTreeCount() { const children = this.rowDimensionTree.tree.children; diff --git a/packages/vtable/src/layout/row-height-map.ts b/packages/vtable/src/layout/row-height-map.ts index 130c80a71..bcb504eb0 100644 --- a/packages/vtable/src/layout/row-height-map.ts +++ b/packages/vtable/src/layout/row-height-map.ts @@ -25,6 +25,7 @@ export class NumberRangeMap { } clear() { + this._keys = []; this.data.clear(); this.cumulativeSum.clear(); this.difference.clear(); diff --git a/packages/vtable/src/layout/simple-header-layout.ts b/packages/vtable/src/layout/simple-header-layout.ts index ffecfe47c..17b8a13cd 100644 --- a/packages/vtable/src/layout/simple-header-layout.ts +++ b/packages/vtable/src/layout/simple-header-layout.ts @@ -2,7 +2,15 @@ import { isValid } from '@visactor/vutils'; import type { ListTable } from '../ListTable'; import { DefaultSparklineSpec } from '../tools/global'; -import type { CellAddress, CellRange, CellLocation, IListTableCellHeaderPaths, LayoutObjectId } from '../ts-types'; +import type { + CellAddress, + CellRange, + CellLocation, + IListTableCellHeaderPaths, + LayoutObjectId, + AggregationType, + Aggregation +} from '../ts-types'; import type { ColumnsDefine, TextColumnDefine } from '../ts-types/list-table/define'; import type { ColumnData, @@ -12,7 +20,9 @@ import type { WidthData } from '../ts-types/list-table/layout-map/api'; import { checkHasChart, getChartDataId } from './chart-helper/get-chart-spec'; -import { DimensionTree } from './layout-helper'; +import { checkHasAggregation, checkHasAggregationOnBottom, checkHasAggregationOnTop } from './layout-helper'; +import type { Aggregator } from '../dataset/statistics-helper'; +import { DimensionTree } from './tree-helper'; // import { EmptyDataCache } from './utils'; // let seqId = 0; @@ -34,6 +44,9 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { _showHeader = true; _recordsCount = 0; _table: ListTable; + _hasAggregation: boolean = false; + _hasAggregationOnTopCount: number = 0; + _hasAggregationOnBottomCount: number = 0; // 缓存行号列号对应的cellRange 需要注意当表头位置拖拽后 这个缓存的行列号已不准确 进行重置 private _cellRangeMap: Map; //存储单元格的行列号范围 针对解决是否为合并单元格情况 constructor(table: ListTable, columns: ColumnsDefine, showHeader: boolean, hierarchyIndent: number) { @@ -49,6 +62,9 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { o[e.id as number] = e; return o; }, {} as { [key in LayoutObjectId]: HeaderData }); + this._hasAggregation = checkHasAggregation(this); + this._hasAggregationOnBottomCount = checkHasAggregationOnBottom(this); + this._hasAggregationOnTopCount = checkHasAggregationOnTop(this); // this._headerObjectFieldKey = this._headerObjects.reduce((o, e) => { // o[e.fieldKey] = e; // return o; @@ -78,6 +94,160 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { } return false; } + isAggregation(col: number, row: number): boolean { + // const column = this.getBody(col, row); + // const aggregation = column.aggregation; + if (this.hasAggregation) { + if (this.hasAggregationOnBottomCount) { + if (this.transpose) { + if (col >= this.colCount - this.hasAggregationOnBottomCount) { + return true; + } + } else { + if (row >= this.rowCount - this.hasAggregationOnBottomCount) { + return true; + } + } + } + if (this.hasAggregationOnTopCount) { + if (this.transpose) { + if (col >= this.rowHeaderLevelCount && col < this.rowHeaderLevelCount + this.hasAggregationOnTopCount) { + return true; + } + } else { + if (row >= this.columnHeaderLevelCount && row < this.columnHeaderLevelCount + this.hasAggregationOnTopCount) { + return true; + } + } + } + } + return false; + } + isTopAggregation(col: number, row: number): boolean { + if (this.hasAggregationOnTopCount) { + if (this.transpose) { + if (col >= this.rowHeaderLevelCount && col < this.rowHeaderLevelCount + this.hasAggregationOnTopCount) { + return true; + } + } else { + if (row >= this.columnHeaderLevelCount && row < this.columnHeaderLevelCount + this.hasAggregationOnTopCount) { + return true; + } + } + } + return false; + } + isBottomAggregation(col: number, row: number): boolean { + if (this.hasAggregationOnBottomCount) { + if (this.transpose) { + if (col >= this.colCount - this.hasAggregationOnBottomCount) { + return true; + } + } else { + if (row >= this.rowCount - this.hasAggregationOnBottomCount) { + return true; + } + } + } + return false; + } + get hasAggregation() { + return this._hasAggregation; + } + + get hasAggregationOnTopCount() { + return this._hasAggregationOnTopCount; + } + + get hasAggregationOnBottomCount() { + return this._hasAggregationOnBottomCount; + } + + getAggregators(col: number, row: number) { + const column = this.getBody(col, row); + const aggregators = column.aggregator; + return aggregators; + } + getAggregatorOnTop(col: number, row: number) { + const column = this.getBody(col, row); + const aggregators = column.aggregator; + const aggregation = column.aggregation; + if (Array.isArray(aggregation)) { + const topAggregationIndexs = aggregation.reduce((indexs, agg, index) => { + if (agg.showOnTop) { + indexs.push(index); + } + return indexs; + }, []); + const topAggregators = topAggregationIndexs.map(index => aggregators[index]); + if (this.transpose) { + return (topAggregators as Aggregator[])[col - this.rowHeaderLevelCount]; + } + return (topAggregators as Aggregator[])[row - this.columnHeaderLevelCount]; + } + if (this.transpose && col - this.rowHeaderLevelCount === 0) { + return (aggregation as Aggregation)?.showOnTop ? (aggregators as Aggregator) : null; + } else if (!this.transpose && row - this.columnHeaderLevelCount === 0) { + return (aggregation as Aggregation)?.showOnTop ? (aggregators as Aggregator) : null; + } + return null; + } + + getAggregatorOnBottom(col: number, row: number) { + const column = this.getBody(col, row); + const aggregators = column.aggregator; + const aggregation = column.aggregation; + if (Array.isArray(aggregation)) { + const bottomAggregationIndexs = aggregation.reduce((indexs, agg, index) => { + if (agg.showOnTop === false) { + indexs.push(index); + } + return indexs; + }, []); + const bottomAggregators = bottomAggregationIndexs.map(index => aggregators[index]); + if (this.transpose) { + return (bottomAggregators as Aggregator[])[col - (this.colCount - this.hasAggregationOnBottomCount)]; + } + return (bottomAggregators as Aggregator[])[row - (this.rowCount - this.hasAggregationOnBottomCount)]; + } + if (this.transpose && col - (this.colCount - this.hasAggregationOnBottomCount) === 0) { + return (aggregation as Aggregation)?.showOnTop === false ? (aggregators as Aggregator) : null; + } else if (!this.transpose && row - (this.rowCount - this.hasAggregationOnBottomCount) === 0) { + return (aggregation as Aggregation)?.showOnTop === false ? (aggregators as Aggregator) : null; + } + return null; + } + /** + * 获取单元格所在行或者列中的聚合值的单元格地址 + * @param col + * @param row + * @returns + */ + getCellAddressHasAggregator(col: number, row: number) { + const cellAddrs = []; + if (this.transpose) { + const topCount = this.hasAggregationOnTopCount; + for (let i = 0; i < topCount; i++) { + cellAddrs.push({ col: this.headerLevelCount + i, row }); + } + + const bottomCount = this.hasAggregationOnBottomCount; + for (let i = 0; i < bottomCount; i++) { + cellAddrs.push({ col: this.rowCount - bottomCount + i, row }); + } + } else { + const topCount = this.hasAggregationOnTopCount; + for (let i = 0; i < topCount; i++) { + cellAddrs.push({ col, row: this.headerLevelCount + i }); + } + + const bottomCount = this.hasAggregationOnBottomCount; + for (let i = 0; i < bottomCount; i++) { + cellAddrs.push({ col, row: this.rowCount - bottomCount + i }); + } + } + return cellAddrs; + } getCellLocation(col: number, row: number): CellLocation { if (this.isHeader(col, row)) { if (this.transpose) { @@ -405,13 +575,6 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { const id = this.getCellId(col, row); return this._headerObjectMap[id as number]!; } - getHeaderFieldKey(col: number, row: number) { - const id = this.getCellId(col, row); - return ( - this._headerObjectMap[id as number]?.fieldKey || - (this.transpose ? this._columns[row]?.fieldKey : this._columns[col]?.fieldKey) - ); - } getHeaderField(col: number, row: number) { const id = this.getCellId(col, row); return ( @@ -630,20 +793,23 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { range1.end.row === range2.end.row ); } - getRecordIndexByCell(col: number, row: number): number { + getRecordShowIndexByCell(col: number, row: number): number { + const skipRowCount = this.hasAggregationOnTopCount ? this.headerLevelCount + 1 : this.headerLevelCount; if (this.transpose) { - if (col < this.headerLevelCount) { + if (col < skipRowCount) { return -1; } - return col - this.headerLevelCount; + return col - skipRowCount; } - if (row < this.headerLevelCount) { + + if (row < skipRowCount) { return -1; } - return row - this.headerLevelCount; + return row - skipRowCount; } getRecordStartRowByRecordIndex(index: number): number { - return this.headerLevelCount + index; + const skipRowCount = this.hasAggregationOnTopCount ? this.headerLevelCount + 1 : this.headerLevelCount; + return skipRowCount + index; } private _addHeaders( row: number, @@ -662,8 +828,7 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { // captionIcon, headerIcon: hd.headerIcon, field: (hd as ColumnDefine).field, - fieldKey: (hd as ColumnDefine)?.fieldKey, - fieldFormat: (hd as ColumnDefine).fieldFormat, + // fieldFormat: (hd as ColumnDefine).fieldFormat, style: hd.headerStyle, headerType: hd.headerType ?? 'text', dropDownMenu: hd.dropDownMenu, @@ -674,21 +839,23 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { results[id] = cell; for (let r = row - 1; r >= 0; r--) { - this._headerCellIds[r][col] = roots[r]; + this._headerCellIds[r] && (this._headerCellIds[r][col] = roots[r]); } if (!hideColumnsSubHeader) { rowCells[col] = id; - } else { + } else if (this._headerCellIds[row - 1]) { rowCells[col] = this._headerCellIds[row - 1][col]; } if (hd.columns) { - this._addHeaders(row + 1, hd.columns, [...roots, id], hd.hideColumnsSubHeader).forEach(c => results.push(c)); + this._addHeaders(row + 1, hd.columns, [...roots, id], hd.hideColumnsSubHeader || hideColumnsSubHeader).forEach( + c => results.push(c) + ); } else { const colDef = hd; this._columns.push({ id: this.seqId++, field: colDef.field, - fieldKey: colDef.fieldKey, + // fieldKey: colDef.fieldKey, fieldFormat: colDef.fieldFormat, width: colDef.width, minWidth: colDef.minWidth, @@ -701,7 +868,8 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { style: colDef.style, define: colDef, columnWidthComputeMode: colDef.columnWidthComputeMode, - disableColumnResize: colDef?.disableColumnResize + disableColumnResize: colDef?.disableColumnResize, + aggregation: this._getAggregationForColumn(colDef, col) }); for (let r = row + 1; r < this._headerCellIds.length; r++) { this._headerCellIds[r][col] = id; @@ -710,6 +878,33 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { }); return results; } + private _getAggregationForColumn(colDef: ColumnDefine, col: number) { + let aggregation; + if (colDef.aggregation) { + aggregation = colDef.aggregation; + } else if (this._table.options.aggregation) { + if (typeof this._table.options.aggregation === 'function') { + aggregation = this._table.options.aggregation({ + col: col, + field: colDef.field as string + }); + } else { + aggregation = this._table.options.aggregation; + } + } + if (aggregation) { + if (Array.isArray(aggregation)) { + return aggregation.map(item => { + if (!isValid(item.showOnTop)) { + item.showOnTop = false; + } + return item; + }); + } + return Object.assign({ showOnTop: false }, aggregation); + } + return null; + } private _newRow(row: number, hideColumnsSubHeader = false): number[] { //如果当前行已经有数组对象 将上一行的id内容补全到当前行上 if (this._headerCellIds[row]) { @@ -909,6 +1104,9 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { } setChartInstance(_col: number, _row: number, chartInstance: any) { const columnObj = this.transpose ? this._columns[_row] : this._columns[_col]; + if (typeof columnObj.chartSpec === 'function') { + return; + } columnObj.chartInstance = chartInstance; } @@ -932,9 +1130,34 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { getChartAxes(col: number, row: number): any[] { return []; } + /** 共享chartSpec 非函数 */ + isShareChartSpec(col: number, row: number): boolean { + const body = this.getBody(col, row); + const chartSpec = body?.chartSpec; + if (typeof chartSpec === 'function') { + return false; + } + return true; + } + getChartSpec(col: number, row: number) { + return this.getRawChartSpec(col, row); + } getRawChartSpec(col: number, row: number): any { const body = this.getBody(col, row); - return body?.chartSpec; + const chartSpec = body?.chartSpec; + if (typeof chartSpec === 'function') { + // 动态组织spec + const arg = { + col, + row, + dataValue: this._table.getCellOriginValue(col, row) || '', + value: this._table.getCellValue(col, row) || '', + rect: this._table.getCellRangeRelativeRect(this._table.getCellRange(col, row)), + table: this._table + }; + return chartSpec(arg); + } + return chartSpec; } getChartDataId(col: number, row: number): any { return getChartDataId(col, row, this); @@ -956,4 +1179,17 @@ export class SimpleHeaderLayoutMap implements LayoutMapAPI { define.title = title; define.define.title = title; } + + getColumnByField(field: string | number): { + col: number; + columnDefine: ColumnData; + }[] { + const result = this.columnObjects?.reduce((pre: { col: number; columnDefine: ColumnData }[], cur, index) => { + if (cur.field === field) { + pre.push({ col: index, columnDefine: cur }); + } + return pre; + }, []); + return result; + } } diff --git a/packages/vtable/src/layout/tree-helper.ts b/packages/vtable/src/layout/tree-helper.ts new file mode 100644 index 000000000..c4cb04c5a --- /dev/null +++ b/packages/vtable/src/layout/tree-helper.ts @@ -0,0 +1,628 @@ +import { cloneDeep, isValid } from '@visactor/vutils'; +import { NumberMap } from '../tools/NumberMap'; +import { IndicatorDimensionKeyPlaceholder } from '../tools/global'; +import type { Either } from '../tools/helper'; +import type { + CellInfo, + ColumnIconOption, + FieldData, + HeaderData, + ICustomRender, + IDimension, + IIndicator, + IRowDimension, + LayoutObjectId +} from '../ts-types'; +import { HierarchyState } from '../ts-types'; +import type { PivotHeaderLayoutMap } from './pivot-header-layout'; +import type { ILinkDimension } from '../ts-types/pivot-table/dimension/link-dimension'; +import type { IImageDimension } from '../ts-types/pivot-table/dimension/image-dimension'; +// import { sharedVar } from './pivot-header-layout'; + +interface ITreeLayoutBaseHeadNode { + id: number; + // dimensionKey: string; + // // title: string; + // indicatorKey?: string; + value: string; + children: ITreeLayoutHeadNode[] | undefined; + columns?: any; //兼容ListTable情况 simple-header-layout中增加了columnTree + level: number; + /** 节点跨占层数 如汇总节点跨几层维度 */ + levelSpan: number; + startIndex: number; + size: number; //对应到colSpan或者rowSpan + // parsing?: 'img' | 'link' | 'video' | 'templateLink'; + startInTotal: number; + // headerStyle:HeaderStyleOption| null; + customRender?: ICustomRender; + + hierarchyState: HierarchyState; + headerIcon?: (string | ColumnIconOption)[] | ((args: CellInfo) => (string | ColumnIconOption)[]); +} + +interface ITreeLayoutDimensionHeadNode extends ITreeLayoutBaseHeadNode { + dimensionKey: string; +} +interface ITreeLayoutIndicatorHeadNode extends ITreeLayoutBaseHeadNode { + indicatorKey: string; +} +export type ITreeLayoutHeadNode = Either; +export class DimensionTree { + sharedVar: { seqId: number }; + // 每一个值对应的序号 结果缓存 + // cache: { + // [propName: string]: any; + // }; + //树形展示 会将非叶子节点单独展示一行 所以size会增加非叶子节点的个数 + sizeIncludeParent = false; + rowExpandLevel: number; + hierarchyType: 'grid' | 'tree'; + tree: ITreeLayoutHeadNode = { + id: 0, + dimensionKey: '', + // title: '', + value: '', + children: [], + level: -1, + levelSpan: 1, + startIndex: 0, + size: 0, + startInTotal: 0, + hierarchyState: undefined + }; + + totalLevel = 0; + + // blockLevel: number = 0; + + // blockStartIndexMap: Map = new Map(); + // blockEndIndexMap: Map = new Map(); + dimensionKeys: NumberMap = new NumberMap(); + // dimensions: IDimension[] | undefined;//目前用不到这个 + + cache: Map = new Map(); + constructor( + tree: ITreeLayoutHeadNode[], + sharedVar: { seqId: number }, + hierarchyType: 'grid' | 'tree' = 'grid', + rowExpandLevel: number = undefined + ) { + this.sizeIncludeParent = rowExpandLevel !== null && rowExpandLevel !== undefined; + this.rowExpandLevel = rowExpandLevel; + this.hierarchyType = hierarchyType; + this.sharedVar = sharedVar; + this.reset(tree); + } + + reset(tree: ITreeLayoutHeadNode[], updateTreeNode = false) { + // 清空缓存的计算 + // this.cache = {}; + // this.dimensions = dimensions; + this.cache.clear(); + this.dimensionKeys = new NumberMap(); + this.tree.children = tree as ITreeLayoutHeadNode[]; + // const re = { totalLevel: 0 }; + // if (updateTreeNode) this.updateTreeNode(this.tree, 0, re, this.tree); + // else + this.setTreeNode(this.tree, 0, this.tree); + this.totalLevel = this.dimensionKeys.count(); + } + setTreeNode(node: ITreeLayoutHeadNode, startIndex: number, parent: ITreeLayoutHeadNode): number { + node.startIndex = startIndex; + node.startInTotal = (parent.startInTotal ?? 0) + node.startIndex; + // if (node.dimensionKey) { + // !this.dimensionKeys.contain(node.dimensionKey) && + // this.dimensionKeys.put(node.level, node.dimensionKey); + // if (!node.id) node.id = ++seqId; + // } + if (node.dimensionKey ?? node.indicatorKey) { + !this.dimensionKeys.contain(node.indicatorKey ? IndicatorDimensionKeyPlaceholder : node.dimensionKey) && + this.dimensionKeys.put(node.level, node.indicatorKey ? IndicatorDimensionKeyPlaceholder : node.dimensionKey); + if (!node.id) { + node.id = ++this.sharedVar.seqId; + } + } + let size = node.dimensionKey ? (this.sizeIncludeParent ? 1 : 0) : 0; + const children = node.children || node.columns; + //平铺展示 分析所有层级 + if (this.hierarchyType === 'grid') { + if (children?.length >= 1) { + children.forEach((n: any) => { + n.level = (node.level ?? 0) + 1; + size += this.setTreeNode(n, size, node); + }); + } else { + size = 1; + // re.totalLevel = Math.max(re.totalLevel, (node.level ?? -1) + 1); + } + } else if (node.hierarchyState === HierarchyState.expand && children?.length >= 1) { + //树形展示 有子节点 且下一层需要展开 + children.forEach((n: any) => { + n.level = (node.level ?? 0) + 1; + size += this.setTreeNode(n, size, node); + }); + } else if (node.hierarchyState === HierarchyState.collapse && children?.length >= 1) { + //树形展示 有子节点 且下一层不需要展开 + children.forEach((n: any) => { + n.level = (node.level ?? 0) + 1; + this.setTreeNode(n, size, node); + }); + } else if (!node.hierarchyState && node.level + 1 < this.rowExpandLevel && children?.length >= 1) { + //树形展示 有子节点 且下一层需要展开 + node.hierarchyState = HierarchyState.expand; + children.forEach((n: any) => { + n.level = (node.level ?? 0) + 1; + size += this.setTreeNode(n, size, node); + }); + } else if (children?.length >= 1) { + //树形展示 有子节点 且下一层不需要展开 + node.hierarchyState = HierarchyState.collapse; + children.forEach((n: any) => { + n.level = (node.level ?? 0) + 1; + this.setTreeNode(n, size, node); + }); + } else { + //树形展示 无children子节点。但不能确定是最后一层的叶子节点 totalLevel还不能确定是计算完整棵树的整体深度 + node.hierarchyState = HierarchyState.none; + size = 1; + // re.totalLevel = Math.max(re.totalLevel, (node.level ?? -1) + 1); + } + + node.size = size; + // startInTotal = parent.startIndex + prevStartIndex + return size; + } + getTreePath(index: number, maxDeep = 30): Array { + const path: any[] = []; + this.searchPath(index, this.tree, path, maxDeep); + path.shift(); + return path; + } + + getTreePathByCellIds(ids: LayoutObjectId[]): Array { + const path: any[] = []; + let nodes = this.tree.children; + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + const pathNode = this.findNodeById(nodes, id); + if (pathNode) { + path.push(pathNode); + nodes = pathNode.children; + } else { + break; + } + } + // path.shift(); + return path; + } + findNodeById(nodes: ITreeLayoutHeadNode[], id: LayoutObjectId) { + return nodes.find(node => { + return node.id === id; + }); + } + searchPath(index: number, node: ITreeLayoutHeadNode, path: Array, maxDeep: number) { + if (!node) { + return; + } + if (index < node.startIndex || index >= node.startIndex + node.size) { + return; + } + path.push(node); + if (!node.children || node.children.length === 0 || node.level >= maxDeep) { + return; + } + + // const cIndex = index - node.startIndex; + // for (let i = 0; i < node.children.length; i++) { + // const element = node.children[i]; + // if (cIndex >= element.startIndex && cIndex < element.startIndex + element.size) { + // this.searchPath(cIndex, element, path, maxDeep); + // break; + // } + // } + + // use dichotomy to optimize search performance + const cIndex = index - node.startIndex; + + if (this.cache.has(node.level + 1)) { + const cacheNode = this.cache.get(node.level + 1); + if (cIndex >= cacheNode.startIndex && cIndex < cacheNode.startIndex + cacheNode.size) { + this.searchPath(cIndex, cacheNode, path, maxDeep); + return; + } + } + + let left = 0; + let right = node.children.length - 1; + + while (left <= right) { + const middle = Math.floor((left + right) / 2); + const element = node.children[middle]; + + if (cIndex >= element.startIndex && cIndex < element.startIndex + element.size) { + this.cache.set(element.level, element); + const deleteLevels: number[] = []; + this.cache.forEach((node, key) => { + if (key > element.level) { + deleteLevels.push(key); + } + }); + deleteLevels.forEach(key => { + this.cache.delete(key); + }); + this.searchPath(cIndex, element, path, maxDeep); + break; + } else if (cIndex < element.startIndex) { + right = middle - 1; + } else { + left = middle + 1; + } + } + return; + } + /** + * 将该树中 层级为level 的sourceIndex处的节点移动到targetIndex位置 + * @param level + * @param sourceIndex + * @param targetIndex + */ + movePosition(level: number, sourceIndex: number, targetIndex: number) { + // let sourceNode: IPivotLayoutHeadNode; + let parNode: ITreeLayoutHeadNode; + let sourceSubIndex: number; + let targetSubIndex: number; + /** + * 对parNode的子节点第subIndex处的node节点 进行判断是否为sourceIndex或者targetIndex + * 如果是 则记录下subIndex 以对parNode中个节点位置进行移位 + * @param node + * @param subIndex + * @returns + */ + const findTargetNode = (node: ITreeLayoutHeadNode, subIndex: number) => { + if (sourceSubIndex !== undefined && targetSubIndex !== undefined) { + return; + } + if (node.level === level) { + if (node.startInTotal === sourceIndex) { + // sourceNode = node; + sourceSubIndex = subIndex; + } + // if (node.startInTotal === targetIndex) targetSubIndex = subIndex; + // 判断targetIndex是否在node的范围内 将当前node的subIndex记为targetSubIndex + if (node.startInTotal <= targetIndex && targetIndex <= node.startInTotal + node.size - 1) { + targetSubIndex = subIndex; + } + } + const children = node.children || node.columns; + if (children && node.level < level) { + parNode = node; + for (let i = 0; i < children.length; i++) { + if ( + (sourceIndex >= children[i].startInTotal && sourceIndex <= children[i].startInTotal + children[i].size) || + (targetIndex >= children[i].startInTotal && targetIndex <= children[i].startInTotal + children[i].size) + ) { + findTargetNode(children[i], i); + } + } + } + }; + findTargetNode(this.tree, 0); + + //对parNode子节点位置进行移位【根据sourceSubIndex和targetSubIndex】 + const children = parNode.children || parNode.columns; + const sourceColumns = children.splice(sourceSubIndex, 1); + sourceColumns.unshift(targetSubIndex as any, 0 as any); + Array.prototype.splice.apply(children, sourceColumns); + } + /** 获取纯净树结构 没有level size index这些属性 */ + getCopiedTree() { + const children = cloneDeep(this.tree.children); + clearNode(children); + return children; + } +} + +//#region 为方法getLayoutRowTree提供的类型和工具方法 +export type LayouTreeNode = { + dimensionKey?: string; + indicatorKey?: string; + value: string; + hierarchyState: HierarchyState; + children?: LayouTreeNode[]; +}; + +export function generateLayoutTree(tree: LayouTreeNode[], children: ITreeLayoutHeadNode[]) { + children?.forEach((node: ITreeLayoutHeadNode) => { + const diemnsonNode: { + dimensionKey?: string; + indicatorKey?: string; + value: string; + hierarchyState: HierarchyState; + children: any; + } = { + dimensionKey: node.dimensionKey, + indicatorKey: node.indicatorKey, + value: node.value, + hierarchyState: node.hierarchyState, + children: undefined + }; + tree.push(diemnsonNode); + if (node.children) { + diemnsonNode.children = []; + generateLayoutTree(diemnsonNode.children, node.children); + } + }); +} +//#endregion + +//#region 为方法getLayoutRowTreeCount提的工具方法 +export function countLayoutTree(children: { children?: any }[], countParentNode: boolean) { + let count = 0; + children?.forEach((node: ITreeLayoutHeadNode) => { + if (countParentNode) { + count++; + } else { + if (!node.children || node.children.length === 0) { + count++; + } + } + if (node.children) { + count += countLayoutTree(node.children, countParentNode); + } + }); + return count; +} +//#endregion + +export function dealHeader( + hd: ITreeLayoutHeadNode, + _headerCellIds: number[][], + results: HeaderData[], + roots: number[], + row: number, + layoutMap: PivotHeaderLayoutMap +) { + // const col = this._columns.length; + const id = hd.id; + const dimensionInfo: IDimension = + (layoutMap.rowsDefine?.find(dimension => + typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey + ) as IDimension) ?? + (layoutMap.columnsDefine?.find(dimension => + typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey + ) as IDimension); + const indicatorInfo = layoutMap.indicatorsDefine?.find(indicator => { + if (typeof indicator === 'string') { + return false; + } + if (hd.indicatorKey) { + return indicator.indicatorKey === hd.indicatorKey; + } + return indicator.title === hd.value; + }) as IIndicator; + const cell: HeaderData = { + id, + title: hd.value ?? indicatorInfo?.title, + field: hd.dimensionKey, + style: + typeof (indicatorInfo ?? dimensionInfo)?.headerStyle === 'function' + ? (indicatorInfo ?? dimensionInfo)?.headerStyle + : Object.assign({}, (indicatorInfo ?? dimensionInfo)?.headerStyle), + headerType: indicatorInfo?.headerType ?? dimensionInfo?.headerType ?? 'text', + headerIcon: indicatorInfo?.headerIcon ?? dimensionInfo?.headerIcon, + // define: hd, + define: Object.assign({}, hd, indicatorInfo ?? dimensionInfo), + fieldFormat: indicatorInfo?.headerFormat ?? dimensionInfo?.headerFormat, + // iconPositionList:[] + dropDownMenu: indicatorInfo?.dropDownMenu ?? dimensionInfo?.dropDownMenu, + pivotInfo: { + value: hd.value, + dimensionKey: hd.dimensionKey, + isPivotCorner: false + // customInfo: dimensionInfo?.customInfo + }, + width: (dimensionInfo as IRowDimension)?.width, + minWidth: (dimensionInfo as IRowDimension)?.minWidth, + maxWidth: (dimensionInfo as IRowDimension)?.maxWidth, + showSort: indicatorInfo?.showSort ?? dimensionInfo?.showSort, + description: dimensionInfo?.description + }; + + if (indicatorInfo) { + //收集indicatorDimensionKey 提到了构造函数中 + // this.indicatorDimensionKey = dimensionInfo.dimensionKey; + if (indicatorInfo.customRender) { + hd.customRender = indicatorInfo.customRender; + } + if (!isValid(layoutMap._indicators?.find(indicator => indicator.indicatorKey === indicatorInfo.indicatorKey))) { + layoutMap._indicators?.push({ + id: ++layoutMap.sharedVar.seqId, + indicatorKey: indicatorInfo.indicatorKey, + field: indicatorInfo.indicatorKey, + fieldFormat: indicatorInfo?.format, + cellType: indicatorInfo?.cellType ?? (indicatorInfo as any)?.columnType ?? 'text', + chartModule: 'chartModule' in indicatorInfo ? indicatorInfo.chartModule : null, + chartSpec: 'chartSpec' in indicatorInfo ? indicatorInfo.chartSpec : null, + sparklineSpec: 'sparklineSpec' in indicatorInfo ? indicatorInfo.sparklineSpec : null, + style: indicatorInfo?.style, + icon: indicatorInfo?.icon, + define: Object.assign({}, hd, indicatorInfo, { + dragHeader: dimensionInfo?.dragHeader + }), + width: indicatorInfo?.width, + minWidth: indicatorInfo?.minWidth, + maxWidth: indicatorInfo?.maxWidth, + disableColumnResize: indicatorInfo?.disableColumnResize + }); + } + } else if (hd.indicatorKey) { + //兼容当某个指标没有设置在dimension.indicators中 + if (!isValid(layoutMap._indicators?.find(indicator => indicator.indicatorKey === hd.indicatorKey))) { + layoutMap._indicators?.push({ + id: ++layoutMap.sharedVar.seqId, + indicatorKey: hd.indicatorKey, + field: hd.indicatorKey, + cellType: 'text', + define: Object.assign({}, hd) + }); + } + } + // if (dimensionInfo.indicators) { + // layoutMap.hideIndicatorName = dimensionInfo.hideIndicatorName ?? false; + // layoutMap.indicatorsAsCol = dimensionInfo.indicatorsAsCol ?? true; + // } + results[id] = cell; + layoutMap._headerObjects[id] = cell; + _headerCellIds[row][layoutMap.colIndex] = id; + for (let r = row - 1; r >= 0; r--) { + _headerCellIds[r][layoutMap.colIndex] = roots[r]; + } + + // 处理汇总小计跨维度层级的情况 + if ((hd as any).levelSpan > 1) { + for (let i = 1; i < (hd as any).levelSpan; i++) { + if (!_headerCellIds[row + i]) { + _headerCellIds[row + i] = []; + } + _headerCellIds[row + i][layoutMap.colIndex] = id; + } + } + + if ((hd as ITreeLayoutHeadNode).children?.length >= 1) { + layoutMap + ._addHeaders(_headerCellIds, row + ((hd as any).levelSpan ?? 1), (hd as ITreeLayoutHeadNode).children ?? [], [ + ...roots, + ...Array((hd as any).levelSpan ?? 1).fill(id) + ]) + .forEach(c => results.push(c)); + } else { + // columns.push([""])//代码一个路径 + for (let r = row + 1; r < _headerCellIds.length; r++) { + _headerCellIds[r][layoutMap.colIndex] = id; + + // if ((hd as any).levelSpan > 1) { + // for (let i = 1; i < (hd as any).levelSpan; i++) { + // _headerCellIds[r + i][layoutMap.colIndex] = id; + // } + // } + } + layoutMap.colIndex++; + } +} + +export function dealHeaderForTreeMode( + hd: ITreeLayoutHeadNode, + _headerCellIds: number[][], + results: HeaderData[], + roots: number[], + row: number, + totalLevel: number, + show: boolean, + dimensions: (IDimension | string)[], + layoutMap: PivotHeaderLayoutMap +) { + const id = hd.id; + // const dimensionInfo: IDimension = + // (this.rowsDefine?.find(dimension => + // typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey + // ) as IDimension) ?? + // (this.columnsDefine?.find(dimension => + // typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey + // ) as IDimension); + const dimensionInfo: IDimension = dimensions.find(dimension => + typeof dimension === 'string' ? false : dimension.dimensionKey === hd.dimensionKey + ) as IDimension; + + const cell: HeaderData = { + id, + title: hd.value, + field: hd.dimensionKey as FieldData, + //如果不是整棵树的叶子节点,都靠左显示 + style: + hd.level + 1 === totalLevel || typeof dimensionInfo?.headerStyle === 'function' + ? dimensionInfo?.headerStyle + : Object.assign({}, dimensionInfo?.headerStyle, { textAlign: 'left' }), + headerType: dimensionInfo?.headerType ?? 'text', + headerIcon: dimensionInfo?.headerIcon, + define: Object.assign(hd, { + linkJump: (dimensionInfo as ILinkDimension)?.linkJump, + linkDetect: (dimensionInfo as ILinkDimension)?.linkDetect, + templateLink: (dimensionInfo as ILinkDimension)?.templateLink, + + // image相关 to be fixed + keepAspectRatio: (dimensionInfo as IImageDimension)?.keepAspectRatio ?? false, + imageAutoSizing: (dimensionInfo as IImageDimension)?.imageAutoSizing, + + headerCustomRender: dimensionInfo?.headerCustomRender, + headerCustomLayout: dimensionInfo?.headerCustomLayout, + dragHeader: dimensionInfo?.dragHeader, + disableHeaderHover: !!dimensionInfo?.disableHeaderHover, + disableHeaderSelect: !!dimensionInfo?.disableHeaderSelect + }), //这里不能新建对象,要用hd保持引用关系 + fieldFormat: dimensionInfo?.headerFormat, + // iconPositionList:[] + dropDownMenu: dimensionInfo?.dropDownMenu, + pivotInfo: { + value: hd.value, + dimensionKey: hd.dimensionKey as string, + isPivotCorner: false + // customInfo: dimensionInfo?.customInfo + }, + hierarchyLevel: hd.level, + dimensionTotalLevel: totalLevel, + hierarchyState: hd.level + 1 === totalLevel ? undefined : hd.hierarchyState, + width: (dimensionInfo as IRowDimension)?.width, + minWidth: (dimensionInfo as IRowDimension)?.minWidth, + maxWidth: (dimensionInfo as IRowDimension)?.maxWidth, + parentCellId: roots[roots.length - 1] + }; + + results[id] = cell; + // this._cellIdDiemnsionMap.set(id, { + // dimensionKey: hd.dimensionKey, + // value: hd.value + // }); + layoutMap._headerObjects[id] = cell; + _headerCellIds[row][layoutMap.colIndex] = id; + for (let r = row - 1; r >= 0; r--) { + _headerCellIds[r][layoutMap.colIndex] = roots[r]; + } + if (hd.hierarchyState === HierarchyState.expand && (hd as ITreeLayoutHeadNode).children?.length >= 1) { + //row传值 colIndex++和_addHeaders有区别 + show && layoutMap.colIndex++; + layoutMap + ._addHeadersForTreeMode( + _headerCellIds, + row, + (hd as ITreeLayoutHeadNode).children ?? [], + [...roots, id], + totalLevel, + show && hd.hierarchyState === HierarchyState.expand, //当前节点show 且当前节点状态为展开 则传给子节点为show:true + dimensions + ) + .forEach(c => results.push(c)); + } else { + // columns.push([""])//代码一个路径 + show && layoutMap.colIndex++; + for (let r = row + 1; r < _headerCellIds.length; r++) { + _headerCellIds[r][layoutMap.colIndex] = id; + } + } +} + +function clearNode(children: any) { + for (let i = 0; i < children.length; i++) { + const node = children[i]; + delete node.level; + delete node.startIndex; + delete node.id; + delete node.levelSpan; + delete node.size; + delete node.startInTotal; + const childrenNew = node.children || node.columns; + if (childrenNew) { + clearNode(childrenNew); + } + } +} diff --git a/packages/vtable/src/scenegraph/component/custom.ts b/packages/vtable/src/scenegraph/component/custom.ts index f27d3d4cf..c807fd7a5 100644 --- a/packages/vtable/src/scenegraph/component/custom.ts +++ b/packages/vtable/src/scenegraph/component/custom.ts @@ -19,11 +19,6 @@ import type { import { Icon } from '../graphic/icon'; import type { BaseTableAPI } from '../../ts-types/base-table'; import type { percentCalcObj } from '../../render/layout'; -import { ProgressBarStyle } from '../../body-helper/style/ProgressBarStyle'; -import { getQuadProps } from '../utils/padding'; -import { getProp } from '../utils/get-prop'; -import type { Group } from '../graphic/group'; -import { resizeCellGroup } from '../group-creater/column-helper'; export function dealWithCustom( customLayout: ICustomLayout, @@ -467,55 +462,3 @@ function parseToGraphic(g: any, props: any) { // } } } - -export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Group, table: BaseTableAPI) { - if (table.internalProps.customMergeCell) { - const customMerge = table.getCustomMerge(col, row); - if (customMerge) { - const { - range: customMergeRange, - text: customMergeText, - style: customMergeStyle, - customLayout: customMergeLayout, - customRender: customMergeRender - } = customMerge; - - if (customMergeLayout || customMergeRender) { - const customResult = dealWithCustom( - customMergeLayout, - customMergeRender, - customMergeRange.start.col, - customMergeRange.start.row, - table.getColsWidth(customMergeRange.start.col, customMergeRange.end.col), - table.getRowsHeight(customMergeRange.start.row, customMergeRange.end.row), - false, - table.heightMode === 'autoHeight', - [0, 0, 0, 0], - table - ); - - const customElementsGroup = customResult.elementsGroup; - - if (cellGroup.childrenCount > 0 && customElementsGroup) { - cellGroup.insertBefore(customElementsGroup, cellGroup.firstChild); - } else if (customElementsGroup) { - cellGroup.appendChild(customElementsGroup); - } - - const rangeHeight = table.getRowHeight(row); - const rangeWidth = table.getColWidth(col); - - const { width: contentWidth } = cellGroup.attribute; - const { height: contentHeight } = cellGroup.attribute; - cellGroup.contentWidth = contentWidth; - cellGroup.contentHeight = contentHeight; - - resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); - - return customResult; - } - } - } - - return undefined; -} diff --git a/packages/vtable/src/scenegraph/component/menu.ts b/packages/vtable/src/scenegraph/component/menu.ts index 9126f93e2..5b6526332 100644 --- a/packages/vtable/src/scenegraph/component/menu.ts +++ b/packages/vtable/src/scenegraph/component/menu.ts @@ -298,7 +298,7 @@ export class MenuHandler { const { field } = this._table.isHeader(col, row) ? this._table.getHeaderDefine(col, row) : this._table.getBodyColumnDefine(col, row); - menuInfo = contextmenu(field, row); + menuInfo = contextmenu(field, row, col); } return { menuInfo, @@ -317,6 +317,7 @@ export class MenuHandler { const resultMenuInfo = this.getEventInfo(target as unknown as Group); const resultTableInfo = this._table.getMenuInfo(this._menuInfo.col, this._menuInfo.row, this._menuInfo.type); const result = Object.assign(resultMenuInfo, resultTableInfo); + result.event = e.nativeEvent; this._table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLICK, result); // 由DROPDOWNMENU_CLICK事件清空菜单 diff --git a/packages/vtable/src/scenegraph/graphic/chart.ts b/packages/vtable/src/scenegraph/graphic/chart.ts index e10052cf7..f13d987f2 100644 --- a/packages/vtable/src/scenegraph/graphic/chart.ts +++ b/packages/vtable/src/scenegraph/graphic/chart.ts @@ -33,14 +33,14 @@ export class Chart extends Group { activeChartInstance: any; active: boolean; cacheCanvas: HTMLCanvasElement | { x: number; y: number; width: number; height: number; canvas: HTMLCanvasElement }[]; // HTMLCanvasElement - - constructor(params: IChartGraphicAttribute) { + isShareChartSpec: boolean; //针对chartSpec用户配置成函数形式的话 就不需要存储chartInstance了 会太占内存,使用这个变量 当渲染出缓存图表会就删除chartInstance实例 + constructor(isShareChartSpec: boolean, params: IChartGraphicAttribute) { super(params); this.numberType = CHART_NUMBER_TYPE; - + this.isShareChartSpec = isShareChartSpec; // 创建chart if (!params.chartInstance) { - params.chartInstance = this.chartInstance = new params.ClassType(params.spec, { + const chartInstance = new params.ClassType(params.spec, { renderCanvas: params.canvas, mode: this.attribute.mode === 'node' ? 'node' : 'desktop-browser', modeParams: this.attribute.modeParams, @@ -59,7 +59,8 @@ export class Chart extends Group { animation: false, autoFit: false }); - this.chartInstance.renderSync(); + chartInstance.renderSync(); + params.chartInstance = this.chartInstance = chartInstance; } else { this.chartInstance = params.chartInstance; } diff --git a/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts b/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts index 90191830d..93fbf2dd4 100644 --- a/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts +++ b/packages/vtable/src/scenegraph/graphic/contributions/chart-render-helper.ts @@ -148,6 +148,12 @@ function cacheStageCanvas(stage: IStage, chart: Chart) { const { viewWidth, viewHeight } = stage; if (viewWidth < cacheCanvasSizeLimit && viewHeight < cacheCanvasSizeLimit) { chart.cacheCanvas = stage.toCanvas(); + if (!chart.isShareChartSpec) { + // 不能整列共享chart的情况 生成完图片后即将chartInstance清除 + chart.chartInstance?.release(); + chart.chartInstance = null; + chart.setAttribute('chartInstance', null); + } return; } diff --git a/packages/vtable/src/scenegraph/graphic/contributions/chart-render.ts b/packages/vtable/src/scenegraph/graphic/contributions/chart-render.ts index 2b2a54688..fabfb1487 100644 --- a/packages/vtable/src/scenegraph/graphic/contributions/chart-render.ts +++ b/packages/vtable/src/scenegraph/graphic/contributions/chart-render.ts @@ -60,7 +60,7 @@ export class DefaultCanvasChartRender implements IGraphicRender { const viewBox = chart.getViewBox(); const { width = groupAttribute.width, height = groupAttribute.height } = chart.attribute; - const { chartInstance, active, cacheCanvas, activeChartInstance } = chart; + const { active, cacheCanvas, activeChartInstance } = chart; // console.log('render chart', chart.parent.col, chart.parent.row, viewBox, cacheCanvas); if (!active && cacheCanvas) { if (isArray(cacheCanvas)) { @@ -89,7 +89,7 @@ export class DefaultCanvasChartRender implements IGraphicRender { : data ?? [], fields: series?.data?.fields }); - if (!chartInstance.updateFullDataSync) { + if (!activeChartInstance.updateFullDataSync) { activeChartInstance.updateDataSync( dataIdStr, dataIdAndField diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 2ff3d5208..81682890f 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -7,7 +7,6 @@ import type { CheckboxColumnDefine, ColumnDefine, ColumnTypeOption, - ICustomRender, ImageColumnDefine, MappingRule, ProgressbarColumnDefine, @@ -22,7 +21,6 @@ import { createProgressBarCell } from './cell-type/progress-bar-cell'; import { createSparkLineCellGroup } from './cell-type/spark-line-cell'; import { createCellGroup } from './cell-type/text-cell'; import { createVideoCellGroup } from './cell-type/video-cell'; -import type { ICustomLayoutFuc } from '../../ts-types/customLayout'; import type { BaseTableAPI, PivotTableProtected } from '../../ts-types/base-table'; import { getCellCornerRadius, getStyleTheme } from '../../core/tableHelper'; import { isPromise } from '../../tools/helper'; @@ -31,10 +29,11 @@ import { CartesianAxis } from '../../components/axis/axis'; import { createCheckboxCellGroup } from './cell-type/checkbox-cell'; // import type { PivotLayoutMap } from '../../layout/pivot-layout'; import type { PivotHeaderLayoutMap } from '../../layout/pivot-header-layout'; -import { resizeCellGroup } from './column-helper'; import { getHierarchyOffset } from '../utils/get-hierarchy-offset'; import { getQuadProps } from '../utils/padding'; import { convertInternal } from '../../tools/util'; +import { updateCellContentHeight, updateCellContentWidth } from '../utils/text-icon-layout'; +import { isArray } from '@visactor/vutils'; export function createCell( type: ColumnTypeOption, @@ -243,13 +242,12 @@ export function createCell( padding, value, (define as ChartColumnDefine).chartModule, - table.isPivotChart() - ? (table.internalProps.layoutMap as PivotHeaderLayoutMap).getChartSpec(col, row) - : (define as ChartColumnDefine).chartSpec, + table.internalProps.layoutMap.getChartSpec(col, row), chartInstance, - (table.internalProps.layoutMap as PivotHeaderLayoutMap)?.getChartDataId(col, row) ?? 'data', + table.internalProps.layoutMap.getChartDataId(col, row) ?? 'data', table, - cellTheme + cellTheme, + table.internalProps.layoutMap.isShareChartSpec(col, row) ); } else if (type === 'progressbar') { const style = table._getCellStyle(col, row) as ProgressBarStyle; @@ -470,9 +468,9 @@ export function updateCell(col: number, row: number, table: BaseTableAPI, addNew dx: hierarchyOffset, x }; - const oldText = textMark.attribute.text; + // const oldText = textMark.attribute.text; textMark.setAttributes(cellTheme.text ? (Object.assign({}, cellTheme.text, attribute) as any) : attribute); - if (!oldText && textMark.attribute.text) { + if (textMark.attribute.text) { const textBaseline = cellTheme.text.textBaseline; const height = cellHeight - (padding[0] + padding[2]); let y = 0; @@ -501,8 +499,8 @@ export function updateCell(col: number, row: number, table: BaseTableAPI, addNew const mayHaveIcon = cellLocation !== 'body' ? true : !!define?.icon || !!define?.tree; const padding = cellTheme._vtable.padding; - const textAlign = cellTheme._vtable.textAlign; - const textBaseline = cellTheme._vtable.textBaseline; + const textAlign = cellTheme.text.textAlign; + const textBaseline = cellTheme.text.textBaseline; let newCellGroup; let bgColorFunc: Function; @@ -586,15 +584,15 @@ export function updateCell(col: number, row: number, table: BaseTableAPI, addNew } if (isMerge) { - const rangeHeight = table.getRowHeight(row); - const rangeWidth = table.getColWidth(col); + // const rangeHeight = table.getRowHeight(row); + // const rangeWidth = table.getColWidth(col); const { width: contentWidth } = newCellGroup.attribute; const { height: contentHeight } = newCellGroup.attribute; newCellGroup.contentWidth = contentWidth; newCellGroup.contentHeight = contentHeight; - resizeCellGroup(newCellGroup, rangeWidth, rangeHeight, range, table); + dealWithMergeCellSize(range, cellWidth, cellHeight, padding, textAlign, textBaseline, table); } return newCellGroup; @@ -665,6 +663,7 @@ function updateCellContent( } function canUseFastUpdate(col: number, row: number, oldCellGroup: Group, autoWrapText: boolean, table: BaseTableAPI) { + // return false; const define = table.getBodyColumnDefine(col, row); const mayHaveIcon = !!define?.icon || !!define?.tree; const cellType = table.getBodyColumnType(col, row); @@ -685,3 +684,171 @@ function canUseFastUpdate(col: number, row: number, oldCellGroup: Group, autoWra } return false; } + +function dealWithMergeCellSize( + range: CellRange, + cellWidth: number, + cellHeight: number, + padding: [number, number, number, number], + textAlign: CanvasTextAlign, + textBaseline: CanvasTextBaseline, + table: BaseTableAPI +) { + // const rangeHeight = table.getRowHeight(row); + // const rangeWidth = table.getColWidth(col); + + // const { width: contentWidth } = newCellGroup.attribute; + // const { height: contentHeight } = newCellGroup.attribute; + // newCellGroup.contentWidth = contentWidth; + // newCellGroup.contentHeight = contentHeight; + + // resizeCellGroup(newCellGroup, rangeWidth, rangeHeight, range, table); + for (let col = range.start.col; col <= range.end.col; col++) { + for (let row = range.start.row; row <= range.end.row; row++) { + const cellGroup = table.scenegraph.getCell(col, row, true); + + if (range.start.row !== range.end.row) { + // const cellGroup = table.scenegraph.getCell(col, row, true); + updateCellContentHeight( + cellGroup, + cellHeight, + cellHeight, + table.heightMode === 'autoHeight', + padding, + textAlign, + textBaseline + // 'middle' + ); + } + if (range.start.col !== range.end.col) { + // const cellGroup = table.scenegraph.getCell(col, row, true); + updateCellContentWidth( + cellGroup, + cellWidth, + cellHeight, + 0, + table.heightMode === 'autoHeight', + padding, + textAlign, + textBaseline, + table.scenegraph + ); + } + + cellGroup.contentWidth = cellWidth; + cellGroup.contentHeight = cellHeight; + + const rangeHeight = table.getRowHeight(row); + const rangeWidth = table.getColWidth(col); + + resizeCellGroup(cellGroup, rangeWidth, rangeHeight, range, table); + } + } +} + +export function resizeCellGroup( + cellGroup: Group, + rangeWidth: number, + rangeHeight: number, + range: CellRange, + table: BaseTableAPI +) { + const { col, row } = cellGroup; + const dx = -table.getColsWidth(range.start.col, col - 1); + const dy = -table.getRowsHeight(range.start.row, row - 1); + + cellGroup.forEachChildren((child: IGraphic) => { + child.setAttributes({ + dx: (child.attribute.dx ?? 0) + dx, + dy: (child.attribute.dy ?? 0) + dy + }); + }); + + const lineWidth = cellGroup.attribute.lineWidth; + const isLineWidthArray = isArray(lineWidth); + const newLineWidth = [0, 0, 0, 0]; + + if (col === range.start.col) { + newLineWidth[3] = isLineWidthArray ? lineWidth[3] : lineWidth; + } + if (row === range.start.row) { + newLineWidth[0] = isLineWidthArray ? lineWidth[0] : lineWidth; + } + if (col === range.end.col) { + newLineWidth[1] = isLineWidthArray ? lineWidth[1] : lineWidth; + } + if (row === range.end.row) { + newLineWidth[2] = isLineWidthArray ? lineWidth[2] : lineWidth; + } + + const widthChange = rangeWidth !== cellGroup.attribute.width; + const heightChange = rangeHeight !== cellGroup.attribute.height; + + cellGroup.setAttributes({ + width: rangeWidth, + height: rangeHeight, + strokeArrayWidth: newLineWidth + } as any); + + cellGroup.mergeStartCol = range.start.col; + cellGroup.mergeStartRow = range.start.row; + cellGroup.mergeEndCol = range.end.col; + cellGroup.mergeEndRow = range.end.row; + + return { + widthChange, + heightChange + }; +} + +export function getCustomCellMergeCustom(col: number, row: number, cellGroup: Group, table: BaseTableAPI) { + if (table.internalProps.customMergeCell) { + const customMerge = table.getCustomMerge(col, row); + if (customMerge) { + const { + range: customMergeRange, + text: customMergeText, + style: customMergeStyle, + customLayout: customMergeLayout, + customRender: customMergeRender + } = customMerge; + + if (customMergeLayout || customMergeRender) { + const customResult = dealWithCustom( + customMergeLayout, + customMergeRender, + customMergeRange.start.col, + customMergeRange.start.row, + table.getColsWidth(customMergeRange.start.col, customMergeRange.end.col), + table.getRowsHeight(customMergeRange.start.row, customMergeRange.end.row), + false, + table.heightMode === 'autoHeight', + [0, 0, 0, 0], + table + ); + + const customElementsGroup = customResult.elementsGroup; + + if (cellGroup.childrenCount > 0 && customElementsGroup) { + cellGroup.insertBefore(customElementsGroup, cellGroup.firstChild); + } else if (customElementsGroup) { + cellGroup.appendChild(customElementsGroup); + } + + const rangeHeight = table.getRowHeight(row); + const rangeWidth = table.getColWidth(col); + + const { width: contentWidth } = cellGroup.attribute; + const { height: contentHeight } = cellGroup.attribute; + cellGroup.contentWidth = contentWidth; + cellGroup.contentHeight = contentHeight; + + resizeCellGroup(cellGroup, rangeWidth, rangeHeight, customMergeRange, table); + + return customResult; + } + } + } + + return undefined; +} diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts index cffc4d998..cb27cc06a 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/chart-cell.ts @@ -22,7 +22,8 @@ export function createChartCellGroup( chartInstance: any, dataId: string | Record, table: BaseTableAPI, - cellTheme: IThemeSpec + cellTheme: IThemeSpec, + isShareChartSpec: true ) { // 获取注册的chart图表类型 const registerCharts = registerChartTypes.get(); @@ -64,7 +65,7 @@ export function createChartCellGroup( } cellGroup.AABBBounds.width(); // TODO 需要底层VRender修改 // chart - const chartGroup = new Chart({ + const chartGroup = new Chart(isShareChartSpec, { stroke: false, x: padding[3], y: padding[0], diff --git a/packages/vtable/src/scenegraph/group-creater/column-helper.ts b/packages/vtable/src/scenegraph/group-creater/column-helper.ts index 5ca12604d..3621f1621 100644 --- a/packages/vtable/src/scenegraph/group-creater/column-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/column-helper.ts @@ -1,15 +1,14 @@ /* eslint-disable no-undef */ -import type { IGraphic, IThemeSpec } from '@src/vrender'; +import type { IThemeSpec } from '@src/vrender'; import type { CellLocation, CellRange, TextColumnDefine } from '../../ts-types'; import type { Group } from '../graphic/group'; import { getProp, getRawProp } from '../utils/get-prop'; import type { MergeMap } from '../scenegraph'; -import { createCell } from './cell-helper'; +import { createCell, resizeCellGroup } from './cell-helper'; import type { BaseTableAPI } from '../../ts-types/base-table'; import { getCellCornerRadius, getStyleTheme } from '../../core/tableHelper'; import { isPromise } from '../../tools/helper'; import { dealPromiseData } from '../utils/deal-promise-data'; -import { isArray } from '@visactor/vutils'; import { dealWithCustom } from '../component/custom'; /** * 创建复合列 同一列支持创建不同类型单元格 @@ -80,7 +79,8 @@ export function createComplexColumn( range = customMergeRange; isMerge = range.start.col !== range.end.col || range.start.row !== range.end.row; if (isMerge) { - const mergeSize = dealMerge(range, mergeMap, table); + const needUpdateRange = rowStart > range.start.row; + const mergeSize = dealMerge(range, mergeMap, table, needUpdateRange); cellWidth = mergeSize.cellWidth; cellHeight = mergeSize.cellHeight; } @@ -122,7 +122,8 @@ export function createComplexColumn( isMerge = range.start.col !== range.end.col || range.start.row !== range.end.row; // 所有Merge单元格,只保留左上角一个真实的单元格,其他使用空Group占位 if (isMerge) { - const mergeSize = dealMerge(range, mergeMap, table); + const needUpdateRange = rowStart > range.start.row; + const mergeSize = dealMerge(range, mergeMap, table, needUpdateRange); cellWidth = mergeSize.cellWidth; cellHeight = mergeSize.cellHeight; } @@ -268,66 +269,11 @@ export function getColumnGroupTheme( return { theme: columnTheme, hasFunctionPros }; } -export function resizeCellGroup( - cellGroup: Group, - rangeWidth: number, - rangeHeight: number, - range: CellRange, - table: BaseTableAPI -) { - const { col, row } = cellGroup; - const dx = -table.getColsWidth(range.start.col, col - 1); - const dy = -table.getRowsHeight(range.start.row, row - 1); - - cellGroup.forEachChildren((child: IGraphic) => { - child.setAttributes({ - dx: (child.attribute.dx ?? 0) + dx, - dy: (child.attribute.dy ?? 0) + dy - }); - }); - - const lineWidth = cellGroup.attribute.lineWidth; - const isLineWidthArray = isArray(lineWidth); - const newLineWidth = [0, 0, 0, 0]; - - if (col === range.start.col) { - newLineWidth[3] = isLineWidthArray ? lineWidth[3] : lineWidth; - } - if (row === range.start.row) { - newLineWidth[0] = isLineWidthArray ? lineWidth[0] : lineWidth; - } - if (col === range.end.col) { - newLineWidth[1] = isLineWidthArray ? lineWidth[1] : lineWidth; - } - if (row === range.end.row) { - newLineWidth[2] = isLineWidthArray ? lineWidth[2] : lineWidth; - } - - const widthChange = rangeWidth !== cellGroup.attribute.width; - const heightChange = rangeHeight !== cellGroup.attribute.height; - - cellGroup.setAttributes({ - width: rangeWidth, - height: rangeHeight, - strokeArrayWidth: newLineWidth - } as any); - - cellGroup.mergeStartCol = range.start.col; - cellGroup.mergeStartRow = range.start.row; - cellGroup.mergeEndCol = range.end.col; - cellGroup.mergeEndRow = range.end.row; - - return { - widthChange, - heightChange - }; -} - -function dealMerge(range: CellRange, mergeMap: MergeMap, table: BaseTableAPI) { +function dealMerge(range: CellRange, mergeMap: MergeMap, table: BaseTableAPI, forceUpdate: boolean) { let cellWidth = 0; let cellHeight = 0; const mergeResult = mergeMap.get(`${range.start.col},${range.start.row};${range.end.col},${range.end.row}`); - if (!mergeResult) { + if (!mergeResult || forceUpdate) { for (let col = range.start.col; col <= range.end.col; col++) { cellWidth += table.getColWidth(col); } diff --git a/packages/vtable/src/scenegraph/layout/compute-col-width.ts b/packages/vtable/src/scenegraph/layout/compute-col-width.ts index ef06f55e8..7117c9821 100644 --- a/packages/vtable/src/scenegraph/layout/compute-col-width.ts +++ b/packages/vtable/src/scenegraph/layout/compute-col-width.ts @@ -624,14 +624,14 @@ function computeTextWidth(col: number, row: number, cellType: ColumnTypeOption, return maxWidth; } -function getCellRect(col: number, row: number, table: BaseTableAPI) { +function getCellRect(col: number, row: number, table: BaseTableAPI): any { return { left: 0, top: 0, right: table.getColWidth(col), bottom: table.getRowHeight(row), - width: 0, - height: 0 + width: null, // vrender 逻辑中通过判断null对flex的元素来自动计算宽高 + height: null }; } diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 5326ed59a..598c7366a 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -8,12 +8,12 @@ import { getProp } from '../utils/get-prop'; import { getQuadProps } from '../utils/padding'; import { updateCellContentHeight } from '../utils/text-icon-layout'; import type { IProgressbarColumnBodyDefine } from '../../ts-types/list-table/define/progressbar-define'; -import { dealWithCustom, getCustomCellMergeCustom } from '../component/custom'; +import { dealWithCustom } from '../component/custom'; import { updateImageCellContentWhileResize } from '../group-creater/cell-type/image-cell'; import { getStyleTheme } from '../../core/tableHelper'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import type { BaseTableAPI } from '../../ts-types/base-table'; -import { resizeCellGroup } from '../group-creater/column-helper'; +import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; import type { IGraphic } from '@src/vrender'; import { getCellMergeRange } from '../../tools/merge-range'; diff --git a/packages/vtable/src/scenegraph/layout/update-width.ts b/packages/vtable/src/scenegraph/layout/update-width.ts index c0c34d06b..6b9807d33 100644 --- a/packages/vtable/src/scenegraph/layout/update-width.ts +++ b/packages/vtable/src/scenegraph/layout/update-width.ts @@ -4,20 +4,19 @@ import { CartesianAxis } from '../../components/axis/axis'; import { getStyleTheme } from '../../core/tableHelper'; import type { BaseTableAPI } from '../../ts-types/base-table'; import type { IProgressbarColumnBodyDefine } from '../../ts-types/list-table/define/progressbar-define'; -import { dealWithCustom, getCustomCellMergeCustom } from '../component/custom'; +import { dealWithCustom } from '../component/custom'; import type { Group } from '../graphic/group'; -import type { Icon } from '../graphic/icon'; import { updateImageCellContentWhileResize } from '../group-creater/cell-type/image-cell'; import { createProgressBarCell } from '../group-creater/cell-type/progress-bar-cell'; import { createSparkLineCellGroup } from '../group-creater/cell-type/spark-line-cell'; -import { resizeCellGroup } from '../group-creater/column-helper'; +import { resizeCellGroup, getCustomCellMergeCustom } from '../group-creater/cell-helper'; import type { Scenegraph } from '../scenegraph'; import { getCellMergeInfo } from '../utils/get-cell-merge'; import { getProp } from '../utils/get-prop'; import { isMergeCellGroup } from '../utils/is-merge-cell-group'; import { getQuadProps } from '../utils/padding'; import { updateCellContentWidth } from '../utils/text-icon-layout'; -import { computeRowHeight, computeRowsHeight } from './compute-row-height'; +import { computeRowHeight } from './compute-row-height'; import { updateCellHeightForRow } from './update-height'; import { getHierarchyOffset } from '../utils/get-hierarchy-offset'; import { getCellMergeRange } from '../../tools/merge-range'; diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 21fa97923..a5dd686ef 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -217,7 +217,7 @@ export class Scenegraph { */ clearCells() { // unbind AutoPoptip - if (this.table.isPivotChart() || this.table.hasCustomRenderOrLayout()) { + if (this.table.isPivotChart() || this.table._hasCustomRenderOrLayout()) { // bind for axis label in pivotChart this.stage.pluginService.findPluginsByName('poptipForText').forEach(plugin => { plugin.deactivate(this.stage.pluginService); @@ -337,7 +337,7 @@ export class Scenegraph { */ createSceneGraph() { // bind AutoPoptip - if (this.table.isPivotChart() || this.table.hasCustomRenderOrLayout()) { + if (this.table.isPivotChart() || this.table._hasCustomRenderOrLayout()) { // bind for axis label in pivotChart (this.stage.pluginService as any).autoEnablePlugins.getContributions().forEach((p: any) => { if (p.name === 'poptipForText') { @@ -1451,12 +1451,7 @@ export class Scenegraph { this.updateTableSize(); - // 记录滚动条原位置 - const oldHorizontalBarPos = this.table.stateManager.scroll.horizontalBarPos; - const oldVerticalBarPos = this.table.stateManager.scroll.verticalBarPos; this.component.updateScrollBar(); - this.table.stateManager.setScrollLeft(oldHorizontalBarPos); - this.table.stateManager.setScrollTop(oldVerticalBarPos); this.updateNextFrame(); } @@ -1499,9 +1494,9 @@ export class Scenegraph { this.rowHeaderGroup, this.isPivot ? this.table.theme.rowHeaderStyle.frameStyle - : this.table.internalProps.transpose - ? this.table.theme.headerStyle.frameStyle - : this.table.theme.bodyStyle.frameStyle, + : // : this.table.internalProps.transpose + // ? this.table.theme.headerStyle.frameStyle + this.table.theme.bodyStyle.frameStyle, this.rowHeaderGroup.role, isListTableWithFrozen ? [true, false, true, true] : undefined ); diff --git a/packages/vtable/src/state/sort/index.ts b/packages/vtable/src/state/sort/index.ts index 08cc052db..149e1f697 100644 --- a/packages/vtable/src/state/sort/index.ts +++ b/packages/vtable/src/state/sort/index.ts @@ -10,7 +10,7 @@ import type { BaseTableAPI } from '../../ts-types/base-table'; * @param {BaseTableAPI} table * @return {*} */ -export function dealSort(col: number, row: number, table: ListTableAPI) { +export function dealSort(col: number, row: number, table: ListTableAPI, event: Event) { //是击中的sort按钮才进行排序 let range1 = null; let tableState: SortState; @@ -42,7 +42,6 @@ export function dealSort(col: number, row: number, table: ListTableAPI) { } else if (headerC?.sort) { //如果当前表头设置了sort 则 转变sort的状态 tableState = { - fieldKey: table.getHeaderFieldKey(col, row), field: table.getHeaderField(col, row), order: 'asc' }; @@ -50,13 +49,13 @@ export function dealSort(col: number, row: number, table: ListTableAPI) { //当前排序规则是该表头field 且仅为显示showSort无sort 什么也不做 } else { tableState = { - fieldKey: table.getHeaderFieldKey(col, row), field: table.getHeaderField(col, row), order: 'normal' }; } + (tableState as SortState & { event: Event }).event = event; // 如果用户监听SORT_CLICK事件的回调函数返回false 则不执行内部排序逻辑 - const sortEventReturns = table.fireListeners(TABLE_EVENT_TYPE.SORT_CLICK, tableState); + const sortEventReturns = table.fireListeners(TABLE_EVENT_TYPE.SORT_CLICK, tableState as SortState & { event: Event }); if (sortEventReturns.includes(false)) { return; } @@ -76,14 +75,8 @@ export function dealSort(col: number, row: number, table: ListTableAPI) { } function executeSort(newState: SortState, table: BaseTableAPI, headerDefine: HeaderDefine): void { - let hd; - if (newState.fieldKey) { - hd = table.internalProps.layoutMap.headerObjects.find( - (col: HeaderData) => col && col.fieldKey === newState.fieldKey - ); - } else { - hd = table.internalProps.layoutMap.headerObjects.find((col: HeaderData) => col && col.field === newState.field); - } + const hd = table.internalProps.layoutMap.headerObjects.find((col: HeaderData) => col && col.field === newState.field); + if (!hd) { return; } diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index 9b59de012..8a8526421 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -108,7 +108,7 @@ export class StateManager { col: number; row: number; field?: string; - fieldKey?: string; + // fieldKey?: string; order: SortOrder; icon?: Icon; }; @@ -406,7 +406,7 @@ export class StateManager { setSortState(sortState: SortState) { this.sort.field = sortState?.field as string; - this.sort.fieldKey = sortState?.fieldKey as string; + // this.sort.fieldKey = sortState?.fieldKey as string; this.sort.order = sortState?.order; // // 这里有一个问题,目前sortState中一般只传入了fieldKey,但是getCellRangeByField需要field // const range = this.table.getCellRangeByField(this.sort.field, 0); @@ -788,10 +788,11 @@ export class StateManager { } } - triggerDropDownMenu(col: number, row: number, x: number, y: number) { + triggerDropDownMenu(col: number, row: number, x: number, y: number, event: Event) { this.table.fireListeners(TABLE_EVENT_TYPE.DROPDOWN_ICON_CLICK, { col, - row + row, + event }); if (this.menu.isShow) { this.hideMenu(); @@ -935,7 +936,7 @@ export class StateManager { } return false; } - triggerSort(col: number, row: number, iconMark: Icon) { + triggerSort(col: number, row: number, iconMark: Icon, event: Event) { if (this.table.isPivotTable()) { // 透视表不执行sort操作 const order = (this.table as PivotTableAPI).getPivotSortState(col, row); @@ -945,7 +946,8 @@ export class StateManager { row: row, order: order || 'normal', dimensionInfo: (this.table.internalProps.layoutMap as PivotHeaderLayoutMap).getPivotDimensionInfo(col, row), - cellLocation: this.table.getCellLocation(col, row) + cellLocation: this.table.getCellLocation(col, row), + event }); return; } @@ -953,7 +955,7 @@ export class StateManager { const oldSortCol = this.sort.col; const oldSortRow = this.sort.row; // 执行sort - dealSort(col, row, this.table as ListTableAPI); + dealSort(col, row, this.table as ListTableAPI, event); this.sort.col = col; this.sort.row = row; diff --git a/packages/vtable/src/themes/theme.ts b/packages/vtable/src/themes/theme.ts index 1c31552a5..d6d5f1d6e 100644 --- a/packages/vtable/src/themes/theme.ts +++ b/packages/vtable/src/themes/theme.ts @@ -391,7 +391,7 @@ export class TableTheme implements ITableThemeDefine { {}, this.defaultStyle, superTheme.rowHeaderStyle, - obj.rowHeaderStyle // ?? obj.headerStyle + obj.rowHeaderStyle ?? obj.headerStyle ); this._rowHeader = this.getStyle(header); } diff --git a/packages/vtable/src/tools/get-data-path/create-dataset.ts b/packages/vtable/src/tools/get-data-path/create-dataset.ts index 62563ab31..86693a4fd 100644 --- a/packages/vtable/src/tools/get-data-path/create-dataset.ts +++ b/packages/vtable/src/tools/get-data-path/create-dataset.ts @@ -3,15 +3,15 @@ import type { AggregationRule, AggregationRules, CollectValueBy, - IDataConfig, IIndicator, - PivotChartConstructorOptions + PivotChartConstructorOptions, + IPivotChartDataConfig } from '../../ts-types'; import { AggregationType } from '../../ts-types'; import type { IChartColumnIndicator } from '../../ts-types/pivot-table/indicator/chart-indicator'; export function createDataset(options: PivotChartConstructorOptions) { - const dataConfig: IDataConfig = { isPivotChart: true }; + const dataConfig: IPivotChartDataConfig = { isPivotChart: true }; const rowKeys = options.rows?.reduce((keys, rowObj) => { diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 2e617a205..f0db2dadc 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -32,7 +32,7 @@ import type { HeaderValues, HeightModeDef, HierarchyState, - IDataConfig, + IPivotTableDataConfig, IPagination, ITableThemeDefine, SortState, @@ -46,7 +46,9 @@ import type { CustomMerge, IColumnDimension, IRowDimension, - TableEventOptions + TableEventOptions, + IPivotChartDataConfig, + IListTableDataConfig } from '.'; import type { TooltipOptions } from './tooltip'; import type { IWrapTextGraphicAttribute } from '../scenegraph/graphic/text'; @@ -156,7 +158,7 @@ export interface IBaseTableProtected { /** 内置下拉菜单的全局设置项 目前只针对基本表格有效 会对每个表头单元格开启默认的下拉菜单功能。代替原来的option.dropDownMenu*/ defaultHeaderMenuItems?: MenuListItem[]; /** 右键菜单。代替原来的option.contextmenu */ - contextMenuItems?: MenuListItem[] | ((field: FieldDef, row: number) => MenuListItem[]); + contextMenuItems?: MenuListItem[] | ((field: FieldDef, row: number, col: number) => MenuListItem[]); /** 设置选中状态的菜单。代替原来的option.dropDownMenuHighlight */ dropDownMenuHighlight?: DropDownMenuHighlightInfo[]; }; @@ -299,7 +301,7 @@ export interface BaseTableConstructorOptions { /** 内置下拉菜单的全局设置项 目前只针对基本表格有效 会对每个表头单元格开启默认的下拉菜单功能。代替原来的option.dropDownMenu*/ defaultHeaderMenuItems?: MenuListItem[]; /** 右键菜单。代替原来的option.contextmenu */ - contextMenuItems?: MenuListItem[] | ((field: string, row: number) => MenuListItem[]); + contextMenuItems?: MenuListItem[] | ((field: string, row: number, col: number) => MenuListItem[]); /** 设置选中状态的菜单。代替原来的option.dropDownMenuHighlight */ dropDownMenuHighlight?: DropDownMenuHighlightInfo[]; }; @@ -375,6 +377,8 @@ export interface BaseTableConstructorOptions { resizeTime?: number; } export interface BaseTableAPI { + /** 数据总条目数 */ + recordsCount: number; /** 表格的行数 */ rowCount: number; /** 表格的列数 */ @@ -505,12 +509,12 @@ export interface BaseTableAPI { setMaxColWidth: (col: number, maxwidth: string | number) => void; getMinColWidth: (col: number) => number; setMinColWidth: (col: number, minwidth: string | number) => void; - getCellRect: (col: number, row: number) => RectProps; - getCellRelativeRect: (col: number, row: number) => RectProps; - getCellsRect: (startCol: number, startRow: number, endCol: number, endRow: number) => RectProps; - getCellRangeRect: (cellRange: CellRange | CellAddress) => RectProps; - getCellRangeRelativeRect: (cellRange: CellRange | CellAddress) => RectProps; - getVisibleCellRangeRelativeRect: (cellRange: CellRange | CellAddress) => RectProps; + getCellRect: (col: number, row: number) => Rect; + getCellRelativeRect: (col: number, row: number) => Rect; + getCellsRect: (startCol: number, startRow: number, endCol: number, endRow: number) => Rect; + getCellRangeRect: (cellRange: CellRange | CellAddress) => Rect; + getCellRangeRelativeRect: (cellRange: CellRange | CellAddress) => Rect; + getVisibleCellRangeRelativeRect: (cellRange: CellRange | CellAddress) => Rect; isFrozenCell: (col: number, row: number) => { row: boolean; col: boolean } | null; getRowAt: (absoluteY: number) => { top: number; row: number; bottom: number }; getColAt: (absoluteX: number) => { left: number; col: number; right: number }; @@ -566,7 +570,6 @@ export interface BaseTableAPI { getRecordStartRowByRecordIndex: (index: number) => number; getHeaderField: (col: number, row: number) => any | undefined; - getHeaderFieldKey: (col: number, row: number) => any | undefined; _getHeaderCellBySortState: (sortState: SortState) => CellAddress | undefined; getHeaderDefine: (col: number, row: number) => ColumnDefine; @@ -686,7 +689,7 @@ export interface BaseTableAPI { /** 获取表格body部分的显示行号范围 */ getBodyVisibleRowRange: () => { rowStart: number; rowEnd: number }; - hasCustomRenderOrLayout: () => boolean; + _hasCustomRenderOrLayout: () => boolean; /** 根据表格单元格的行列号 获取在body部分的列索引及行索引 */ getBodyIndexByTableIndex: (col: number, row: number) => CellAddress; /** 根据body部分的列索引及行索引,获取单元格的行列号 */ @@ -700,6 +703,7 @@ export interface BaseTableAPI { export interface ListTableProtected extends IBaseTableProtected { /** 表格数据 */ records: any[] | null; + dataConfig?: IListTableDataConfig; columns: ColumnsDefine; layoutMap: SimpleHeaderLayoutMap; } @@ -708,7 +712,7 @@ export interface PivotTableProtected extends IBaseTableProtected { /** 表格数据 */ records: any[] | null; layoutMap: PivotHeaderLayoutMap; - dataConfig?: IDataConfig; + dataConfig?: IPivotTableDataConfig; /** * 透视表是否开启数据分析 * 如果传入数据是明细数据需要聚合分析则开启 @@ -731,7 +735,7 @@ export interface PivotChartProtected extends IBaseTableProtected { /** 表格数据 */ records: any[] | Record; layoutMap: PivotHeaderLayoutMap; - dataConfig?: IDataConfig; + dataConfig?: IPivotChartDataConfig; columnTree?: IHeaderTreeDefine[]; /** 行表头维度结构 */ rowTree?: IHeaderTreeDefine[]; diff --git a/packages/vtable/src/ts-types/common.ts b/packages/vtable/src/ts-types/common.ts index b9910d0bb..3647ca810 100644 --- a/packages/vtable/src/ts-types/common.ts +++ b/packages/vtable/src/ts-types/common.ts @@ -1,6 +1,6 @@ import type { ColumnTypeOption } from './column'; -import type { ColumnData } from './list-table/layout-map/api'; import type { CellLocation, FieldData, FieldDef } from './table-engine'; +import type { Rect } from '../tools/Rect'; export type MaybePromise = T | Promise; @@ -49,7 +49,7 @@ export type CellInfo = { /**单元格行列表头paths */ cellHeaderPaths?: ICellHeaderPaths; /**单元格的位置 */ - cellRange?: RectProps; + cellRange?: Rect; /**整条数据-原始数据 */ originData?: any; /**format之后的值 */ diff --git a/packages/vtable/src/ts-types/events.ts b/packages/vtable/src/ts-types/events.ts index 192d83207..b88454bea 100644 --- a/packages/vtable/src/ts-types/events.ts +++ b/packages/vtable/src/ts-types/events.ts @@ -82,11 +82,12 @@ export interface TableEventHandlersEventArgumentMap { scrollRatioY?: number; }; resize_column: { col: number; colWidth: number }; - resize_column_end: { col: number; columns: number[] }; + resize_column_end: { col: number; colWidths: number[] }; change_header_position: { source: CellAddress; target: CellAddress }; sort_click: { field: FieldDef; order: SortOrder; + event: Event; }; freeze_click: { col: number; row: number; fields: FieldDef[]; colCount: number }; dropdown_menu_click: DropDownMenuEventArgs; @@ -97,7 +98,7 @@ export interface TableEventHandlersEventArgumentMap { copy_data: { cellRange: CellRange[]; copyData: string }; drillmenu_click: DrillMenuEventInfo; - dropdown_icon_click: CellAddress; + dropdown_icon_click: CellAddress & { event: Event }; dropdown_menu_clear: CellAddress; show_menu: { @@ -116,6 +117,7 @@ export interface TableEventHandlersEventArgumentMap { y: number; funcType?: IconFuncTypeEnum | string; icon: Icon; + event: Event; }; pivot_sort_click: { @@ -124,6 +126,7 @@ export interface TableEventHandlersEventArgumentMap { order: SortOrder; dimensionInfo: IDimensionInfo[]; cellLocation: CellLocation; + event: Event; }; tree_hierarchy_state_change: { col: number; @@ -164,6 +167,7 @@ export interface DrillMenuEventInfo { drillUp: boolean; col: number; row: number; + event: Event; } export interface TableEventHandlersReturnMap { selected_cell: void; diff --git a/packages/vtable/src/ts-types/list-table/define/basic-define.ts b/packages/vtable/src/ts-types/list-table/define/basic-define.ts index 182c75b10..b74ecf5cb 100644 --- a/packages/vtable/src/ts-types/list-table/define/basic-define.ts +++ b/packages/vtable/src/ts-types/list-table/define/basic-define.ts @@ -6,15 +6,12 @@ import type { ColumnIconOption } from '../../icon'; import type { MenuListItem } from '../../menu'; import type { BaseTableAPI } from '../../base-table'; import type { IEditor } from '@visactor/vtable-editors'; +import type { Aggregation, CustomAggregation } from '../../new-data-set'; // eslint-disable-next-line no-unused-vars export interface IBasicHeaderDefine { // 表头的标题 title?: string | (() => string); //支持图文混合 - /** @deprecated - * 已废除该配置 标题中显示图标 现在请使用headerIcon进行配置 - */ - // captionIcon?: ColumnIconOption; /** 表头Icon配置 */ headerIcon?: string | ColumnIconOption | (string | ColumnIconOption)[]; // | ((args: CellInfo) => string | ColumnIconOption | (string | ColumnIconOption)[]); @@ -86,4 +83,5 @@ export interface IBasicColumnBodyDefine { customRender?: ICustomRender; customLayout?: ICustomLayout; editor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); + aggregation?: Aggregation | CustomAggregation | (Aggregation | CustomAggregation)[]; } diff --git a/packages/vtable/src/ts-types/list-table/layout-map/api.ts b/packages/vtable/src/ts-types/list-table/layout-map/api.ts index 84cca17d6..1c9660a66 100644 --- a/packages/vtable/src/ts-types/list-table/layout-map/api.ts +++ b/packages/vtable/src/ts-types/list-table/layout-map/api.ts @@ -18,8 +18,10 @@ import type { FieldKeyDef, CustomRenderFunctionArg, SparklineSpec, - HierarchyState + HierarchyState, + Aggregation } from '../../'; +import type { Aggregator } from '../../../dataset/statistics-helper'; import type { HeaderDefine, ColumnDefine, ColumnBodyDefine } from '../define'; @@ -53,7 +55,6 @@ export interface HeaderData extends WidthData { icons?: (string | ColumnIconOption)[] | ((args: CellInfo) => (string | ColumnIconOption)[]); field: FieldDef; - fieldKey?: FieldKeyDef; fieldFormat?: FieldFormat; style?: HeaderStyleOption | ColumnStyle | null | undefined; headerType: 'text' | 'link' | 'image' | 'video' | 'checkbox'; // headerType.BaseHeader; @@ -104,7 +105,7 @@ export interface WidthData { export interface ColumnData extends WidthData { id: LayoutObjectId; field: FieldDef; - fieldKey?: FieldKeyDef; + // fieldKey?: FieldKeyDef; fieldFormat?: FieldFormat; // icon?: ColumnIconOption | ColumnIconOption[]; icon?: @@ -130,6 +131,8 @@ export interface ColumnData extends WidthData { * 是否禁用调整列宽,如果是转置表格或者是透视表的指标是行方向指定 那该配置不生效 */ disableColumnResize?: boolean; + aggregation?: Aggregation | Aggregation[]; + aggregator?: Aggregator | Aggregator[]; } export interface IndicatorData extends WidthData { @@ -234,7 +237,7 @@ interface LayoutMapAPI { // getBodyLayoutRangeById: (id: LayoutObjectId) => CellRange; getHeaderCellAdressById: (id: number) => CellAddress | undefined; getHeaderCellAddressByField: (field: string) => CellAddress | undefined; - getRecordIndexByCell: (col: number, row: number) => number; + getRecordShowIndexByCell: (col: number, row: number) => number; getRecordStartRowByRecordIndex: (index: number) => number; /** 从定义中获取一列配置项width的定义值 */ getColumnWidthDefined: (col: number) => WidthData; diff --git a/packages/vtable/src/ts-types/menu.ts b/packages/vtable/src/ts-types/menu.ts index 62ec392d4..9eb7834fc 100644 --- a/packages/vtable/src/ts-types/menu.ts +++ b/packages/vtable/src/ts-types/menu.ts @@ -89,4 +89,5 @@ export type DropDownMenuEventInfo = { cellHeaderPaths?: ICellHeaderPaths; cellLocation: CellLocation; + event: Event; }; diff --git a/packages/vtable/src/ts-types/new-data-set.ts b/packages/vtable/src/ts-types/new-data-set.ts index e782943aa..5d0a3be7e 100644 --- a/packages/vtable/src/ts-types/new-data-set.ts +++ b/packages/vtable/src/ts-types/new-data-set.ts @@ -1,4 +1,5 @@ -import type { SortOrder } from './common'; +import type { Either } from '../tools/helper'; +import type { BaseTableAPI } from './base-table'; //#region 总计小计 export interface TotalsStatus { @@ -15,7 +16,8 @@ export enum AggregationType { MIN = 'MIN', MAX = 'MAX', AVG = 'AVG', - COUNT = 'COUNT' + COUNT = 'COUNT', + CUSTOM = 'CUSTOM' } export enum SortType { ASC = 'ASC', @@ -119,12 +121,14 @@ export type SortRules = SortRule[]; //#endregion 排序规则 //#region 过滤规则 -export interface FilterRule { +export interface FilterFuncRule { + filterFunc?: (row: Record) => boolean; +} +export interface FilterValueRule { filterKey?: string; filteredValues?: unknown[]; - filterFunc?: (row: Record) => boolean; } -export type FilterRules = FilterRule[]; +export type FilterRules = Either[]; //#endregion 过滤规则 //#region 聚合规则 @@ -135,7 +139,7 @@ export interface AggregationRule { field: T extends AggregationType.RECORD ? string[] | string : string; aggregationType: T; /**计算结果格式化 */ - formatFun?: (num: number) => string; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => number | string; } export type AggregationRules = AggregationRule[]; //#endregion 聚合规则 @@ -169,9 +173,19 @@ export interface DerivedFieldRule { } export type DerivedFieldRules = DerivedFieldRule[]; /** - * 数据处理配置 + * 基本表数据处理配置 + */ +export interface IListTableDataConfig { + // aggregationRules?: AggregationRules; //按照行列维度聚合值计算规则; + // sortRules?: SortTypeRule | SortByRule | SortFuncRule; //排序规则 不能简单的将sortState挪到这里 sort的规则在column中配置的; + filterRules?: FilterRules; //过滤规则; + // totals?: Totals; //小计或总计; + // derivedFieldRules?: DerivedFieldRules; +} +/** + * 透视表数据处理配置 */ -export interface IDataConfig { +export interface IPivotTableDataConfig { aggregationRules?: AggregationRules; //按照行列维度聚合值计算规则; sortRules?: SortRules; //排序规则; filterRules?: FilterRules; //过滤规则; @@ -181,17 +195,22 @@ export interface IDataConfig { */ mappingRules?: MappingRules; derivedFieldRules?: DerivedFieldRules; +} +/** + * 透视图数据处理配置 + */ +export interface IPivotChartDataConfig extends IPivotTableDataConfig { /** - * PivotChart专有 请忽略 + * PivotChart专有 */ collectValuesBy?: Record; /** - * PivotChart专有 请忽略 + * PivotChart专有 */ isPivotChart?: boolean; /** - * PivotChart专有 请忽略 + * PivotChart专有 */ dimensionSortArray?: string[]; } @@ -210,3 +229,18 @@ export type CollectValueBy = { sortBy?: string[]; }; export type CollectedValue = { max?: number; min?: number } | Array; + +//#region 提供给基本表格的类型 +export type Aggregation = { + aggregationType: AggregationType; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; + +export type CustomAggregation = { + aggregationType: AggregationType.CUSTOM; + aggregationFun: (values: any[], records: any[]) => any; + showOnTop?: boolean; + formatFun?: (value: number, col: number, row: number, table: BaseTableAPI) => string | number; +}; +//#endregion diff --git a/packages/vtable/src/ts-types/table-engine.ts b/packages/vtable/src/ts-types/table-engine.ts index 6e9934fa6..1b0a10093 100644 --- a/packages/vtable/src/ts-types/table-engine.ts +++ b/packages/vtable/src/ts-types/table-engine.ts @@ -3,8 +3,14 @@ import type { SvgIcon } from './icon'; export type { HeaderData } from './list-table/layout-map/api'; export type LayoutObjectId = number | string; import type { Rect } from '../tools/Rect'; -import type { BaseTableAPI, BaseTableConstructorOptions } from './base-table'; -import type { IDataConfig } from './new-data-set'; +import type { BaseTableAPI, BaseTableConstructorOptions, ListTableProtected } from './base-table'; +import type { + Aggregation, + AggregationType, + CustomAggregation, + FilterRules, + IPivotTableDataConfig +} from './new-data-set'; import type { Either } from '../tools/helper'; import type { IChartIndicator, @@ -98,14 +104,12 @@ export interface DataSourceAPI { updatePagination: (pagination: IPagination) => void; getIndexKey: (index: number) => number | number[]; /** 数据是否为树形结构 且可以展开收起 */ - enableHierarchyState: boolean; + hierarchyExpandLevel: number; } export interface SortState { /** 排序依据字段 */ field: FieldDef; - - fieldKey?: FieldKeyDef; /** 排序规则 */ order: SortOrder; } @@ -164,6 +168,8 @@ export interface ListTableConstructorOptions extends BaseTableConstructorOptions * 排序状态 */ sortState?: SortState | SortState[]; + /** 数据分析相关配置 enableDataAnalysis开启后该配置才会有效 */ + // dataConfig?: IListTableDataConfig; /** 全局设置表头编辑器 */ headerEditor?: string | IEditor | ((args: BaseCellInfo & { table: BaseTableAPI }) => string | IEditor); /** 全局设置编辑器 */ @@ -176,13 +182,23 @@ export interface ListTableConstructorOptions extends BaseTableConstructorOptions * "fixedFrozenCount"(可调整冻结列,并维持冻结数量不变):允许自由拖拽其他列的表头移入或移出冻结列位置,同时保持冻结列的数量不变。 */ frozenColDragHeaderMode?: 'disabled' | 'adjustFrozenCount' | 'fixedFrozenCount'; + aggregation?: + | Aggregation + | CustomAggregation + | (Aggregation | CustomAggregation)[] + | ((args: { + col: number; + field: string; + }) => Aggregation | CustomAggregation | (Aggregation | CustomAggregation)[] | null); } export interface ListTableAPI extends BaseTableAPI { options: ListTableConstructorOptions; editorManager: EditManeger; sortState: SortState[] | SortState | null; - // internalProps: ListTableProtected; + // /** 数据分析相关配置 */ + // dataConfig?: IListTableDataConfig; + internalProps: ListTableProtected; isListTable: () => true; isPivotTable: () => false; /** 设置单元格的value值,注意对应的是源数据的原始值,vtable实例records会做对应修改 */ @@ -206,6 +222,12 @@ export interface ListTableAPI extends BaseTableAPI { addRecord: (record: any, recordIndex?: number) => void; addRecords: (records: any[], recordIndex?: number) => void; deleteRecords: (recordIndexs: number[]) => void; + updateRecords: (records: any[], recordIndexs: number[]) => void; + updateFilterRules: (filterRules: FilterRules) => void; + getAggregateValuesByField: (field: string | number) => { + col: number; + aggregateValue: { aggregationType: AggregationType; value: number | string }[]; + }[]; } export interface PivotTableConstructorOptions extends BaseTableConstructorOptions { /** @@ -271,13 +293,8 @@ export interface PivotTableConstructorOptions extends BaseTableConstructorOption rowHeaderTitle?: ITitleDefine; //#endregion /** 数据分析相关配置 enableDataAnalysis开启后该配置才会有效 */ - dataConfig?: IDataConfig; - /** - * 透视表是否开启数据分析 默认false - * 如果传入数据是明细数据需要聚合分析则开启 赋值为true - * 如传入数据是经过聚合好的为了提升性能这里设为false即可,同时呢需要传入自己组织好的行头树结构columnTree和rowTree - */ - enableDataAnalysis?: boolean; + dataConfig?: IPivotTableDataConfig; + /** 指标标题 用于显示到角头的值*/ indicatorTitle?: string; /** 分页配置 */