diff --git a/build.config.base.mjs b/build.config.base.mjs index a2a703a47b..04968185cb 100644 --- a/build.config.base.mjs +++ b/build.config.base.mjs @@ -6,7 +6,10 @@ import path from 'path'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import { visualizer } from 'rollup-plugin-visualizer'; -export const getBaseConfig = () => { +export const getBaseConfig = ({ + aliasReact = false, + aliasReactComponents = false, +} = {}) => { const entry = './src/index.ts'; const OUT_DIR_NAME_MAP = { @@ -45,7 +48,22 @@ export const getBaseConfig = () => { find: /^@antv\/s2$/, replacement: path.join(__dirname, './packages/s2-core/src'), }, - ], + { + find: /^@antv\/s2\/extends$/, + replacement: path.join(__dirname, './packages/s2-core/src/extends'), + }, + aliasReact && { + find: /^@antv\/s2-react$/, + replacement: path.join(__dirname, './packages/s2-react/src'), + }, + aliasReactComponents && { + find: /^@antv\/s2-react-components$/, + replacement: path.join( + __dirname, + './packages/s2-react-components/src', + ), + }, + ].filter(Boolean), ); } diff --git a/jest.config.base.js b/jest.config.base.js index 38232a0aa7..1bb0193f5e 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -48,6 +48,7 @@ module.exports = { '^tests/(.*)': '/__tests__/$1', '^@antv/s2$': path.join(__dirname, 'packages/s2-core/src'), '^@antv/s2/esm/(.*)$': path.join(__dirname, 'packages/s2-core/src/$1'), + '^@antv/s2/extends$': path.join(__dirname, 'packages/s2-core/src/extends'), '^@antv/s2-react$': path.join(__dirname, 'packages/s2-react/src'), '^@antv/s2-react-components$': path.join( __dirname, diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap index 78e51f7466..4eb99c5a55 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap @@ -7,30 +7,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns and value } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 99.33, @@ -38,30 +24,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 99.33, @@ -78,30 +50,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns and value } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -109,30 +67,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 96, @@ -140,30 +84,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 96, @@ -180,29 +110,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns and value } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city/数值", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 0, "spreadsheet": Anything, "value": "province/city/数值", @@ -220,30 +137,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns and value } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -251,29 +154,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city/数值", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 80, "spreadsheet": Anything, "value": "province/city/数值", @@ -291,30 +181,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns is empty } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 149, @@ -322,30 +198,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 149, @@ -362,30 +224,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns is empty } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -393,30 +241,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 109, @@ -424,30 +258,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 109, @@ -464,29 +284,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns is empty } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 0, "spreadsheet": Anything, "value": "province/city", @@ -504,30 +311,16 @@ exports[`PivotSheet Corner Tests should render row corner when columns is empty } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -535,29 +328,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 80, "spreadsheet": Anything, "value": "province/city", @@ -575,30 +355,16 @@ exports[`PivotSheet Corner Tests should render row corner when measure hidden fo } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 99.33, @@ -606,30 +372,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 99.33, @@ -646,30 +398,16 @@ exports[`PivotSheet Corner Tests should render row corner when measure hidden fo } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -677,30 +415,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "province", "height": 30, - "hierarchy": undefined, "id": "province", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", "width": 96, @@ -708,30 +432,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "city", "height": 30, - "hierarchy": undefined, "id": "city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", "width": 96, @@ -748,29 +458,16 @@ exports[`PivotSheet Corner Tests should render row corner when measure hidden fo } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 0, "spreadsheet": Anything, "value": "province/city", @@ -788,30 +485,16 @@ exports[`PivotSheet Corner Tests should render row corner when measure hidden fo } 1`] = ` Array [ Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "series", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, - "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "序号", "width": 80, @@ -819,29 +502,16 @@ Array [ "y": 0, }, Object { - "belongsCell": undefined, "children": Array [], "colIndex": -1, "cornerType": "row", - "extra": undefined, "field": "", "height": 30, - "hierarchy": undefined, "id": "province/city", - "inCollapseNode": undefined, - "isCollapsed": undefined, - "isGrandTotals": undefined, - "isLeaf": undefined, + "isLeaf": false, "isPivotMode": true, - "isSubTotals": undefined, - "isTotalMeasure": undefined, - "isTotalRoot": undefined, - "isTotals": undefined, - "level": undefined, + "level": 0, "padding": 0, - "parent": undefined, - "query": undefined, - "rowIndex": undefined, "seriesNumberWidth": 80, "spreadsheet": Anything, "value": "province/city", diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/pivot-chart-sheet-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/pivot-chart-sheet-spec.ts.snap new file mode 100644 index 0000000000..aea0a56eb7 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/pivot-chart-sheet-spec.ts.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 1`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]总计", + "width": 200, + "x": 0, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 2`] = ` +Array [ + Object { + "height": 250, + "id": "root[&]四川省", + "width": 200, + "x": 0, + "y": 300, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 3`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]总计", + "width": 100, + "x": 0, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 4`] = ` +Array [ + Object { + "height": 250, + "id": "root[&]四川省", + "width": 100, + "x": 0, + "y": 300, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 5`] = `Array []`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 6`] = `Array []`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 7`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]家具[&]桌子[&]number", + "width": 200, + "x": 0, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen 8`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]办公用品[&]纸张[&]number", + "width": 200, + "x": 600, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen but row header 1`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]家具[&]桌子[&]number", + "width": 300, + "x": 0, + "y": 60, + }, + Object { + "height": 30, + "id": "root[&]家具[&]桌子", + "width": 300, + "x": 0, + "y": 30, + }, + Object { + "height": 30, + "id": "root[&]家具", + "width": 300, + "x": 0, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen but row header 2`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]办公用品[&]纸张[&]number", + "width": 300, + "x": 900, + "y": 60, + }, + Object { + "height": 30, + "id": "root[&]办公用品[&]纸张", + "width": 300, + "x": 900, + "y": 30, + }, + Object { + "height": 30, + "id": "root[&]办公用品", + "width": 300, + "x": 900, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen but row header 3`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]家具[&]桌子[&]number", + "width": 300, + "x": 0, + "y": 0, + }, +] +`; + +exports[`Pivot Chart Tests frozen should render pivot chart with frozen but row header 4`] = ` +Array [ + Object { + "height": 50, + "id": "root[&]办公用品[&]纸张[&]number", + "width": 300, + "x": 900, + "y": 0, + }, +] +`; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-spec.ts.snap index 383d6706a4..34a649920d 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-spec.ts.snap @@ -72,10 +72,8 @@ Object { "style": Object { "colCell": Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "cornerCell": Object { @@ -92,11 +90,9 @@ Object { }, "layoutWidthType": "adaptive", "rowCell": Object { - "heightByField": null, "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "seriesNumberCell": Object { diff --git a/packages/s2-core/__tests__/spreadsheet/pivot-chart-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/pivot-chart-sheet-spec.ts new file mode 100644 index 0000000000..066b576c5b --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/pivot-chart-sheet-spec.ts @@ -0,0 +1,884 @@ +import { get, head, map, omit } from 'lodash'; +import { getContainer, sleep } from 'tests/util/helpers'; +import { asyncGetAllPlainData } from '../../src'; +import { + EXTRA_FIELD, + LayoutWidthType, + OriginEventType, + TAB_SEPARATOR, +} from '../../src/common'; +import { Aggregation, type S2Options } from '../../src/common/interface'; +import { PivotChartSheet } from '../../src/extends'; +import { + KEY_GROUP_COL_AXIS_RESIZE_AREA, + KEY_GROUP_ROW_AXIS_RESIZE_AREA, + PLACEHOLDER_FIELD, +} from '../../src/extends/pivot-chart/constant'; +import type { PivotChartFacet } from '../../src/extends/pivot-chart/facet/pivot-chart-facet'; +import type { FrozenFacet } from '../../src/facet'; +import dataCfg from '../data/mock-dataset.json'; +import { pickMap } from '../util/fp'; + +describe('Pivot Chart Tests', () => { + let container: HTMLElement; + let s2: PivotChartSheet; + + const s2Options: S2Options = { + width: 800, + height: 700, + seriesNumber: { + enable: true, + }, + }; + + beforeEach(() => { + container = getContainer(); + }); + afterEach(() => { + s2?.destroy(); + }); + + describe('cartesian coordinate', () => { + test('should render pivot chart with 1 level row', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: true, + }, + }, + s2Options, + ); + + await s2.render(); + + const { + rowsHierarchy, + axisRowsHierarchy, + colsHierarchy, + axisColsHierarchy, + } = s2.facet.getLayoutResult(); + + // 只有一个维度时,会被拆分到 axisRow 中 + expect(rowsHierarchy.width).toEqual(0); + expect(axisRowsHierarchy!.width).toEqual(100); + expect(colsHierarchy.height).toEqual(60); + expect(axisColsHierarchy!.height).toEqual(50); + }); + + test('should render pivot chart with 2 level rows', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province', 'city'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + s2Options, + ); + + await s2.render(); + const { + rowsHierarchy, + axisRowsHierarchy, + colsHierarchy, + axisColsHierarchy, + } = s2.facet.getLayoutResult(); + + // 多个维度时,最后一个维度会被拆分到 axisRow 中 + expect(rowsHierarchy.width).toEqual(206); + // 默认情况,axis row cell 宽度固定为 100 + expect(axisRowsHierarchy!.width).toEqual(100); + + expect(colsHierarchy.height).toEqual(30); + // 默认情况下, axis col cell 高度固定为 50 + expect(axisColsHierarchy!.height).toEqual(50); + }); + + test('should render pivot chart with 3 level rows', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province', 'city', 'type'], + columns: ['sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + s2Options, + ); + + await s2.render(); + const { rowsHierarchy, axisRowsHierarchy, colLeafNodes } = + s2.facet.getLayoutResult(); + + // 多个维度时,最后一个维度会被拆分到 axisRow 中 + expect(rowsHierarchy.width).toEqual(264); + // 默认情况,axis row cell 宽度固定为 100 + expect(axisRowsHierarchy!.width).toEqual(100); + + // 列头只有一个维度,且数值置于行头时,列头会生成 placeholder 占位 + const leaf = head(colLeafNodes)!; + + expect(colLeafNodes).toHaveLength(1); + expect(leaf.field).toEqual(PLACEHOLDER_FIELD); + expect(leaf.value).toEqual('子类别'); + expect(leaf.width).toEqual(352); + expect(leaf.height).toEqual(30); + }); + + test('should render pivot chart with row totals', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + data: dataCfg.data.concat(dataCfg.totalData as any), + fields: { + rows: ['province', 'city', 'type', 'sub_type'], + columns: [], + values: ['number'], + valueInCols: true, + }, + }, + { + ...s2Options, + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province', 'city'], + grandTotalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }, + ); + + await s2.render(); + + const { rowNodes } = s2.facet.getLayoutResult(); + // 总计格子的横跨省份、城市、类别 + const grandTotalRoot = rowNodes.find((node) => node.id === 'root[&]总计'); + + expect(grandTotalRoot?.width).toEqual(600); + + // 省份的小计格子横跨城市和类别 + const subTotalRoot = rowNodes.find( + (node) => node.id === 'root[&]浙江省[&]小计', + ); + + expect(subTotalRoot?.width).toEqual(400); + }); + + test('should render pivot chart with cols totals', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + data: dataCfg.data.concat(dataCfg.totalData as any), + fields: { + rows: [], + columns: ['province', 'city', 'type', 'sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + { + ...s2Options, + totals: { + col: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province', 'city'], + grandTotalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }, + ); + + await s2.render(); + + const { colNodes } = s2.facet.getLayoutResult(); + // 总计格子的横跨列头区域 + const grandTotalRoot = colNodes.find((node) => node.id === 'root[&]总计'); + + expect(grandTotalRoot?.height).toEqual(90); + + // 省份的小计格子横跨城市和类别 + const subTotalRoot = colNodes.find( + (node) => node.id === 'root[&]浙江省[&]小计', + ); + + expect(subTotalRoot?.height).toEqual(60); + }); + }); + + describe('polar coordinate', () => { + const polarOptions: S2Options = { + ...s2Options, + chart: { + coordinate: 'polar', + dataCellSpec: { + type: 'interval', + transform: [{ type: 'stackY' }], + coordinate: { type: 'theta', outerRadius: 0.8 }, + }, + }, + }; + + test('should render pivot chart with 1 level row', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: true, + }, + }, + polarOptions, + ); + + await s2.render(); + + const { rowLeafNodes } = s2.facet.getLayoutResult(); + + // 只有一个维度时,因为是极坐标,所有会增加 placeholder 占位 + + const leaf = head(rowLeafNodes)!; + + expect(rowLeafNodes).toHaveLength(1); + expect(leaf.field).toEqual('province'); + expect(leaf.value).toEqual('省份'); + expect(leaf.width).toEqual(100); + expect(leaf.height).toEqual(200); + }); + + test('should render pivot chart with 2 level rows', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province', 'city'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + { + ...polarOptions, + style: { + layoutWidthType: 'compact', + }, + }, + ); + + await s2.render(); + const { axisRowsHierarchy, colsHierarchy, axisColsHierarchy } = + s2.facet.getLayoutResult(); + + // 默认情况,axis row cell 宽度固定为 100 + expect(axisRowsHierarchy!.width).toEqual(100); + + // 极坐标情况下,不展示坐标轴,而是按照原文字形式展示 + const axisRowCell = head((s2.facet as PivotChartFacet).getAxisRowCells()); + + expect(axisRowCell?.getActualText()).toEqual('数量'); + + expect(colsHierarchy.height).toEqual(30); + // 极坐标不展示单独坐标轴 + expect(axisColsHierarchy!.height).toEqual(0); + }); + + test('should render pivot chart with 3 level rows', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + fields: { + rows: ['province', 'city', 'type'], + columns: ['sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + polarOptions, + ); + + await s2.render(); + const { colLeafNodes } = s2.facet.getLayoutResult(); + + // 列头只有一个维度,且数值置于行头时,列头会生成 placeholder 占位 + const leaf = head(colLeafNodes)!; + + expect(colLeafNodes).toHaveLength(1); + expect(leaf.field).toEqual(PLACEHOLDER_FIELD); + expect(leaf.value).toEqual('子类别'); + expect(leaf.width).toEqual(200); + expect(leaf.height).toEqual(30); + }); + + test('should render pivot chart with row totals', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + data: dataCfg.data.concat(dataCfg.totalData as any), + fields: { + rows: ['province', 'city', 'type', 'sub_type'], + columns: [], + values: ['number'], + valueInCols: true, + }, + }, + { + ...polarOptions, + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province', 'city'], + grandTotalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }, + ); + + await s2.render(); + + const { rowNodes } = s2.facet.getLayoutResult(); + // 总计格子的横跨省份和城市 + const grandTotalRoot = rowNodes.find((node) => node.id === 'root[&]总计'); + + expect(grandTotalRoot?.width).toEqual(600); + + // 省份的小计格子横跨城市和类别 + const subTotalRoot = rowNodes.find( + (node) => node.id === 'root[&]浙江省[&]小计', + ); + + expect(subTotalRoot?.width).toEqual(400); + }); + + test('should render pivot chart with cols totals', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + data: dataCfg.data.concat(dataCfg.totalData as any), + fields: { + rows: [], + columns: ['province', 'city', 'type', 'sub_type'], + values: ['number'], + valueInCols: false, + }, + }, + { + ...polarOptions, + totals: { + col: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province', 'city'], + grandTotalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }, + ); + + await s2.render(); + + const { colNodes } = s2.facet.getLayoutResult(); + // 总计格子的横跨列头区域 + const grandTotalRoot = colNodes.find((node) => node.id === 'root[&]总计'); + + expect(grandTotalRoot?.height).toEqual(90); + + // 省份的小计格子横跨城市和类别 + const subTotalRoot = colNodes.find( + (node) => node.id === 'root[&]浙江省[&]小计', + ); + + expect(subTotalRoot?.height).toEqual(60); + }); + }); + + describe('layoutWithType', () => { + test('should render pivot chart with adaptive layout', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...s2Options, + style: { + layoutWidthType: LayoutWidthType.Adaptive, + }, + }); + + await s2.render(); + + const { + rowsHierarchy, + axisRowsHierarchy, + colLeafNodes, + axisColsHierarchy, + } = s2.facet.getLayoutResult(); + + const rowSampleNodeWidths = rowsHierarchy.sampleNodesForAllLevels.map( + (node) => node.width, + ); + + const colLeafNodeWidths = colLeafNodes.map((node) => node.width); + + // 只有一个维度时,会被拆分到 axisRow 中 + expect(rowSampleNodeWidths).toEqual([200]); + expect(colLeafNodeWidths).toEqual([200, 200, 200, 200]); + expect(axisRowsHierarchy!.width).toEqual(100); + expect(axisColsHierarchy!.height).toEqual(50); + }); + test('should render pivot chart with colAdaptive layout', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...s2Options, + style: { + layoutWidthType: LayoutWidthType.ColAdaptive, + }, + }); + + await s2.render(); + + const { + rowsHierarchy, + axisRowsHierarchy, + colLeafNodes, + axisColsHierarchy, + } = s2.facet.getLayoutResult(); + + const rowSampleNodeWidths = rowsHierarchy.sampleNodesForAllLevels.map( + (node) => node.width, + ); + + const colLeafNodeWidths = colLeafNodes.map((node) => node.width); + + // 只有一个维度时,会被拆分到 axisRow 中 + expect(rowSampleNodeWidths).toEqual([54]); + expect(colLeafNodeWidths).toEqual([200, 200, 200, 200]); + expect(axisRowsHierarchy!.width).toEqual(100); + expect(axisColsHierarchy!.height).toEqual(50); + }); + + test('should render pivot chart with compact layout', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...s2Options, + style: { + layoutWidthType: LayoutWidthType.Compact, + }, + }); + + await s2.render(); + + const { + rowsHierarchy, + axisRowsHierarchy, + colLeafNodes, + axisColsHierarchy, + } = s2.facet.getLayoutResult(); + + const rowSampleNodeWidths = rowsHierarchy.sampleNodesForAllLevels.map( + (node) => node.width, + ); + + const colLeafNodeWidths = colLeafNodes.map((node) => node.width); + + // 只有一个维度时,会被拆分到 axisRow 中 + expect(rowSampleNodeWidths).toEqual([54]); + expect(colLeafNodeWidths).toEqual([200, 200, 200, 200]); + expect(axisRowsHierarchy!.width).toEqual(100); + expect(axisColsHierarchy!.height).toEqual(50); + }); + }); + + describe('formatter', () => { + test('should render pivot chart with formatter', async () => { + s2 = new PivotChartSheet( + container, + { + ...dataCfg, + meta: [ + { + field: 'city', + name: '城市', + formatter: (v) => `[[${v}]]`, + }, + { + field: 'number', + name: '数量', + description: '数量说明。。', + formatter: (v: number) => v.toFixed(2), + }, + ], + }, + { + ...s2Options, + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }, + ); + + await s2.render(); + + await sleep(3000); + + // row axis formatter + const rowCell = (s2.facet as PivotChartFacet).getAxisRowCells()[1]; + const rowAxisOptions = rowCell?.getChartOptions(); + const domain = get(rowAxisOptions, 'scale.x.domain'); + + expect(domain).toEqual([ + '小计', + '[[杭州市]]', + '[[绍兴市]]', + '[[宁波市]]', + '[[舟山市]]', + ]); + + // col axis formatter + const colCell = (s2.facet as PivotChartFacet).getAxisColCells()[0]; + const colAxisOptions = colCell?.getChartOptions(); + const formatter = get(colAxisOptions, 'labelFormatter'); + + expect(formatter(4000)).toEqual('4000.00'); + + // tooltip formatter + await sleep(3000); + + const canvas = s2.getCanvasElement(); + const bbox = canvas.getBoundingClientRect(); + + let mousemoveEvent = new MouseEvent(OriginEventType.POINTER_MOVE, { + clientX: bbox.left + 460, + clientY: bbox.top + 150, + }); + + canvas.dispatchEvent(mousemoveEvent); + + expect( + document.querySelector('.g2-tooltip-title')!.innerText, + ).toEqual('小计'); + + expect( + document.querySelector('.g2-tooltip-list')!.innerText, + ).toEqual('数量\n18375.00'); + + await sleep(3000); + + mousemoveEvent = new MouseEvent(OriginEventType.POINTER_MOVE, { + clientX: bbox.left + 460, + clientY: bbox.top + 200, + }); + + canvas.dispatchEvent(mousemoveEvent); + + expect( + document.querySelector('.g2-tooltip-title')!.innerText, + ).toEqual('[[杭州市]]'); + + expect( + document.querySelector('.g2-tooltip-list')!.innerText, + ).toEqual('数量\n7789.00'); + }); + }); + + describe('frozen', () => { + function expectFrozenGroup(s2: PivotChartSheet, headerName: string) { + const pickCoordinate = pickMap(['id', 'x', 'y', 'width', 'height']); + + const actualHead = pickCoordinate( + map(s2.facet[headerName]?.frozenGroup.children, 'meta'), + ); + + expect(actualHead).toMatchSnapshot(); + + const actualTrailing = pickCoordinate( + map(s2.facet[headerName]?.frozenTrailingGroup.children, 'meta'), + ); + + expect(actualTrailing).toMatchSnapshot(); + } + + function getFrozenGroupPosition(s2: PivotChartSheet, headerName: string) { + return [ + s2.facet[headerName]?.frozenGroup.getPosition().map(Math.floor), + s2.facet[headerName]?.frozenTrailingGroup.getPosition().map(Math.floor), + ]; + } + const options = { + ...s2Options, + width: 1000, + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + }, + }, + }; + + test('should render pivot chart with frozen', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...options, + frozen: { + rowCount: 1, + trailingRowCount: 1, + colCount: 1, + trailingColCount: 1, + }, + }); + + await s2.render(); + + expect((s2.facet as FrozenFacet).frozenGroupAreas).toMatchObject({ + frozenCol: { + width: 200, + x: 0, + range: [0, 0], + }, + frozenTrailingCol: { + width: 200, + x: 600, + range: [3, 3], + }, + frozenRow: { + height: 50, + y: 0, + range: [0, 0], + }, + frozenTrailingRow: { + height: 250, + y: 300, + range: [2, 2], + }, + }); + + expectFrozenGroup(s2, 'rowHeader'); + expectFrozenGroup(s2, 'axisRowHeader'); + expectFrozenGroup(s2, 'colHeader'); + expectFrozenGroup(s2, 'axisColumnHeader'); + }); + + test('should render pivot chart with frozen but row header', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...options, + style: { + colCell: { + widthByField: { [EXTRA_FIELD]: 300 }, + }, + }, + frozen: { + rowHeader: false, + colCount: 1, + trailingColCount: 1, + }, + }); + + await s2.render(); + + expect((s2.facet as FrozenFacet).frozenGroupAreas).toMatchObject({ + frozenCol: { + width: 300, + x: 0, + range: [0, 0], + }, + frozenTrailingCol: { + width: 300, + x: 900, + range: [3, 3], + }, + frozenRow: { + height: 0, + y: 0, + range: [], + }, + frozenTrailingRow: { + height: 0, + y: 0, + range: [], + }, + }); + + expectFrozenGroup(s2, 'columnHeader'); + expectFrozenGroup(s2, 'axisColumnHeader'); + let prevCol = getFrozenGroupPosition(s2, 'columnHeader'); + let prevAxisCol = getFrozenGroupPosition(s2, 'axisColumnHeader'); + + s2.interaction.scrollTo({ offsetX: { value: 100, animate: false } }); + // 移动后,frozen col 会改变 而 trailing col 的位置不变 + let currentCol = getFrozenGroupPosition(s2, 'columnHeader'); + let currentAxisCol = getFrozenGroupPosition(s2, 'axisColumnHeader'); + + expect(currentCol[0]?.[0]).toEqual(prevCol[0]?.[0]! - 100); + expect(currentCol[1]).toEqual(prevCol[1]); + + expect(currentAxisCol[0]?.[0]).toEqual(prevAxisCol[0]?.[0]! - 100); + expect(currentAxisCol[1]).toEqual(prevAxisCol[1]); + + // 移动超过角头宽度 + // 移动后,frozen col 和 trailing col 的位置都不变 + s2.interaction.scrollTo({ offsetX: { value: 400, animate: false } }); + prevCol = getFrozenGroupPosition(s2, 'columnHeader'); + prevAxisCol = getFrozenGroupPosition(s2, 'axisColumnHeader'); + + s2.interaction.scrollTo({ offsetX: { value: 400, animate: false } }); + + currentCol = getFrozenGroupPosition(s2, 'columnHeader'); + currentAxisCol = getFrozenGroupPosition(s2, 'axisColumnHeader'); + + expect(currentCol).toEqual(prevCol); + expect(currentCol).toEqual([ + [2, 0, 0], + [-200, 0, 0], + ]); + + expect(currentAxisCol).toEqual(prevAxisCol); + expect(currentAxisCol).toEqual([ + [2, 612, 0], + [-200, 612, 0], + ]); + }); + }); + + describe('interaction', () => { + test('should render axis resize area', async () => { + s2 = new PivotChartSheet(container, dataCfg, s2Options); + await s2.render(); + + const group = s2.facet.foregroundGroup; + + expect( + group.getElementById(KEY_GROUP_ROW_AXIS_RESIZE_AREA), + ).not.toBeNull(); + expect( + group.getElementById(KEY_GROUP_COL_AXIS_RESIZE_AREA), + ).not.toBeNull(); + }); + + test('should render axis resize area with polar coordinate', async () => { + s2 = new PivotChartSheet(container, dataCfg, { + ...s2Options, + chart: { + coordinate: 'polar', + }, + }); + await s2.render(); + + const group = s2.facet.foregroundGroup; + + expect(group.getElementById(KEY_GROUP_ROW_AXIS_RESIZE_AREA)).toBeNull(); + expect( + group.getElementById(KEY_GROUP_COL_AXIS_RESIZE_AREA), + ).not.toBeNull(); + }); + + test('should throw error when call asyncGetAllPlainData', async () => { + s2 = new PivotChartSheet(container, dataCfg, s2Options); + await s2.render(); + + expect.assertions(1); + + try { + await asyncGetAllPlainData({ + sheetInstance: s2, + split: TAB_SEPARATOR, + formatOptions: true, + }); + } catch (e) { + // eslint-disable-next-line jest/no-conditional-expect + expect(e.message).toEqual( + "pivot chart doesn't support export all data", + ); + } + }); + }); + + describe('style', () => { + test('should match theme color', async () => { + s2 = new PivotChartSheet(container, dataCfg, s2Options); + s2.setThemeCfg({ name: 'dark' }); + await s2.render(); + expect(s2.theme.axisCornerCell).toEqual(s2.theme.cornerCell); + expect(s2.theme.axisRowCell).toEqual(s2.theme.rowCell); + expect(omit(s2.theme.axisColCell, ['measureText'])).toEqual( + omit(s2.theme.colCell, ['measureText']), + ); + expect(omit(s2.theme.axisColCell.measureText, ['textAlign'])).toEqual( + omit(s2.theme.colCell.measureText, ['textAlign']), + ); + expect(s2.theme.axisColCell.measureText.textAlign).toEqual('center'); + }); + }); +}); diff --git a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts index d447c70ce8..562dbff832 100644 --- a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts @@ -146,9 +146,6 @@ describe('Data Cell Tests', () => { const dataCell = new DataCell(meta, s2); expect(dataCell.isMultiData()).toBeFalsy(); - expect(dataCell.isChartData()).toBeFalsy(); - expect(dataCell.getRenderChartData()).toBeUndefined(); - expect(dataCell.getRenderChartOptions()).toMatchSnapshot(); }); test('should get correctly cell data status', () => { @@ -161,7 +158,6 @@ describe('Data Cell Tests', () => { const dataCell = new DataCell(multipleMeta, s2); expect(dataCell.isMultiData()).toBeTruthy(); - expect(dataCell.isChartData()).toBeFalsy(); }); test('should get multiple chart data and all options', () => { @@ -195,9 +191,6 @@ describe('Data Cell Tests', () => { const dataCell = new DataCell(multipleMeta, s2); expect(dataCell.isMultiData()).toBeTruthy(); - expect(dataCell.isChartData()).toBeTruthy(); - expect(dataCell.getRenderChartData()).toMatchSnapshot(); - expect(dataCell.getRenderChartOptions()).toMatchSnapshot(); }); }); diff --git a/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts b/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts index 229ef9cde7..9afc3e9c23 100644 --- a/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts @@ -22,6 +22,9 @@ describe('cornerBBox test', () => { getLayoutResult() { return layoutResult; }, + getCellCustomSize() { + return 20; + }, getSeriesNumberWidth() { return 80; }, @@ -40,6 +43,7 @@ describe('cornerBBox test', () => { }, }, }, + facet: mockFacet, }, } as unknown as BaseFacet; }); diff --git a/packages/s2-core/__tests__/unit/facet/layout/node-spec.ts b/packages/s2-core/__tests__/unit/facet/layout/node-spec.ts index e6735e5e24..35e0dbc7e3 100644 --- a/packages/s2-core/__tests__/unit/facet/layout/node-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/layout/node-spec.ts @@ -4,20 +4,17 @@ import { SERIES_NUMBER_FIELD } from '../../../../src'; describe('Node Test', () => { const root = new Node({ id: 'root', - key: 'root', value: 'root', children: [], }); const child = new Node({ id: 'child', - key: 'child', value: 'child', isLeaf: true, children: [], }); const node = new Node({ id: 'root[&]country', - key: '', value: '', field: 'country', parent: root, @@ -46,7 +43,7 @@ describe('Node Test', () => { }); test('#getHeadLeafChild()', () => { - expect(node.getHeadLeafChild().id).toEqual('child'); + expect(node.getHeadLeafChild()?.id).toEqual('child'); }); test('#getTotalHeightForTreeHierarchy()', () => { diff --git a/packages/s2-core/__tests__/unit/interaction/__snapshots__/row-column-resize-spec.ts.snap b/packages/s2-core/__tests__/unit/interaction/__snapshots__/row-column-resize-spec.ts.snap index f836cf3c9f..c4e218eb43 100644 --- a/packages/s2-core/__tests__/unit/interaction/__snapshots__/row-column-resize-spec.ts.snap +++ b/packages/s2-core/__tests__/unit/interaction/__snapshots__/row-column-resize-spec.ts.snap @@ -3,7 +3,6 @@ exports[`Interaction Row Column Resize Tests should get horizontal filed resize style by field for all resize type and table mode 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": 5, @@ -17,7 +16,6 @@ Object { exports[`Interaction Row Column Resize Tests should get horizontal filed resize style by field for current resize type 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": undefined, @@ -31,7 +29,6 @@ Object { exports[`Interaction Row Column Resize Tests should get horizontal filed resize style by field for current resize type and table mode 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": undefined, @@ -45,7 +42,6 @@ Object { exports[`Interaction Row Column Resize Tests should get horizontal filed resize style by field for selected resize type 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": undefined, @@ -60,7 +56,6 @@ Object { exports[`Interaction Row Column Resize Tests should get multiple horizontal filed resize style by field for selected resize type 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": undefined, @@ -82,7 +77,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -96,7 +90,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -104,10 +97,8 @@ Object { exports[`Interaction Row Column Resize Tests should get vertical cell resize style 2`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -131,7 +122,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -146,7 +136,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -160,7 +149,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; @@ -168,7 +156,6 @@ Object { exports[`Interaction Row Column Resize Tests should not effect default resize style by field for selected resize type 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": 50, @@ -182,7 +169,6 @@ Object { exports[`Interaction Row Column Resize Tests should rerender by resize col cell 1`] = ` Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", "width": 40, @@ -202,7 +188,6 @@ Object { "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, } `; diff --git a/packages/s2-core/__tests__/unit/shared/utils/__snapshots__/options-spec.ts.snap b/packages/s2-core/__tests__/unit/shared/utils/__snapshots__/options-spec.ts.snap index b7b90481cb..a59fe91271 100644 --- a/packages/s2-core/__tests__/unit/shared/utils/__snapshots__/options-spec.ts.snap +++ b/packages/s2-core/__tests__/unit/shared/utils/__snapshots__/options-spec.ts.snap @@ -91,10 +91,8 @@ Object { "style": Object { "colCell": Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "cornerCell": Object { @@ -111,11 +109,9 @@ Object { }, "layoutWidthType": "adaptive", "rowCell": Object { - "heightByField": null, "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "seriesNumberCell": Object { diff --git a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts index 80cfd60646..2f4d9a437a 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts @@ -25,7 +25,7 @@ import { cloneDeep, last } from 'lodash'; import dataCfg from 'tests/data/simple-data.json'; import { waitForRender } from 'tests/util'; import { createPivotSheet, getContainer, sleep } from 'tests/util/helpers'; -import { +import type { BaseEvent, BaseTooltipOperatorMenuOptions, CornerCell, diff --git a/packages/s2-core/__tests__/unit/utils/__snapshots__/merge-spec.ts.snap b/packages/s2-core/__tests__/unit/utils/__snapshots__/merge-spec.ts.snap index 3218784a8a..c15107a693 100644 --- a/packages/s2-core/__tests__/unit/utils/__snapshots__/merge-spec.ts.snap +++ b/packages/s2-core/__tests__/unit/utils/__snapshots__/merge-spec.ts.snap @@ -72,10 +72,8 @@ Object { "style": Object { "colCell": Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "cornerCell": Object { @@ -92,11 +90,9 @@ Object { }, "layoutWidthType": "adaptive", "rowCell": Object { - "heightByField": null, "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "seriesNumberCell": Object { @@ -125,10 +121,8 @@ exports[`merge test should not setup correctly compact layout width type style 1 Object { "colCell": Object { "height": 30, - "heightByField": null, "maxLines": 1, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "cornerCell": Object { @@ -145,11 +139,9 @@ Object { }, "layoutWidthType": "compact", "rowCell": Object { - "heightByField": null, "maxLines": 1, "showTreeLeafNodeAlignDot": false, "textOverflow": "ellipsis", - "widthByField": null, "wordWrap": true, }, "seriesNumberCell": Object { diff --git a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts index e860d79cb1..e5d1b136bd 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts @@ -3,7 +3,7 @@ import type { Node } from '@/facet/layout/node'; import type { SpreadSheet } from '@/sheet-type/spread-sheet'; import { getActiveHoverHeaderCells, - updateAllColHeaderCellState, + updateAllHeaderCellState, } from '@/utils/interaction/hover-event'; import { InteractionStateName } from '@/common'; @@ -51,7 +51,7 @@ describe('Hover Event Utils Tests', () => { new ColCell({} as unknown as Node, {} as unknown as SpreadSheet), ]; - updateAllColHeaderCellState( + updateAllHeaderCellState( 'root[&]city', cells, InteractionStateName.HOVER, diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index 06ff5b6462..b25c4b45e7 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -26,6 +26,17 @@ "*.css", "dist/*" ], + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./lib/index.js" + }, + "./extends": { + "import": "./esm/extends/index.js", + "require": "./lib/extends/index.js" + }, + "./*": "./*" + }, "main": "lib/index.js", "unpkg": "dist/s2.min.js", "module": "esm/index.js", @@ -74,6 +85,7 @@ "tinycolor2": "^1.6.0" }, "devDependencies": { + "@antv/g2": "^5.1.21", "@testing-library/dom": "^10.1.0", "@types/d3-dsv": "^3.0.7", "@types/d3-ease": "^3.0.2", @@ -83,6 +95,14 @@ "csstype": "^3.1.3", "d3-dsv": "^1.1.1" }, + "peerDependencies": { + "@antv/g2": ">=5.1.21" + }, + "peerDependenciesMeta": { + "@antv/g2": { + "optional": true + } + }, "publishConfig": { "access": "public" }, diff --git a/packages/s2-core/rollup.config.mjs b/packages/s2-core/rollup.config.mjs index 1244e2952e..4fd868cdc3 100644 --- a/packages/s2-core/rollup.config.mjs +++ b/packages/s2-core/rollup.config.mjs @@ -27,6 +27,7 @@ const output = { exports: 'named', name: 'S2', sourcemap: true, + dir: outDir, }; const plugins = [ @@ -85,15 +86,32 @@ if (enableAnalysis) { } if (isUmdFormat) { - output.file = 'dist/s2.min.js'; + output.globals = { + '@antv/s2': 'S2', + }; + output.entryFileNames = '[name].min.js'; plugins.push(terser()); -} else { - output.dir = outDir; } // eslint-disable-next-line import/no-default-export -export default { - input: 'src/index.ts', - output, - plugins, -}; +export default [ + { + input: { + s2: 'src/index.ts', + }, + output, + plugins, + }, + { + input: { + 's2-extends': 'src/extends/index.ts', + }, + output: { + ...output, + name: 'S2Extends', + }, + plugins, + + external: ['@antv/s2'], + }, +]; diff --git a/packages/s2-core/scripts/sync-event.mjs b/packages/s2-core/scripts/sync-event.mjs index 7d7f3593d2..b0a754e7f3 100644 --- a/packages/s2-core/scripts/sync-event.mjs +++ b/packages/s2-core/scripts/sync-event.mjs @@ -82,7 +82,6 @@ const readListLine = (question) => { const VUE_USE_EVENTS_PATH = 'packages/s2-vue/src/hooks/useEvents.ts'; const REACT_USE_EVENTS_PATH = 'packages/s2-react/src/hooks/useEvents.ts'; const VUE_INTERFACE_PATH = 'packages/s2-vue/src/utils/initPropAndEmits.ts'; -const COMMON_INTERFACE_PATH = 'packages/s2-shared/src/interface.ts'; function insertVueUseEvent(eventName, eventHookName) { const vueStr = vueEventTemplate(eventName, eventHookName); @@ -105,17 +104,6 @@ function insertVueInterface(eventName) { insertEventIntoFile(vuePath, `'${vueEventName}',`); } -function insertCommonInterface(eventName, eventHookName) { - const commonEventName = getReactEventName(eventName); - const commonInterfaceTemplate = getCommonInterfaceTemplate( - eventHookName, - commonEventName, - ); - const reactPath = resolve(process.cwd(), `../../${COMMON_INTERFACE_PATH}`); - - insertEventIntoFile(reactPath, `${commonInterfaceTemplate}`); -} - const syncEvent = async () => { const { eventName } = await readInputLine('请输入事件名称:'); const { eventHookName } = await readListLine( @@ -126,7 +114,6 @@ const syncEvent = async () => { insertVueUseEvent(eventName, eventHookName); insertVueInterface(eventName); insertReactUseEvent(eventName, eventHookName); - insertCommonInterface(eventName, eventHookName); // eslint-disable-next-line no-console console.warn(`✅${eventName}插入完成! ⚠️ 注意自己检查生成结果和格式化一下`); diff --git a/packages/s2-core/src/cell/col-cell.ts b/packages/s2-core/src/cell/col-cell.ts index 58bc79dca4..2dfe02f3e0 100644 --- a/packages/s2-core/src/cell/col-cell.ts +++ b/packages/s2-core/src/cell/col-cell.ts @@ -121,7 +121,6 @@ export class ColCell extends HeaderCell { viewportWidth, cornerWidth = 0, scrollX = 0, - position, } = this.getHeaderConfig(); const frozenGroupAreas = (this.spreadsheet.facet as FrozenFacet) @@ -138,18 +137,13 @@ export class ColCell extends HeaderCell { }; } - const scrollXUntilColStickToLeft = frozenColGroupWidth - ? cornerWidth - : position.x; - return { - start: - frozenColGroupWidth + Math.max(0, scrollX - scrollXUntilColStickToLeft), + start: frozenColGroupWidth + Math.max(0, scrollX - cornerWidth), size: viewportWidth - frozenColGroupWidth - frozenTrailingColGroupWidth + - Math.min(scrollX, scrollXUntilColStickToLeft), + Math.min(scrollX, cornerWidth), }; } @@ -265,6 +259,7 @@ export class ColCell extends HeaderCell { } const { y, height } = this.meta; + const { position } = this.getHeaderConfig(); const resizeStyle = this.getResizeAreaStyle(); const resizeArea = this.getColResizeArea(); @@ -282,6 +277,7 @@ export class ColCell extends HeaderCell { return; } + const offsetY = position.y + y; const resizeAreaWidth = this.getResizeAreaWidth(); // 列高调整热区 @@ -290,7 +286,7 @@ export class ColCell extends HeaderCell { type: ResizeDirectionType.Vertical, effect: ResizeAreaEffect.Field, offsetX: 0, - offsetY: y, + offsetY, width: resizeAreaWidth, height, meta: this.meta, @@ -303,7 +299,7 @@ export class ColCell extends HeaderCell { style: { ...attrs.style, x: 0, - y: y + height - resizeStyle.size!, + y: offsetY + height - resizeStyle.size!, width: resizeAreaWidth, }, }, @@ -322,16 +318,19 @@ export class ColCell extends HeaderCell { } protected shouldAddVerticalResizeArea() { + if (this.getMeta().isFrozen) { + return true; + } + const { x, y, width, height } = this.meta; const { - scrollX, + scrollX = 0, scrollY, cornerWidth = 0, height: headerHeight, width: headerWidth, } = this.getHeaderConfig(); - const scrollContainsRowHeader = !this.spreadsheet.isFrozenRowHeader(); const resizeStyle = this.getResizeAreaStyle(); const resizeAreaBBox: SimpleBBox = { @@ -341,12 +340,29 @@ export class ColCell extends HeaderCell { height, }; - const resizeClipAreaBBox: SimpleBBox = { - x: scrollContainsRowHeader ? -cornerWidth : 0, - y: 0, - width: scrollContainsRowHeader ? cornerWidth + headerWidth : headerWidth, - height: headerHeight, - }; + const frozenGroupAreas = (this.spreadsheet.facet as FrozenFacet) + .frozenGroupAreas; + const colWidth = frozenGroupAreas[FrozenGroupArea.Col].width; + const trailingColWidth = + frozenGroupAreas[FrozenGroupArea.TrailingCol].width; + + let resizeClipAreaBBox: SimpleBBox; + + if (this.spreadsheet.isFrozenRowHeader()) { + resizeClipAreaBBox = { + x: colWidth, + y: 0, + width: headerWidth - colWidth - trailingColWidth, + height: headerHeight, + }; + } else { + resizeClipAreaBBox = { + x: colWidth - cornerWidth, + y: 0, + width: headerWidth - colWidth - trailingColWidth + cornerWidth, + height: headerHeight, + }; + } return shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { scrollX, @@ -356,10 +372,41 @@ export class ColCell extends HeaderCell { protected getVerticalResizeAreaOffset() { const { x, y } = this.meta; - const { scrollX = 0, position } = this.getHeaderConfig(); + const { + scrollX = 0, + position, + cornerWidth = 0, + viewportWidth, + } = this.getHeaderConfig(); + + const isFrozenRowHeader = this.spreadsheet.isFrozenRowHeader(); + + const frozenGroupAreas = (this.spreadsheet.facet as FrozenFacet) + .frozenGroupAreas; + + const frozenColGroup = frozenGroupAreas[FrozenGroupArea.Col]; + const frozenTrailingColGroup = + frozenGroupAreas[FrozenGroupArea.TrailingCol]; + + let offsetX = position?.x; + + if (this.getMeta().isFrozenHead) { + offsetX += + x - + frozenColGroup.x - + (isFrozenRowHeader ? 0 : Math.min(scrollX, cornerWidth)); + } else if (this.getMeta().isFrozenTrailing) { + offsetX += + x - + frozenTrailingColGroup.x + + viewportWidth - + frozenTrailingColGroup.width; + } else { + offsetX += x - scrollX; + } return { - x: position?.x + x - scrollX, + x: offsetX, y: position?.y + y, }; } @@ -367,6 +414,7 @@ export class ColCell extends HeaderCell { protected drawVerticalResizeArea() { if ( !this.meta.isLeaf || + this.meta.hideColCellHorizontalResize || !this.shouldDrawResizeAreaByType('colCellHorizontal', this) ) { return; diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 8a800de508..87cfadc644 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -1,16 +1,6 @@ import type { PointLike } from '@antv/g'; -import { - find, - first, - get, - isEmpty, - isEqual, - isObject, - isPlainObject, - merge, -} from 'lodash'; +import { find, first, get, isEmpty, isEqual, isObject, merge } from 'lodash'; import { BaseCell } from '../cell/base-cell'; -import { G2_THEME_TYPE } from '../common'; import { EMPTY_PLACEHOLDER } from '../common/constant/basic'; import { CellType, @@ -18,7 +8,6 @@ import { SHAPE_STYLE_MAP, } from '../common/constant/interaction'; import type { - BaseChartData, CellMeta, Condition, ConditionMappingResult, @@ -26,8 +15,6 @@ import type { HeaderActionNameOptions, IconCondition, InteractionStateTheme, - MiniChartData, - MultiData, TextTheme, ValueRange, ViewMeta, @@ -91,33 +78,6 @@ export class DataCell extends BaseCell { return isObject(fieldValue); } - public isChartData() { - const fieldValue = this.getFieldValue(); - - return isPlainObject( - (fieldValue as unknown as MultiData)?.values, - ); - } - - public getRenderChartData(): BaseChartData { - const { fieldValue } = this.meta; - - return (fieldValue as MultiData)?.values as BaseChartData; - } - - public getRenderChartOptions() { - const chartData = this.getRenderChartData(); - const cellArea = this.getBBoxByType(CellClipBox.CONTENT_BOX); - const themeName = this.spreadsheet.getThemeName(); - - return { - autoFit: true, - theme: { type: G2_THEME_TYPE[themeName] }, - ...cellArea, - ...chartData, - }; - } - protected getBorderPositions(): CellBorderPosition[] { return [CellBorderPosition.BOTTOM, CellBorderPosition.RIGHT]; } diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index 442175309f..553d543048 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -442,6 +442,15 @@ export abstract class HeaderCell< return this.leftIconPosition || this.rightIconPosition; } + protected getInteractedCells() { + return this.spreadsheet.interaction?.getCells([ + CellType.CORNER_CELL, + CellType.COL_CELL, + CellType.ROW_CELL, + CellType.SERIES_NUMBER_CELL, + ]); + } + public update() { const { interaction } = this.spreadsheet; const stateInfo = interaction?.getState(); @@ -452,12 +461,7 @@ export abstract class HeaderCell< return; } - const cells = interaction?.getCells([ - CellType.CORNER_CELL, - CellType.COL_CELL, - CellType.ROW_CELL, - CellType.SERIES_NUMBER_CELL, - ]); + const cells = this.getInteractedCells(); if (!first(cells)) { return; diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index 026824986b..14e67847c1 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -212,9 +212,17 @@ export class RowCell extends HeaderCell { return (!isLeaf && level === 0) || isTotals; } + protected getResizesArea() { + return getOrCreateResizeAreaGroupById( + this.spreadsheet, + KEY_GROUP_ROW_RESIZE_AREA, + ); + } + protected drawResizeAreaInLeaf() { if ( !this.meta.isLeaf || + this.meta.hideRowCellVerticalResize || !this.shouldDrawResizeAreaByType('rowCellVertical', this) ) { return; @@ -222,10 +230,7 @@ export class RowCell extends HeaderCell { const { x, y, width, height } = this.getBBoxByType(); const resizeStyle = this.getResizeAreaStyle(); - const resizeArea = getOrCreateResizeAreaGroupById( - this.spreadsheet, - KEY_GROUP_ROW_RESIZE_AREA, - ); + const resizeArea = this.getResizesArea(); if (!resizeArea) { return; diff --git a/packages/s2-core/src/cell/table-col-cell.ts b/packages/s2-core/src/cell/table-col-cell.ts index 584c568110..11601f5eab 100644 --- a/packages/s2-core/src/cell/table-col-cell.ts +++ b/packages/s2-core/src/cell/table-col-cell.ts @@ -1,16 +1,10 @@ import { find } from 'lodash'; import { ColCell } from '../cell/col-cell'; -import { - FrozenGroupArea, - HORIZONTAL_RESIZE_AREA_KEY_PRE, -} from '../common/constant'; +import { HORIZONTAL_RESIZE_AREA_KEY_PRE } from '../common/constant'; import type { FormatResult } from '../common/interface'; -import type { SimpleBBox } from '../engine'; -import type { FrozenFacet } from '../facet/frozen-facet'; import type { BaseHeaderConfig } from '../facet/header'; import { formattedFieldValue } from '../utils/cell/header-cell'; import { renderRect } from '../utils/g-renders'; -import { shouldAddResizeArea } from '../utils/interaction/resize'; import { getSortTypeIcon } from '../utils/sort-action'; export class TableColCell extends ColCell { @@ -35,82 +29,6 @@ export class TableColCell extends ColCell { ); } - protected shouldAddVerticalResizeArea() { - if (this.getMeta().isFrozen) { - return true; - } - - const { - scrollX, - scrollY, - width: headerWidth, - height: headerHeight, - spreadsheet, - } = this.getHeaderConfig(); - const { x, y, width, height } = this.getBBoxByType(); - const resizeStyle = this.getResizeAreaStyle(); - - const resizeAreaBBox: SimpleBBox = { - x: x + width - resizeStyle.size!, - y, - width: resizeStyle.size!, - height, - }; - - const frozenGroupAreas = (spreadsheet.facet as FrozenFacet) - .frozenGroupAreas; - const colWidth = frozenGroupAreas[FrozenGroupArea.Col].width; - const trailingColWidth = - frozenGroupAreas[FrozenGroupArea.TrailingCol].width; - - const resizeClipAreaBBox: SimpleBBox = { - x: colWidth, - y: 0, - width: headerWidth - colWidth - trailingColWidth, - height: headerHeight, - }; - - return shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { - scrollX, - scrollY, - }); - } - - protected getVerticalResizeAreaOffset() { - const { x, y } = this.meta; - const { - scrollX = 0, - position, - spreadsheet, - viewportWidth, - } = this.getHeaderConfig(); - - const frozenGroupAreas = (spreadsheet.facet as FrozenFacet) - .frozenGroupAreas; - - const frozenColGroup = frozenGroupAreas[FrozenGroupArea.Col]; - const frozenTrailingColGroup = - frozenGroupAreas[FrozenGroupArea.TrailingCol]; - let offsetX = position?.x; - - if (this.getMeta().isFrozenHead) { - offsetX += x - frozenColGroup.x; - } else if (this.getMeta().isFrozenTrailing) { - offsetX += - x - - frozenTrailingColGroup.x + - viewportWidth - - frozenTrailingColGroup.width; - } else { - offsetX += x - scrollX; - } - - return { - x: offsetX, - y: position?.y + y, - }; - } - protected isSortCell() { return true; } diff --git a/packages/s2-core/src/common/constant/basic.ts b/packages/s2-core/src/common/constant/basic.ts index 39ba5bbadc..655ea65ee6 100644 --- a/packages/s2-core/src/common/constant/basic.ts +++ b/packages/s2-core/src/common/constant/basic.ts @@ -41,9 +41,8 @@ export const KEY_GROUP_ROW_INDEX_RESIZE_AREA = 'rowIndexResizeAreaGroup'; * row */ export const KEY_GROUP_ROW_SCROLL = 'rowScrollGroup'; -export const KEY_GROUP_ROW_HEADER_FROZEN = 'rowHeaderFrozenGroup'; -export const KEY_GROUP_ROW_HEADER_FROZEN_TRAILING = - 'rowHeaderFrozenTrailingGroup'; +export const KEY_GROUP_ROW_FROZEN = 'rowHeaderFrozenGroup'; +export const KEY_GROUP_ROW_FROZEN_TRAILING = 'rowHeaderFrozenTrailingGroup'; export const KEY_GROUP_ROW_RESIZE_AREA = 'rowResizeAreaGroup'; /** diff --git a/packages/s2-core/src/common/constant/events/basic.ts b/packages/s2-core/src/common/constant/events/basic.ts index 9160b0a8b8..d6aa997d1f 100644 --- a/packages/s2-core/src/common/constant/events/basic.ts +++ b/packages/s2-core/src/common/constant/events/basic.ts @@ -17,6 +17,8 @@ export enum S2Event { // 内部用来通信的 event ROW_CELL_COLLAPSED__PRIVATE = 'row-cell:collapsed__private', ROW_CELL_ALL_COLLAPSED__PRIVATE = 'row-cell:all-collapsed__private', + DATA_CELL_HOVER_TRIGGERED_PRIVATE = 'data-cell:hover-trigger__private', + DATA_CELL_CLICK_TRIGGERED_PRIVATE = 'data-cell:click-trigger__private', /** ================ Col Cell ================ */ COL_CELL_HOVER = 'col-cell:hover', diff --git a/packages/s2-core/src/common/constant/events/origin.ts b/packages/s2-core/src/common/constant/events/origin.ts index 93a2856ecb..97386e02ad 100644 --- a/packages/s2-core/src/common/constant/events/origin.ts +++ b/packages/s2-core/src/common/constant/events/origin.ts @@ -12,7 +12,6 @@ export enum OriginEventType { KEY_UP = 'keyup', CLICK = 'click', HOVER = 'hover', - DOUBLE_CLICK = 'dblclick', CONTEXT_MENU = 'contextmenu', POINTER_DOWN = 'pointerdown', POINTER_MOVE = 'pointermove', diff --git a/packages/s2-core/src/common/constant/options.ts b/packages/s2-core/src/common/constant/options.ts index e4c4f85e31..d68512f8fd 100644 --- a/packages/s2-core/src/common/constant/options.ts +++ b/packages/s2-core/src/common/constant/options.ts @@ -43,14 +43,10 @@ export const DEFAULT_STYLE: S2Style = { rowCell: { ...DEFAULT_CELL_TEXT_WORD_WRAP_STYLE, showTreeLeafNodeAlignDot: false, - widthByField: null, - heightByField: null, }, colCell: { ...DEFAULT_CELL_TEXT_WORD_WRAP_STYLE, height: 30, - widthByField: null, - heightByField: null, }, dataCell: { ...DEFAULT_CELL_TEXT_WORD_WRAP_STYLE, diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 871e0c2ba2..a9f57d981f 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -117,7 +117,7 @@ export interface Fields extends BaseFields { /** * 自定义指标维度在行列头中的层级顺序 (即 `values` 的 顺序,从 `0` 开始 */ - customValueOrder?: number; + customValueOrder?: number | null; } export enum Aggregation { diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index ad1ca1a704..219ac0226d 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -102,6 +102,8 @@ export interface EmitterType { [S2Event.DATA_CELL_BRUSH_SELECTION]: (cells: (DataCell | CellMeta)[]) => void; [S2Event.DATA_CELL_SELECT_MOVE]: (metas: CellMeta[]) => void; [S2Event.DATA_CELL_RENDER]: (cell: DataCell) => void; + [S2Event.DATA_CELL_HOVER_TRIGGERED_PRIVATE]: (cell: DataCell) => void; + [S2Event.DATA_CELL_CLICK_TRIGGERED_PRIVATE]: (cell: DataCell) => void; [S2Event.DATA_CELL_SELECTED]: CellSelectedHandler; /** ================ Row Cell ================ */ diff --git a/packages/s2-core/src/common/interface/theme.ts b/packages/s2-core/src/common/interface/theme.ts index 7b4f1c252a..504326c0ad 100644 --- a/packages/s2-core/src/common/interface/theme.ts +++ b/packages/s2-core/src/common/interface/theme.ts @@ -358,12 +358,17 @@ export interface S2Theme extends CellThemes { export type ThemeName = keyof typeof PALETTE_MAP; +export type SimplePalette = Pick< + Palette, + 'basicColors' | 'semanticColors' | 'others' +>; + export interface ThemeCfg { /** 主题 */ theme?: S2Theme; /** 色板 */ - palette?: Pick; + palette?: SimplePalette; /** 主题名 */ name?: ThemeName; diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 93e3cbf707..ae29e5e356 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -292,6 +292,15 @@ export abstract class BaseDataSet { return [] as unknown as Indexes; } + /** + * 获取 field 的取值范围 + * ! 取值范围是以传入的 data 作为的值做计算 + * ! 没有考虑总计、小计的情况,如果存在总计、小计,但是 data 里面并没有传,这里计算出来的范围就会不准确 + * ! 表格会采用计算明细数据得出,而这些计算出来的总计、小计是和布局结构强相关 + * ! 而要根据 totals 配置来覆盖所有情况,场景非常复杂 + * @param field values 中的具体数值字段 + * @returns 对应 field 的取值范围 + */ public getValueRangeByField(field: string): ValueRange { const cacheRange = getValueRangeState(this.spreadsheet, field); diff --git a/packages/s2-core/src/data-set/pivot-data-set.ts b/packages/s2-core/src/data-set/pivot-data-set.ts index 5907f76b5f..6039cbb8da 100644 --- a/packages/s2-core/src/data-set/pivot-data-set.ts +++ b/packages/s2-core/src/data-set/pivot-data-set.ts @@ -670,7 +670,7 @@ export class PivotDataSet extends BaseDataSet { } // 是否开启自定义度量组位置值 - private isCustomMeasuresPosition(customValueOrder?: number) { + private isCustomMeasuresPosition(customValueOrder?: number | null) { return isNumber(customValueOrder); } diff --git a/packages/s2-core/src/extends/index.ts b/packages/s2-core/src/extends/index.ts new file mode 100644 index 0000000000..0b999cd768 --- /dev/null +++ b/packages/s2-core/src/extends/index.ts @@ -0,0 +1 @@ +export * from './pivot-chart'; diff --git a/packages/s2-core/src/extends/pivot-chart/cell/axis-col-cell.ts b/packages/s2-core/src/extends/pivot-chart/cell/axis-col-cell.ts new file mode 100644 index 0000000000..c46a74be3d --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/axis-col-cell.ts @@ -0,0 +1,127 @@ +import { Group } from '@antv/g'; +import { corelib, renderToMountedElement, type AxisComponent } from '@antv/g2'; +import { + CellBorderPosition, + CellClipBox, + CellType, + ColCell, + customMerge, + getOrCreateResizeAreaGroupById, + waitForCellMounted, +} from '@antv/s2'; +import { isFunction } from 'lodash'; +import { DEFAULT_G2_SPEC, KEY_GROUP_COL_AXIS_RESIZE_AREA } from '../constant'; +import type { PivotChartSheet } from '../pivot-chart-sheet'; +import { + getAxisStyle, + getAxisXOptions, + getAxisYOptions, + getCoordinate, + getTheme, +} from '../utils/chart-options'; +import { AxisCellType } from './cell-type'; + +export class AxisColCell extends ColCell { + protected declare spreadsheet: PivotChartSheet; + + protected axisShape: Group; + + public get cellType() { + return AxisCellType.AXIS_COL_CELL as any; + } + + protected getBorderPositions(): CellBorderPosition[] { + return [ + CellBorderPosition.TOP, + CellBorderPosition.BOTTOM, + CellBorderPosition.RIGHT, + ]; + } + + protected isBolderText(): boolean { + return false; + } + + protected getInteractedCells() { + return this.spreadsheet.interaction?.getCells([ + CellType.COL_CELL, + AxisCellType.AXIS_COL_CELL as any, + ]); + } + + protected initCell(): void { + this.drawBackgroundShape(); + this.drawInteractiveBgShape(); + this.drawInteractiveBorderShape(); + this.drawTextShape(); + this.drawBorders(); + this.drawResizeArea(); + this.update(); + } + + protected getColResizeArea() { + return getOrCreateResizeAreaGroupById( + this.spreadsheet, + KEY_GROUP_COL_AXIS_RESIZE_AREA, + ); + } + + protected isCrossColumnLeafNode() { + return false; + } + + public drawTextShape(): void { + if (this.spreadsheet.isPolarCoordinate()) { + super.drawTextShape(); + + return; + } + + this.drawAxisShape(); + } + + getChartOptions(): AxisComponent { + const style = this.getStyle(); + + let customSpec = this.spreadsheet.options.chart?.axisColCellSpec; + + if (isFunction(customSpec)) { + customSpec = customSpec(this); + } + + return customMerge( + { + ...DEFAULT_G2_SPEC, + ...this.getBBoxByType(CellClipBox.CONTENT_BOX), + + ...getCoordinate(this.spreadsheet), + ...(this.spreadsheet.isValueInCols() + ? getAxisYOptions(this.meta, this.spreadsheet) + : getAxisXOptions(this.meta, this.spreadsheet)), + + ...getAxisStyle(style), + ...getTheme(this.spreadsheet), + } as AxisComponent, + customSpec, + ); + } + + drawAxisShape() { + const chartOptions = this.getChartOptions(); + + this.axisShape = this.appendChild(new Group({})); + + // delay 到实例被挂载到 parent 后,再渲染 chart + waitForCellMounted(() => { + if (this.destroyed) { + return; + } + + // https://g2.antv.antgroup.com/manual/extra-topics/bundle#g2corelib + renderToMountedElement(chartOptions, { + group: this.axisShape, + library: corelib(), + }); + }); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/cell/axis-corner-cell.ts b/packages/s2-core/src/extends/pivot-chart/cell/axis-corner-cell.ts new file mode 100644 index 0000000000..5f8528172a --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/axis-corner-cell.ts @@ -0,0 +1,43 @@ +import { CellBorderPosition, CellClipBox, CornerCell } from '@antv/s2'; +import { AxisCellType } from './cell-type'; + +export class AxisCornerCell extends CornerCell { + public get cellType() { + return AxisCellType.AXIS_CORNER_CELL as any; + } + + protected getBorderPositions(): CellBorderPosition[] { + return [ + CellBorderPosition.TOP, + CellBorderPosition.BOTTOM, + CellBorderPosition.LEFT, + ]; + } + + protected isBolderText(): boolean { + return false; + } + + public getMaxTextWidth(): number { + const { width } = this.getBBoxByType(CellClipBox.CONTENT_BOX); + + return width; + } + + protected getTreeIconWidth() { + return 0; + } + + protected getInteractedCells() { + return this.spreadsheet.interaction?.getCells([ + AxisCellType.AXIS_CORNER_CELL as any, + ]); + } + + protected initCell(): void { + this.drawBackgroundShape(); + this.drawTextShape(); + this.drawBorders(); + this.update(); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/cell/axis-row-cell.ts b/packages/s2-core/src/extends/pivot-chart/cell/axis-row-cell.ts new file mode 100644 index 0000000000..aa8f92cb7f --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/axis-row-cell.ts @@ -0,0 +1,119 @@ +import { Group } from '@antv/g'; +import { corelib, renderToMountedElement, type AxisComponent } from '@antv/g2'; +import { + CellBorderPosition, + CellClipBox, + CellType, + RowCell, + customMerge, + getOrCreateResizeAreaGroupById, + waitForCellMounted, +} from '@antv/s2'; +import { isFunction } from 'lodash'; +import { DEFAULT_G2_SPEC, KEY_GROUP_ROW_AXIS_RESIZE_AREA } from '../constant'; +import type { PivotChartSheet } from '../pivot-chart-sheet'; +import { + getAxisStyle, + getAxisXOptions, + getAxisYOptions, + getCoordinate, + getTheme, +} from '../utils/chart-options'; +import { AxisCellType } from './cell-type'; + +export class AxisRowCell extends RowCell { + protected declare spreadsheet: PivotChartSheet; + + protected axisShape: Group; + + public get cellType() { + return AxisCellType.AXIS_ROW_CELL as any; + } + + protected getBorderPositions(): CellBorderPosition[] { + return [CellBorderPosition.BOTTOM, CellBorderPosition.LEFT]; + } + + protected isBolderText(): boolean { + return false; + } + + protected getInteractedCells() { + return this.spreadsheet.interaction?.getCells([ + CellType.ROW_CELL, + AxisCellType.AXIS_ROW_CELL as any, + ]); + } + + protected initCell(): void { + this.drawBackgroundShape(); + this.drawInteractiveBgShape(); + this.drawInteractiveBorderShape(); + this.drawTextShape(); + this.drawBorders(); + this.drawResizeAreaInLeaf(); + this.update(); + } + + protected getResizesArea() { + return getOrCreateResizeAreaGroupById( + this.spreadsheet, + KEY_GROUP_ROW_AXIS_RESIZE_AREA, + ); + } + + public drawTextShape(): void { + if (this.spreadsheet.isPolarCoordinate()) { + super.drawTextShape(); + + return; + } + + this.drawAxisShape(); + } + + getChartOptions(): AxisComponent { + const style = this.getStyle(); + + let customSpec = this.spreadsheet.options.chart?.axisRowCellSpec; + + if (isFunction(customSpec)) { + customSpec = customSpec(this); + } + + return customMerge( + { + ...DEFAULT_G2_SPEC, + ...this.getBBoxByType(CellClipBox.CONTENT_BOX), + + ...getCoordinate(this.spreadsheet), + ...(this.spreadsheet.isValueInCols() + ? getAxisXOptions(this.meta, this.spreadsheet) + : getAxisYOptions(this.meta, this.spreadsheet)), + + ...getAxisStyle(style), + ...getTheme(this.spreadsheet), + } as AxisComponent, + customSpec, + ); + } + + drawAxisShape() { + const chartOptions = this.getChartOptions(); + + this.axisShape = this.appendChild(new Group({})); + + // delay 到实例被挂载到 parent 后,再渲染 chart + waitForCellMounted(() => { + if (this.destroyed) { + return; + } + + // https://g2.antv.antgroup.com/manual/extra-topics/bundle#g2corelib + renderToMountedElement(chartOptions, { + group: this.axisShape, + library: corelib(), + }); + }); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/cell/cell-type.ts b/packages/s2-core/src/extends/pivot-chart/cell/cell-type.ts new file mode 100644 index 0000000000..72bcd1c283 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/cell-type.ts @@ -0,0 +1,5 @@ +export enum AxisCellType { + AXIS_ROW_CELL = 'axisRowCell', + AXIS_COL_CELL = 'axisColCell', + AXIS_CORNER_CELL = 'axisCornerCell', +} diff --git a/packages/s2-core/src/extends/pivot-chart/cell/chart-data-cell.ts b/packages/s2-core/src/extends/pivot-chart/cell/chart-data-cell.ts new file mode 100644 index 0000000000..b43c3648b9 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/chart-data-cell.ts @@ -0,0 +1,66 @@ +import { Group } from '@antv/g-lite'; +import { corelib, renderToMountedElement } from '@antv/g2'; +import { + CellClipBox, + DataCell, + waitForCellMounted, + type BaseChartData, + type MultiData, +} from '@antv/s2'; +import { isPlainObject } from 'lodash'; +import { getTheme } from '../utils/chart-options'; + +export class ChartDataCell extends DataCell { + chartShape: Group; + + public drawTextShape() { + // 普通数值单元格正常展示 + if (!this.isChartData()) { + super.drawTextShape(); + + return; + } + + this.chartShape = this.appendChild(new Group({ style: { zIndex: 1 } })); + + const chartOptions = this.getChartOptions(); + + waitForCellMounted(() => { + if (this.destroyed) { + return; + } + + // https://g2.antv.antgroup.com/manual/extra-topics/bundle#g2corelib + renderToMountedElement(chartOptions, { + group: this.chartShape, + library: corelib(), + }); + }); + } + + public isChartData() { + const fieldValue = this.getFieldValue(); + + return isPlainObject( + (fieldValue as unknown as MultiData)?.values, + ); + } + + public getChartData(): BaseChartData { + const { fieldValue } = this.meta; + + return (fieldValue as MultiData)?.values as BaseChartData; + } + + public getChartOptions() { + const chartData = this.getChartData(); + const cellArea = this.getBBoxByType(CellClipBox.CONTENT_BOX); + + return { + autoFit: true, + ...getTheme(this.spreadsheet), + ...cellArea, + ...chartData, + }; + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/cell/pivot-chart-data-cell.ts b/packages/s2-core/src/extends/pivot-chart/cell/pivot-chart-data-cell.ts new file mode 100644 index 0000000000..af1b49bac6 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/cell/pivot-chart-data-cell.ts @@ -0,0 +1,102 @@ +import { Group } from '@antv/g'; +import { corelib, renderToMountedElement, type G2Spec } from '@antv/g2'; +import { + CellClipBox, + customMerge, + waitForCellMounted, + type CellMeta, +} from '@antv/s2'; +import { isFunction } from 'lodash'; +import { DEFAULT_CHART_SPEC } from '../constant'; +import type { PivotChartSheet } from '../pivot-chart-sheet'; +import { + getCoordinate, + getScaleY, + getTheme, + getTooltip, +} from '../utils/chart-options'; +import { AxisCellType } from './cell-type'; +import { ChartDataCell } from './chart-data-cell'; + +export class PivotChartDataCell extends ChartDataCell { + public isChartData(): boolean { + return true; + } + + public getChartData(): any { + const { data, xField, yField } = this.meta; + + return { + data, + encode: { + x: (this.spreadsheet as PivotChartSheet).isPolarCoordinate() + ? null + : xField, + y: yField, + color: xField, + }, + }; + } + + public getChartOptions(): any { + const { yField } = this.meta; + + let customSpec = this.spreadsheet.options.chart?.dataCellSpec; + + if (isFunction(customSpec)) { + customSpec = customSpec(this); + } + + return customMerge( + { + ...DEFAULT_CHART_SPEC, + ...this.getBBoxByType(CellClipBox.CONTENT_BOX), + ...getCoordinate(this.spreadsheet), + ...this.getChartData(), + ...getScaleY(yField!, this.spreadsheet), + + ...getTooltip(this.meta, this.spreadsheet), + ...getTheme(this.spreadsheet), + } as G2Spec, + customSpec, + ); + } + + public drawTextShape(): void { + const chartOptions = this.getChartOptions(); + + this.chartShape = this.appendChild(new Group({ style: { zIndex: 1 } })); + + waitForCellMounted(() => { + if (this.destroyed) { + return; + } + + // https://g2.antv.antgroup.com/manual/extra-topics/bundle#g2corelib + renderToMountedElement(chartOptions, { + group: this.chartShape, + library: corelib(), + }); + }); + } + + protected handleSelect(cells: CellMeta[]) { + super.handleSelect(cells); + + const currentCellType = cells?.[0]?.type as unknown as AxisCellType; + + switch (currentCellType) { + // 列多选 + case AxisCellType.AXIS_COL_CELL: + this.changeRowColSelectState('colIndex'); + break; + // 行多选 + case AxisCellType.AXIS_ROW_CELL: + this.changeRowColSelectState('rowIndex'); + break; + + default: + break; + } + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/constant.ts b/packages/s2-core/src/extends/pivot-chart/constant.ts new file mode 100644 index 0000000000..6fd671ce6d --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/constant.ts @@ -0,0 +1,84 @@ +import type { G2Spec } from '@antv/g2'; +import { type S2DataConfig, type S2Options } from '@antv/s2'; +import { AxisRowColumnClick } from './interaction/axis-click'; +import { AxisHover } from './interaction/axis-hover'; + +export const DEFAULT_G2_SPEC = { + autoFit: true, + animate: false, + // https://g2.antv.antgroup.com/manual/core/size + margin: 1, +}; + +export const DEFAULT_CHART_SPEC: G2Spec = { + ...DEFAULT_G2_SPEC, + type: 'interval', + axis: false, + legend: false, +}; + +export const FIXED_OPTIONS: S2Options = { + hierarchyType: 'grid', + interaction: { + selectedCellsSpotlight: false, + copy: { + enable: false, + }, + }, + style: { + colCell: { + hideValue: false, + }, + }, +}; + +export const DEFAULT_OPTIONS: S2Options = { + chart: { + coordinate: 'cartesian', + }, + + interaction: { + customInteractions: [ + { + key: 'axisHover', + interaction: AxisHover, + }, + { + key: 'axisClick', + interaction: AxisRowColumnClick, + }, + ], + }, +}; + +export const FIXED_DATA_CONFIG: Partial = { + fields: { + customValueOrder: null, + }, +}; + +export const DEFAULT_MEASURE_SIZE = 200; +export const DEFAULT_ROW_AXIS_SIZE = 100; +export const DEFAULT_COL_AXIS_SIZE = 50; +export const DEFAULT_DIMENSION_SIZE = 50; + +/** + * row axis + */ +export const KEY_GROUP_ROW_AXIS_SCROLL = 'rowAxisScrollGroup'; +export const KEY_GROUP_ROW_AXIS_FROZEN = 'rowAxisHeaderFrozenGroup'; +export const KEY_GROUP_ROW_AXIS_HEADER_FROZEN_TRAILING = + 'rowAxisHeaderFrozenTrailingGroup'; +export const KEY_GROUP_ROW_AXIS_RESIZE_AREA = 'rowAxisHeaderResizeArea'; + +/** + * column axis + */ +export const KEY_GROUP_COL_AXIS_SCROLL = 'colAxisScrollGroup'; +export const KEY_GROUP_COL_AXIS_FROZEN = 'colAxisFrozenGroup'; +export const KEY_GROUP_COL_AXIS_FROZEN_TRAILING = 'colAxisFrozenTrailingGroup'; +export const KEY_GROUP_COL_AXIS_RESIZE_AREA = 'colAxisHeaderResizeArea'; + +export const PLACEHOLDER_FIELD = '$$placeholder$$'; + +export const X_FIELD_FORMATTER = '$$should_formatter$$'; diff --git a/packages/s2-core/src/extends/pivot-chart/facet/corner-bbox.ts b/packages/s2-core/src/extends/pivot-chart/facet/corner-bbox.ts new file mode 100644 index 0000000000..ec96e76cbe --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/facet/corner-bbox.ts @@ -0,0 +1,13 @@ +import { CornerBBox as OriginCornerBBox, floor } from '@antv/s2'; + +export class CornerBBox extends OriginCornerBBox { + protected calculateOriginWidth(): void { + const { rowsHierarchy, axisRowsHierarchy } = this.layoutResult; + + const rowAxisWidth = axisRowsHierarchy?.width ?? 0; + + this.originalWidth = floor( + this.facet.getSeriesNumberWidth() + rowsHierarchy.width + rowAxisWidth, + ); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/facet/frame.ts b/packages/s2-core/src/extends/pivot-chart/facet/frame.ts new file mode 100644 index 0000000000..a0a515779c --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/facet/frame.ts @@ -0,0 +1,79 @@ +import { Frame as OriginFrame, renderLine } from '@antv/s2'; + +export class Frame extends OriginFrame { + protected override getCornerRightBorderSizeForPivotMode() { + const { cornerHeight, viewportHeight, position, spreadsheet } = this.cfg; + + const { horizontalBorderWidth } = spreadsheet.theme?.splitLine!; + + const y = position.y; + const axisColsHierarchy = + spreadsheet.facet.getLayoutResult().axisColsHierarchy; + + const height = + cornerHeight + + horizontalBorderWidth! + + viewportHeight + + (axisColsHierarchy?.height ?? 0); + + return { y, height }; + } + + protected addCornerRightBottomHeaderBorder() { + // 为底部坐标轴执行一样的逻辑绘制分割线 + const axisColsHierarchy = + this.cfg.spreadsheet.facet.getLayoutResult().axisColsHierarchy; + + if (!axisColsHierarchy?.height) { + return; + } + + const { cornerWidth, cornerHeight, viewportHeight, position, spreadsheet } = + this.cfg; + const { verticalBorderColor, verticalBorderColorOpacity } = + spreadsheet.theme?.splitLine!; + const frameVerticalWidth = Frame.getVerticalBorderWidth(spreadsheet); + const frameHorizontalWidth = Frame.getVerticalBorderWidth(spreadsheet); + const x = position.x + cornerWidth + frameVerticalWidth! / 2; + + // 表头和表身的单元格背景色不同, 分割线不能一条线拉通, 不然视觉不协调. + // 分两条线绘制, 默认和分割线所在区域对应的单元格边框颜色保持一致 + const { + verticalBorderColor: headerVerticalBorderColor, + verticalBorderColorOpacity: headerVerticalBorderColorOpacity, + backgroundColor, + backgroundColorOpacity, + } = spreadsheet.theme.cornerCell!.cell!; + + const y1 = + position.y + cornerHeight + frameHorizontalWidth + viewportHeight; + + /** + * G 6.0 颜色混合模式有调整, 相同颜色的 Line 在不同背景色绘制, 实际渲染的颜色会不一致 + * 在绘制分割线前, 先填充一个和单元格相同的底色, 保证分割线和单元格边框表现一致 + */ + [ + { stroke: backgroundColor, strokeOpacity: backgroundColorOpacity }, + { + stroke: verticalBorderColor || headerVerticalBorderColor, + strokeOpacity: + verticalBorderColorOpacity || headerVerticalBorderColorOpacity, + }, + ].forEach(({ stroke, strokeOpacity }) => { + renderLine(this, { + x1: x, + y1, + x2: x, + y2: y1 + axisColsHierarchy.height, + lineWidth: frameVerticalWidth, + stroke, + strokeOpacity, + }); + }); + } + + protected addCornerRightBorder() { + super.addCornerRightBorder(); + this.addCornerRightBottomHeaderBorder(); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/facet/panel-bbox.ts b/packages/s2-core/src/extends/pivot-chart/facet/panel-bbox.ts new file mode 100644 index 0000000000..197e59e7af --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/facet/panel-bbox.ts @@ -0,0 +1,18 @@ +import { PanelBBox as OriginPanelBBox } from '@antv/s2'; + +export class PanelBBox extends OriginPanelBBox { + protected override getPanelHeight(): number { + const scrollBarSize = this.spreadsheet.theme.scrollBar!.size; + const { height: canvasHeight } = this.spreadsheet.options; + + const { axisColsHierarchy } = this.layoutResult; + const colAxisHeight = axisColsHierarchy?.height ?? 0; + + const panelHeight = Math.max( + 0, + canvasHeight! - this.y - scrollBarSize! - colAxisHeight, + ); + + return panelHeight; + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/facet/pivot-chart-facet.ts b/packages/s2-core/src/extends/pivot-chart/facet/pivot-chart-facet.ts new file mode 100644 index 0000000000..a8d0e6e492 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/facet/pivot-chart-facet.ts @@ -0,0 +1,678 @@ +import { + CellData, + EXTRA_FIELD, + Node, + ORIGIN_FIELD, + PivotFacet, + ScrollType, + getAllChildCells, + getCellWidth, + getDataCellId, + getHeaderTotalStatus, + type FrameConfig, + type LayoutResult, + type S2CellType, + type ScrollChangeParams, + type SelectedIds, + type ViewMeta, +} from '@antv/s2'; +import { + concat, + floor, + get, + isEmpty, + isNumber, + last, + merge, + sum, +} from 'lodash'; +import { + KEY_GROUP_COL_AXIS_RESIZE_AREA, + KEY_GROUP_ROW_AXIS_RESIZE_AREA, + X_FIELD_FORMATTER, +} from '../constant'; +import { AxisColHeader } from '../header/axis-col'; +import { AxisCornerHeader } from '../header/axis-corner'; +import { AxisRowHeader } from '../header/axis-row'; +import { CornerHeader } from '../header/corner'; + +import { AxisColCell } from '../cell/axis-col-cell'; +import { AxisCornerCell } from '../cell/axis-corner-cell'; +import { AxisRowCell } from '../cell/axis-row-cell'; +import { AxisCellType } from '../cell/cell-type'; +import type { PivotChartSheet } from '../pivot-chart-sheet'; +import { separateRowColLeafNodes } from '../utils/separate-axis'; +import { CornerBBox } from './corner-bbox'; +import { Frame } from './frame'; +import { PanelBBox } from './panel-bbox'; + +export class PivotChartFacet extends PivotFacet { + declare spreadsheet: PivotChartSheet; + + axisRowHeader: AxisRowHeader | null; + + axisColumnHeader: AxisColHeader | null; + + axisCornerHeader: AxisCornerHeader | null; + + protected override doLayout(): LayoutResult { + let layoutResult = this.buildAllHeaderHierarchy() as LayoutResult; + + layoutResult = separateRowColLeafNodes(layoutResult, this.spreadsheet); + + this.calculateHeaderNodesCoordinate(layoutResult); + + this.calculateAxisHierarchyCoordinate(layoutResult); + + const { + rowsHierarchy, + colsHierarchy, + axisRowsHierarchy, + axisColsHierarchy, + } = layoutResult; + + return { + axisRowsHierarchy, + axisColsHierarchy, + rowsHierarchy, + rowNodes: rowsHierarchy.getNodes(), + rowLeafNodes: rowsHierarchy.getLeaves(), + colsHierarchy, + colNodes: colsHierarchy.getNodes(), + colLeafNodes: colsHierarchy.getLeaves(), + }; + } + + protected getColLeafNodeRelatedCount(colNode: Node) { + const isValueInCols = this.spreadsheet.isValueInCols(); + const isPolar = this.spreadsheet.isPolarCoordinate(); + + const size = + !isValueInCols && !isPolar + ? get(colNode.relatedNode, 'children', []).length + : 1; + + return size; + } + + protected getRowLeafNodeRelatedCount(rowNode: Node) { + const isValueInCols = this.spreadsheet.isValueInCols(); + const isPolar = this.spreadsheet.isPolarCoordinate(); + + const size = + isValueInCols && !isPolar + ? get(rowNode.relatedNode, 'children', []).length + : 1; + + return size; + } + + protected getRowAxisWidth() { + const { rowCell } = this.spreadsheet.options.style!; + + const { rows = [] } = this.spreadsheet.dataSet.fields; + const lastRow = last(rows) as string; + + return rowCell?.widthByField?.[lastRow] ?? 0; + } + + protected getColAxisHeight() { + const { colCell } = this.spreadsheet.options.style!; + + const { columns = [] } = this.spreadsheet.dataSet.fields; + const lastCol = last(columns) as string; + + return colCell?.heightByField?.[lastCol] ?? 0; + } + + protected override getCompactGridColNodeWidth(colNode: Node) { + const { dataCell } = this.spreadsheet.options.style!; + const dataCellWidth = getCellWidth( + dataCell!, + this.getColLeafNodeRelatedCount(colNode), + ); + + return dataCellWidth; + } + + protected override getAdaptGridColWidth( + colLeafNodes: Node[], + colNode?: Node, + rowHeaderWidth?: number, + ) { + const { rows = [] } = this.spreadsheet.dataSet.fields; + const { dataCell } = this.spreadsheet.options.style!; + + const rowHeaderColSize = Math.max(0, rows.length - 1); + + const colHeaderColSize = sum( + colLeafNodes.map((node) => this.getColLeafNodeRelatedCount(node)), + ); + const { width } = this.getCanvasSize(); + const availableWidth = + width - + this.getSeriesNumberWidth() - + this.getRowAxisWidth() - + Frame.getVerticalBorderWidth(this.spreadsheet); + + const colSize = Math.max(1, rowHeaderColSize + colHeaderColSize); + + const currentSize = colNode ? this.getColLeafNodeRelatedCount(colNode) : 1; + + if (!rowHeaderWidth) { + return ( + currentSize * + Math.max(getCellWidth(dataCell!), floor(availableWidth / colSize)) + ); + } + + return ( + currentSize * + Math.max( + getCellWidth(dataCell!), + floor((availableWidth - rowHeaderWidth) / colHeaderColSize), + ) + ); + } + + protected getRowLeafNodeHeight(rowLeafNode: Node) { + const customHeight = this.getCustomRowCellHeight(rowLeafNode); + + // 1. 拖拽后的宽度优先级最高 + if (isNumber(customHeight)) { + return customHeight; + } + + const { dataCell } = this.spreadsheet.options.style!; + const dataCellHeight = dataCell?.height ?? 0; + + return this.getRowLeafNodeRelatedCount(rowLeafNode) * dataCellHeight; + } + + protected calculateAxisHierarchyCoordinate(layoutResult: LayoutResult) { + this.adjustTotalNodesCoordinateAfterSeparateAxisHierarchy(layoutResult); + this.calculateAxisRowsHierarchyCoordinate(layoutResult); + this.calculateAxisColsHierarchyCoordinate(layoutResult); + } + + protected adjustTotalNodesCoordinateAfterSeparateAxisHierarchy( + layoutResult: LayoutResult, + ) { + // 最后一个维度分离出去后,再存在总计、小计分组时,会存在总计、小计格子出现空缺,因为 pivot-facet 层是按照未拆分的逻辑做的补全。 + // 拆分后需要再处理一下,而且只需要针对维度拆分的部分做处理即可,指标拆分正常显示 + + const { rowsHierarchy, colsHierarchy } = layoutResult; + + if ( + !isEmpty(this.spreadsheet.options.totals?.row) && + this.spreadsheet.isValueInCols() + ) { + const sampleNodeForLastLevel = rowsHierarchy.sampleNodeForLastLevel!; + const maxX = sampleNodeForLastLevel.x + sampleNodeForLastLevel.width; + + rowsHierarchy.getLeaves().forEach((leaf) => { + const rightX = leaf.x + leaf.width; + + if (maxX > rightX) { + leaf.width += maxX - rightX; + } + }); + } + + if ( + !isEmpty(this.spreadsheet.options.totals?.col) && + !this.spreadsheet.isValueInCols() + ) { + const sampleNodeForLastLevel = colsHierarchy.sampleNodeForLastLevel!; + const maxY = sampleNodeForLastLevel.y + sampleNodeForLastLevel.height; + + colsHierarchy.getLeaves().forEach((leaf) => { + const bottomY = leaf.y + leaf.height; + + if (maxY > bottomY) { + leaf.height += maxY - bottomY; + } + }); + } + } + + protected calculateAxisRowsHierarchyCoordinate(layoutResult: LayoutResult) { + const { rowsHierarchy, axisRowsHierarchy } = layoutResult; + + if (!axisRowsHierarchy) { + return; + } + + const isValueInCols = this.spreadsheet.isValueInCols(); + const isPolar = this.spreadsheet.isPolarCoordinate(); + + rowsHierarchy.width = + rowsHierarchy.isPlaceholder && isValueInCols && !isPolar + ? 0 + : rowsHierarchy.width; + + const rowAxisWidth = this.getRowAxisWidth(); + + rowsHierarchy.getLeaves().forEach((leaf) => { + const relatedNode = leaf.relatedNode; + + if (!relatedNode) { + return; + } + + relatedNode.y = leaf.y; + relatedNode.width = rowAxisWidth; + relatedNode.height = leaf.height; + }); + + if (isValueInCols && isPolar) { + axisRowsHierarchy.width = 0; + axisRowsHierarchy.getNodes().forEach((node) => { + node.width = 0; + }); + } else { + axisRowsHierarchy.width = rowAxisWidth; + } + + axisRowsHierarchy.height = rowsHierarchy.height; + } + + protected calculateAxisColsHierarchyCoordinate(layoutResult: LayoutResult) { + const { colsHierarchy, axisColsHierarchy } = layoutResult; + + if (!axisColsHierarchy) { + return; + } + + const isValueInCols = this.spreadsheet.isValueInCols(); + const isPolar = this.spreadsheet.isPolarCoordinate(); + + const colAxisHeight = this.getColAxisHeight(); + + colsHierarchy.getLeaves().forEach((leaf) => { + const relatedNode = leaf.relatedNode; + + if (!relatedNode) { + return; + } + + relatedNode.x = leaf.x; + relatedNode.width = leaf.width; + relatedNode.height = colAxisHeight; + }); + + axisColsHierarchy.width = colsHierarchy.width; + + if (!isValueInCols && isPolar) { + axisColsHierarchy.height = 0; + axisColsHierarchy.getNodes().forEach((node) => { + node.height = 0; + }); + } else { + axisColsHierarchy.height = colAxisHeight; + } + } + + protected override calculateCornerBBox(): void { + this.cornerBBox = new CornerBBox(this, true); + } + + protected override calculatePanelBBox() { + this.panelBBox = new PanelBBox(this, true); + } + + protected override getCenterFrame() { + if (!this.centerFrame) { + const { viewportWidth, viewportHeight } = this.panelBBox; + const cornerWidth = this.cornerBBox.width; + const cornerHeight = this.cornerBBox.height; + const frame = this.spreadsheet.options?.frame; + const frameCfg: FrameConfig = { + position: { + x: this.cornerBBox.x, + y: this.cornerBBox.y, + }, + cornerWidth, + cornerHeight, + viewportWidth, + viewportHeight, + showViewportLeftShadow: false, + showViewportRightShadow: false, + spreadsheet: this.spreadsheet, + }; + + return frame ? frame(frameCfg) : new Frame(frameCfg); + } + + return this.centerFrame; + } + + protected override renderHeaders(): void { + super.renderHeaders(); + this.axisRowHeader = this.getAxisRowHeader(); + + if (this.axisRowHeader) { + this.foregroundGroup.appendChild(this.axisRowHeader); + } + + this.axisColumnHeader = this.getAxisColHeader(); + if (this.axisColumnHeader) { + this.foregroundGroup.appendChild(this.axisColumnHeader); + } + + this.axisCornerHeader = this.getAxisCornerHeader(); + if (this.axisCornerHeader) { + this.foregroundGroup.appendChild(this.axisCornerHeader); + } + } + + protected override getCornerHeader(): CornerHeader { + return ( + this.cornerHeader || + CornerHeader.getCornerHeader({ + panelBBox: this.panelBBox, + cornerBBox: this.cornerBBox, + seriesNumberWidth: this.getSeriesNumberWidth(), + layoutResult: this.layoutResult, + spreadsheet: this.spreadsheet, + }) + ); + } + + protected getAxisRowHeader(): AxisRowHeader | null { + if (this.axisRowHeader) { + return this.axisRowHeader; + } + + const { y, viewportHeight, viewportWidth, height } = this.panelBBox; + const { rowsHierarchy, axisRowsHierarchy } = this.layoutResult; + const seriesNumberWidth = this.getSeriesNumberWidth(); + + return new AxisRowHeader({ + width: this.cornerBBox.width, + height, + viewportWidth, + viewportHeight, + position: { x: seriesNumberWidth + rowsHierarchy.width, y }, + nodes: axisRowsHierarchy?.getNodes() ?? [], + spreadsheet: this.spreadsheet, + }); + } + + protected getAxisColHeader(): AxisColHeader | null { + if (this.axisColumnHeader) { + return this.axisColumnHeader; + } + + const { x, width, viewportWidth, y, viewportHeight } = this.panelBBox; + const { axisColsHierarchy } = this.layoutResult; + + return new AxisColHeader({ + width, + cornerWidth: this.cornerBBox.width, + height: axisColsHierarchy?.height ?? 0, + viewportWidth, + viewportHeight, + position: { x, y: y + viewportHeight }, + nodes: axisColsHierarchy?.getNodes() ?? [], + spreadsheet: this.spreadsheet, + }); + } + + protected getAxisCornerHeader(): AxisCornerHeader | null { + return ( + this.axisCornerHeader || + AxisCornerHeader.getCornerHeader({ + panelBBox: this.panelBBox, + cornerBBox: this.cornerBBox, + seriesNumberWidth: this.getSeriesNumberWidth(), + layoutResult: this.layoutResult, + spreadsheet: this.spreadsheet, + }) + ); + } + + protected override translateRelatedGroups( + scrollX: number, + scrollY: number, + hRowScroll: number, + ): void { + super.translateRelatedGroups(scrollX, scrollY, hRowScroll); + + this.axisRowHeader?.onScrollXY( + this.getRealScrollX(scrollX, hRowScroll), + scrollY, + KEY_GROUP_ROW_AXIS_RESIZE_AREA, + ); + + this.axisColumnHeader?.onColScroll(scrollX, KEY_GROUP_COL_AXIS_RESIZE_AREA); + + this.axisCornerHeader?.onCorScroll( + this.getRealScrollX(scrollX, hRowScroll), + ); + } + + protected override renderRowScrollBar(rowHeaderScrollX: number) { + super.renderRowScrollBar(rowHeaderScrollX); + + if (this.hRowScrollBar) { + const maxOffset = this.cornerBBox.originalWidth - this.cornerBBox.width; + + this.hRowScrollBar.addEventListener( + ScrollType.ScrollChange, + ({ offset }: ScrollChangeParams) => { + const newOffset = this.getValidScrollBarOffset(offset, maxOffset); + const newRowHeaderScrollX = floor(newOffset); + + this.setScrollOffset({ rowHeaderScrollX: newRowHeaderScrollX }); + + this.axisRowHeader?.onRowScrollX( + newRowHeaderScrollX, + KEY_GROUP_ROW_AXIS_RESIZE_AREA, + ); + + this.axisCornerHeader?.onRowScrollX(newRowHeaderScrollX); + }, + ); + } + } + + /** + * 根据行列索引获取单元格元数据 + */ + public override getCellMeta(rowIndex = 0, colIndex = 0) { + const { options, dataSet } = this.spreadsheet; + const { axisRowsHierarchy, axisColsHierarchy } = this.getLayoutResult(); + + const rowAxisLeafNodes = axisRowsHierarchy?.getLeaves() ?? []; + const colAxisLeafNodes = axisColsHierarchy?.getLeaves() ?? []; + + const rowAxis = rowAxisLeafNodes[rowIndex]; + const colAxis = colAxisLeafNodes[colIndex]; + + if (!rowAxis || !colAxis) { + return null; + } + + const data: any = []; + + const xField = + rowAxis.field === EXTRA_FIELD ? colAxis.field : rowAxis.field; + const yField = + rowAxis.field === EXTRA_FIELD ? rowAxis.value : colAxis.value; + + for (const rowChild of rowAxis.children) { + for (const colChild of colAxis.children) { + const rowQuery = rowChild.query; + const colQuery = colChild.query; + + const isTotals = + rowChild.isTotals || + rowChild.isTotalMeasure || + colChild.isTotals || + colChild.isTotalMeasure; + + const totalStatus = getHeaderTotalStatus(rowChild, colChild); + + const dataQuery = merge({}, rowQuery, colQuery); + const current = dataSet.getCellData({ + query: dataQuery, + isTotals, + totalStatus, + }) as CellData; + + let xValue; + let xValueShouldFormatter = true; + + if (rowChild.field === EXTRA_FIELD) { + xValue = colChild.value; + xValueShouldFormatter = !colChild.isTotalRoot; + } else { + xValue = rowChild.value; + xValueShouldFormatter = !rowChild.isTotalRoot; + } + + const origin = { + [xField]: xValue, + [X_FIELD_FORMATTER]: xValueShouldFormatter, + ...current?.[ORIGIN_FIELD], + }; + + data.push(origin); + } + } + + const cellMeta: ViewMeta = { + spreadsheet: this.spreadsheet, + x: colAxis.x, + y: rowAxis.y, + width: colAxis.width, + height: rowAxis.height, + data, + rowIndex, + colIndex, + rowId: rowAxis.id, + colId: colAxis.id, + fieldValue: data, + valueField: yField, + xField, + yField, + id: getDataCellId(rowAxis.id, colAxis.id), + }; + + return options.layoutCellMeta?.(cellMeta) ?? cellMeta; + } + + protected getFrozenColSplitLineSize() { + const { viewportHeight, y: panelBBoxStartY } = this.panelBBox; + const { axisColsHierarchy } = this.layoutResult; + const height = + viewportHeight + panelBBoxStartY + (axisColsHierarchy?.height ?? 0); + + return { + y: 0, + height, + }; + } + + public getAxisCornerCells(): AxisCornerCell[] { + const headerChildren = (this.getAxisCornerHeader()?.children || + []) as AxisCornerCell[]; + + return getAllChildCells(headerChildren, AxisCornerCell).filter( + (cell: S2CellType) => + cell.cellType === (AxisCellType.AXIS_CORNER_CELL as any), + ); + } + + public getAxisRowCells(): AxisRowCell[] { + const headerChildren = (this.getAxisRowHeader()?.children || + []) as AxisRowCell[]; + + return getAllChildCells(headerChildren, AxisRowCell).filter( + (cell: S2CellType) => + cell.cellType === (AxisCellType.AXIS_ROW_CELL as any), + ); + } + + public getAxisColCells(): AxisColCell[] { + const headerChildren = (this.getAxisColHeader()?.children || + []) as AxisColCell[]; + + return getAllChildCells(headerChildren, AxisColCell).filter( + (cell: S2CellType) => + cell.cellType === (AxisCellType.AXIS_COL_CELL as any), + ); + } + + /** + * 获取表头单元格 (序号,角头,行头,列头) (不含可视区域) + * @example 获取全部: facet.getHeaderCells() + * @example 获取一组 facet.getHeaderCells(['root[&]浙江省[&]宁波市', 'root[&]浙江省[&]杭州市']) + */ + public getHeaderCells( + cellIds?: string[] | SelectedIds, + ): S2CellType[] { + const headerCells = concat( + this.getCornerCells(), + this.getSeriesNumberCells(), + this.getRowCells(), + this.getColCells(), + this.getAxisCornerCells(), + this.getAxisRowCells(), + this.getAxisColCells(), + ); + + return this.filterCells(headerCells, cellIds); + } + + public getAxisCornerNodes(): Node[] { + return this.axisCornerHeader?.getNodes() || []; + } + + public getAxisRowNodes(): Node[] { + return this.axisRowHeader?.getNodes() || []; + } + + public getAxisColNodes(): Node[] { + return this.axisColumnHeader?.getNodes() || []; + } + + /** + * 获取表头节点 (角头,序号,行头,列头) (含可视区域) + * @example 获取全部: facet.getHeaderNodes() + * @example 获取一组 facet.getHeaderNodes(['root[&]浙江省[&]宁波市', 'root[&]浙江省[&]杭州市']) + */ + public getHeaderNodes(nodeIds?: string[]): Node[] { + const headerNodes = concat( + this.getCornerNodes(), + this.getSeriesNumberNodes(), + this.getRowNodes(), + this.getColNodes(), + this.getAxisCornerNodes(), + this.getAxisRowNodes(), + this.getAxisColNodes(), + ); + + if (!nodeIds) { + return headerNodes; + } + + return headerNodes.filter((node) => nodeIds.includes(node.id)); + } + + /** + * 获取单元格的所有子节点 (含非可视区域) + * @example + * const rowCell = facet.getRowCells()[0] + * facet.getCellChildrenNodes(rowCell) + */ + public getCellChildrenNodes = (cell: S2CellType): Node[] => { + const selectNode = cell?.getMeta?.() as Node; + + return Node.getAllChildrenNodes(selectNode, (node) => { + // 行列头区域,也把对应的 axis 区域 node 返回 + return node.relatedNode ? [node, node.relatedNode] : [node]; + }); + }; +} diff --git a/packages/s2-core/src/extends/pivot-chart/header/axis-col.ts b/packages/s2-core/src/extends/pivot-chart/header/axis-col.ts new file mode 100644 index 0000000000..e300351a90 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/header/axis-col.ts @@ -0,0 +1,57 @@ +import { Group } from '@antv/g'; +import { + ColHeader, + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FRONT_GROUND_GROUP_SCROLL_Z_INDEX, + FrozenFacet, + Node, +} from '@antv/s2'; +import { AxisColCell } from '../cell/axis-col-cell'; +import { + KEY_GROUP_COL_AXIS_FROZEN, + KEY_GROUP_COL_AXIS_SCROLL, +} from '../constant'; +import { getExtraFrozenColAxisNodes } from '../utils/frozen'; + +export class AxisColHeader extends ColHeader { + protected initGroups(): void { + this.scrollGroup = this.appendChild( + new Group({ + name: KEY_GROUP_COL_AXIS_SCROLL, + style: { zIndex: FRONT_GROUND_GROUP_SCROLL_Z_INDEX }, + }), + ); + + this.frozenGroup = this.appendChild( + new Group({ + name: KEY_GROUP_COL_AXIS_FROZEN, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, + }), + ); + this.frozenTrailingGroup = this.appendChild( + new Group({ + name: KEY_GROUP_COL_AXIS_FROZEN, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, + }), + ); + + const { spreadsheet, nodes } = this.getHeaderConfig(); + + this.extraFrozenNodes = getExtraFrozenColAxisNodes( + spreadsheet.facet as FrozenFacet, + nodes, + ); + } + + public getCellInstance(node: Node): any { + const headerConfig = this.getHeaderConfig(); + + const { spreadsheet } = headerConfig; + const { axisColCell: colAxisCell } = spreadsheet.options; + + return ( + colAxisCell?.(node, spreadsheet, headerConfig) || + new AxisColCell(node, spreadsheet, headerConfig) + ); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/header/axis-corner.ts b/packages/s2-core/src/extends/pivot-chart/header/axis-corner.ts new file mode 100644 index 0000000000..cc214925a0 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/header/axis-corner.ts @@ -0,0 +1,111 @@ +import type { PointLike } from '@antv/g-lite'; +import { + CornerBBox, + CornerNodeType, + Node, + CornerHeader as OriginCornerHeader, + PanelBBox, + type BaseCornerOptions, +} from '@antv/s2'; +import { AxisCornerCell } from '../cell/axis-corner-cell'; + +export class AxisCornerHeader extends OriginCornerHeader { + /** + * Get corner Header by config + */ + public static getCornerHeader( + options: BaseCornerOptions & { + panelBBox: PanelBBox; + cornerBBox: CornerBBox; + }, + ) { + const { + panelBBox, + cornerBBox, + seriesNumberWidth, + layoutResult, + spreadsheet, + } = options; + const { y, viewportWidth, viewportHeight } = panelBBox; + const { originalWidth: cornerOriginalWidth, width: cornerWidth } = + cornerBBox; + + const { axisColsHierarchy } = layoutResult; + + const position = { + x: cornerBBox.x, + y: y + viewportHeight, + }; + + const height = axisColsHierarchy?.height ?? 0; + + const cornerNodes = this.getCornerNodes({ + position, + width: cornerOriginalWidth, + height, + layoutResult, + seriesNumberWidth, + spreadsheet, + }); + + return new AxisCornerHeader({ + nodes: cornerNodes, + position, + width: cornerWidth, + height, + originalWidth: cornerOriginalWidth, + originalHeight: height, + viewportWidth, + viewportHeight, + seriesNumberWidth, + spreadsheet, + }); + } + + public static getCornerNodes( + options: BaseCornerOptions & { + position: PointLike; + width: number; + height: number; + }, + ): Node[] { + const cornerNodes = []; + // 创建角头区域竖轴 + + const { layoutResult, spreadsheet } = options; + + const { axisColsHierarchy } = layoutResult; + + const colAxisNode = axisColsHierarchy?.sampleNodeForLastLevel; + + if (colAxisNode) { + const cornerNode = new Node({ + id: colAxisNode.id, + field: colAxisNode.field, + value: spreadsheet.dataSet.getFieldName(colAxisNode.field), + x: 0, + y: 0, + width: spreadsheet.facet.cornerBBox.originalWidth, + height: colAxisNode.height, + isPivotMode: true, + cornerType: CornerNodeType.Col, + spreadsheet, + }); + + cornerNodes.push(cornerNode); + } + + return cornerNodes; + } + + protected getCellInstance(node: Node): any { + const headerConfig = this.getHeaderConfig(); + const { spreadsheet } = headerConfig; + const { axisCornerCell } = spreadsheet.options; + + return ( + axisCornerCell?.(node, spreadsheet, headerConfig) || + new AxisCornerCell(node, spreadsheet, headerConfig) + ); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/header/axis-row.ts b/packages/s2-core/src/extends/pivot-chart/header/axis-row.ts new file mode 100644 index 0000000000..4e119f1ae3 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/header/axis-row.ts @@ -0,0 +1,57 @@ +import { Group } from '@antv/g'; +import { + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FRONT_GROUND_GROUP_SCROLL_Z_INDEX, + FrozenFacet, + Node, + RowHeader, +} from '@antv/s2'; +import { AxisRowCell } from '../cell/axis-row-cell'; +import { + KEY_GROUP_ROW_AXIS_FROZEN, + KEY_GROUP_ROW_AXIS_SCROLL, +} from '../constant'; +import { getExtraFrozenRowAxisNodes } from '../utils/frozen'; + +export class AxisRowHeader extends RowHeader { + protected initGroups(): void { + this.scrollGroup = this.appendChild( + new Group({ + name: KEY_GROUP_ROW_AXIS_SCROLL, + style: { zIndex: FRONT_GROUND_GROUP_SCROLL_Z_INDEX }, + }), + ); + + this.frozenGroup = this.appendChild( + new Group({ + name: KEY_GROUP_ROW_AXIS_FROZEN, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, + }), + ); + this.frozenTrailingGroup = this.appendChild( + new Group({ + name: KEY_GROUP_ROW_AXIS_FROZEN, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, + }), + ); + + const { spreadsheet, nodes } = this.getHeaderConfig(); + + this.extraFrozenNodes = getExtraFrozenRowAxisNodes( + spreadsheet.facet as FrozenFacet, + nodes, + ); + } + + public getCellInstance(node: Node): any { + const headerConfig = this.getHeaderConfig(); + + const { spreadsheet } = headerConfig; + const { axisRowCell: rowAxisCell } = spreadsheet.options; + + return ( + rowAxisCell?.(node, spreadsheet, headerConfig) || + new AxisRowCell(node, spreadsheet, headerConfig) + ); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/header/corner.ts b/packages/s2-core/src/extends/pivot-chart/header/corner.ts new file mode 100644 index 0000000000..a62c464767 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/header/corner.ts @@ -0,0 +1,54 @@ +import type { PointLike } from '@antv/g-lite'; +import { + CornerNodeType, + Node, + CornerHeader as OriginCornerHeader, + type BaseCornerOptions, +} from '@antv/s2'; + +export class CornerHeader extends OriginCornerHeader { + public static getCornerNodes( + options: BaseCornerOptions & { + position: PointLike; + width: number; + height: number; + }, + ): Node[] { + const cornerNodes = super.getCornerNodes(options); + // 创建角头区域竖轴 + + const { seriesNumberWidth, layoutResult, spreadsheet } = options; + + const { rowsHierarchy, axisRowsHierarchy, colsHierarchy } = layoutResult; + + const rowAxisNode = axisRowsHierarchy?.sampleNodeForLastLevel; + + if (rowAxisNode) { + const leafNode = colsHierarchy?.sampleNodeForLastLevel; + + const cornerNode = new Node({ + id: rowAxisNode.id, + field: rowAxisNode.field, + value: spreadsheet.dataSet.getFieldName(rowAxisNode.field), + x: seriesNumberWidth + rowsHierarchy.width + rowAxisNode.x, + y: leafNode?.y ?? 0, + width: rowAxisNode.width, + height: + leafNode?.height ?? + spreadsheet.facet.getCellCustomSize( + null, + spreadsheet.options.style?.colCell?.height, + ) ?? + 0, + + isPivotMode: true, + cornerType: CornerNodeType.Row, + spreadsheet, + }); + + cornerNodes.push(cornerNode); + } + + return cornerNodes; + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/index.ts b/packages/s2-core/src/extends/pivot-chart/index.ts new file mode 100644 index 0000000000..482fcc3a98 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/index.ts @@ -0,0 +1,11 @@ +export * from './cell/axis-col-cell'; +export * from './cell/axis-corner-cell'; +export * from './cell/axis-row-cell'; +export * from './cell/chart-data-cell'; +export * from './cell/pivot-chart-data-cell'; +export * from './facet/pivot-chart-facet'; +export * from './header/axis-col'; +export * from './header/axis-corner'; +export * from './header/axis-row'; +export * from './interface'; +export * from './pivot-chart-sheet'; diff --git a/packages/s2-core/src/extends/pivot-chart/interaction/axis-click.ts b/packages/s2-core/src/extends/pivot-chart/interaction/axis-click.ts new file mode 100644 index 0000000000..5af6c77cb8 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/interaction/axis-click.ts @@ -0,0 +1,43 @@ +import type { FederatedPointerEvent as CanvasEvent } from '@antv/g-lite'; +import { InteractionStateName, RowColumnClick, S2Event } from '@antv/s2'; +import { AxisCellType } from '../cell/cell-type'; +import { updateDataCellRelevantHeaderCells } from '../utils/handle-interaction'; + +export class AxisRowColumnClick extends RowColumnClick { + public bindEvents() { + this.bindKeyboardDown(); + this.bindKeyboardUp(); + this.bindAxisCellClick(); + this.bindDataCellClick(); + this.bindMouseMove(); + } + + protected bindAxisCellClick() { + this.spreadsheet.on(S2Event.GLOBAL_CLICK, (event: CanvasEvent) => { + const cell = this.spreadsheet.getCell(event.target); + + if (!cell) { + return; + } + + // axis col cell 在底部,点击后再往上选择 data cell 有点奇怪,暂时不处理 + if (cell.cellType === (AxisCellType.AXIS_ROW_CELL as any)) { + this.handleRowColClick(event); + } + }); + } + + protected bindDataCellClick() { + this.spreadsheet.on(S2Event.DATA_CELL_CLICK_TRIGGERED_PRIVATE, (cell) => { + const meta = cell.getMeta(); + + if (this.spreadsheet.options.interaction?.selectedCellHighlight) { + updateDataCellRelevantHeaderCells( + InteractionStateName.SELECTED, + meta, + this.spreadsheet, + ); + } + }); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/interaction/axis-hover.ts b/packages/s2-core/src/extends/pivot-chart/interaction/axis-hover.ts new file mode 100644 index 0000000000..5dbc0923c3 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/interaction/axis-hover.ts @@ -0,0 +1,57 @@ +import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; +import { + DataCell, + HoverEvent, + InteractionStateName, + S2Event, + type ViewMeta, +} from '@antv/s2'; +import { isEmpty } from 'lodash'; +import { AxisCellType } from '../cell/cell-type'; +import { updateDataCellRelevantHeaderCells } from '../utils/handle-interaction'; + +export class AxisHover extends HoverEvent { + public shouldSkipDataCellHoverEvent(event: CanvasEvent) { + const cell = this.spreadsheet.getCell(event.target); + + if (isEmpty(cell)) { + return true; + } + } + + public bindDataCellHover() { + this.spreadsheet.on( + S2Event.DATA_CELL_HOVER_TRIGGERED_PRIVATE, + (cell: DataCell) => { + const { options } = this.spreadsheet; + const { interaction: interactionOptions } = options; + const meta = cell?.getMeta() as ViewMeta; + + if (interactionOptions?.hoverHighlight) { + updateDataCellRelevantHeaderCells( + InteractionStateName.HOVER, + meta, + this.spreadsheet, + ); + } + }, + ); + } + + public bindHeaderCellHover() { + this.spreadsheet.on(S2Event.GLOBAL_HOVER, (event) => { + const cell = this.spreadsheet.getCell(event.target); + + if (!cell) { + return; + } + + if ( + cell.cellType === (AxisCellType.AXIS_ROW_CELL as any) || + cell.cellType === (AxisCellType.AXIS_COL_CELL as any) + ) { + this.handleHeaderHover(event); + } + }); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/interaction/root.ts b/packages/s2-core/src/extends/pivot-chart/interaction/root.ts new file mode 100644 index 0000000000..ae4e2a2f59 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/interaction/root.ts @@ -0,0 +1,30 @@ +import { + CellType, + RootInteraction as OriginRootInteraction, + type CellMeta, +} from '@antv/s2'; +import { isEqual, map, sortBy, uniq } from 'lodash'; +import { AxisCellType } from '../cell/cell-type'; + +const SameTypes = [ + sortBy([CellType.ROW_CELL, AxisCellType.AXIS_ROW_CELL]), + sortBy([CellType.COL_CELL, AxisCellType.AXIS_COL_CELL]), +]; + +export class RootInteraction extends OriginRootInteraction { + public shouldForbidHeaderCellSelected = (selectedCells: CellMeta[]) => { + // 禁止跨单元格选择, 这样计算出来的数据和交互没有任何意义 + + const types = sortBy(uniq(map(selectedCells, 'type'))); + + if (types.length <= 1) { + return false; + } + + if (SameTypes.some((same) => isEqual(same, types))) { + return false; + } + + return true; + }; +} diff --git a/packages/s2-core/src/extends/pivot-chart/interface.ts b/packages/s2-core/src/extends/pivot-chart/interface.ts new file mode 100644 index 0000000000..cac288eb64 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/interface.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import { type AxisComponent, type G2Spec } from '@antv/g2'; +import type { + CellCallback, + ColHeaderConfig, + CornerHeaderConfig, + DefaultCellTheme, + Hierarchy, + RowHeaderConfig, +} from '@antv/s2'; +import type { AxisColCell } from './cell/axis-col-cell'; +import type { AxisCornerCell } from './cell/axis-corner-cell'; +import type { AxisRowCell } from './cell/axis-row-cell'; +import type { AxisCellType } from './cell/cell-type'; +import type { PivotChartDataCell } from './cell/pivot-chart-data-cell'; + +export type ChartCoordinate = 'cartesian' | 'polar'; + +export interface Chart { + /** + * 当前图表的坐标系类型,chartSheet 通过该类型判断是否需要在行列头区域绘制坐标系 + * 独立配置是因为要从 spec 里面判断是笛卡尔坐标还是极坐标,场景非常多,覆盖完全很困难 + */ + coordinate?: ChartCoordinate; + dataCellSpec?: G2Spec | ((cell: PivotChartDataCell) => G2Spec); + axisRowCellSpec?: AxisComponent | ((cell: AxisRowCell) => AxisComponent); + axisColCellSpec?: AxisComponent | ((cell: AxisColCell) => AxisComponent); +} + +// @ts-ignore +declare module '@antv/s2' { + interface LayoutResult { + axisRowsHierarchy?: Hierarchy; + axisColsHierarchy?: Hierarchy; + } + + interface S2PivotSheetOptions { + chart?: Chart; + axisRowCell?: CellCallback; + axisColCell?: CellCallback; + axisCornerCell?: CellCallback; + } + + type AxisCellThemes = { + [K in AxisCellType]?: DefaultCellTheme; + }; + + interface S2Theme extends AxisCellThemes {} + + interface ViewMeta { + xField?: string; + yField?: string; + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/pivot-chart-sheet.ts b/packages/s2-core/src/extends/pivot-chart/pivot-chart-sheet.ts new file mode 100644 index 0000000000..f9d323e86b --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/pivot-chart-sheet.ts @@ -0,0 +1,166 @@ +import { + EXTRA_FIELD, + PivotSheet, + ResizeType, + setupDataConfig, + setupOptions, + type S2DataConfig, + type S2Options, + type ThemeCfg, + type ViewMeta, +} from '@antv/s2'; +import { last } from 'lodash'; +import { PivotChartDataCell } from './cell/pivot-chart-data-cell'; +import { + DEFAULT_COL_AXIS_SIZE, + DEFAULT_DIMENSION_SIZE, + DEFAULT_MEASURE_SIZE, + DEFAULT_OPTIONS, + DEFAULT_ROW_AXIS_SIZE, + FIXED_DATA_CONFIG, + FIXED_OPTIONS, +} from './constant'; +import { PivotChartFacet } from './facet/pivot-chart-facet'; +import { RootInteraction } from './interaction/root'; +import { getCustomTheme as defaultGetCustomTheme } from './utils/theme'; + +export class PivotChartSheet extends PivotSheet { + protected override initInteraction() { + this.interaction?.destroy?.(); + this.interaction = new RootInteraction(this); + } + + protected override setupDataConfig(dataCfg: S2DataConfig): void { + this.dataCfg = setupDataConfig(dataCfg, FIXED_DATA_CONFIG); + } + + protected override setupOptions(options: S2Options | null) { + this.options = setupOptions( + DEFAULT_OPTIONS, + this.getRuntimeDefaultOptions(options), + options, + this.getRuntimeFixedOptions(), + FIXED_OPTIONS, + ); + } + + public setThemeCfg( + themeCfg: ThemeCfg = {}, + getCustomTheme = defaultGetCustomTheme, + ) { + super.setThemeCfg(themeCfg, getCustomTheme); + } + + protected override buildFacet(): void { + if (this.isCustomRowFields() || this.isCustomColumnFields()) { + super.buildFacet(); + + return; + } + + const defaultCell = (viewMeta: ViewMeta) => + new PivotChartDataCell(viewMeta, this); + + this.options.dataCell ??= defaultCell; + this.facet?.destroy(); + this.facet = this.options.facet?.(this) ?? new PivotChartFacet(this); + this.facet.render(); + } + + protected getRuntimeDefaultOptions(options: S2Options | null): S2Options { + const { + rows = [], + columns = [], + valueInCols = true, + } = this.dataCfg.fields ?? {}; + + /** + * 下面的逻辑准则: + * 如果是笛卡尔坐标系,希望 x 轴 dimension 的每个维度默认宽度大致相同,y 轴 measure 的宽度始终保持都相同 + * 比如对于 rows: province-> city , value: number 来说 + * 四川下面有 n 个城市,北京下面有 m 个城市,那么四川的宽度是 n * width, 北京的宽度是 m * width + * 而不管是四川,还是北京, y 轴展示的都是 number 的值,那么 y 轴的宽度保持相同,能快速通过图形的尺寸看出数据的相对大小。 + * 如果是极坐标系, 希望 x 轴宽度相同,y 轴 measure 的宽度也都相同 + * 比如对于 rows: province-> city , value: number 来说 + * 四川下面有 n 个城市,北京下面有 m 个城市,那么四川的宽度是 width, 北京的宽度也是 width,不再以维度数量作为依据,能让数据的呈现效果更好 + */ + + const isPolar = this.isPolarCoordinate(options); + + if (valueInCols) { + const lastRow = last(rows) as string; + + return { + style: { + rowCell: { + widthByField: { + [lastRow]: DEFAULT_ROW_AXIS_SIZE, + }, + }, + colCell: { + heightByField: { + [EXTRA_FIELD]: DEFAULT_COL_AXIS_SIZE, + }, + }, + dataCell: { + width: DEFAULT_MEASURE_SIZE, + height: isPolar ? DEFAULT_MEASURE_SIZE : DEFAULT_DIMENSION_SIZE, + }, + }, + }; + } + + const lastCol = last(columns) as string; + + return { + style: { + rowCell: { + widthByField: { + [EXTRA_FIELD]: DEFAULT_ROW_AXIS_SIZE, + }, + }, + colCell: { + heightByField: { + [lastCol]: DEFAULT_COL_AXIS_SIZE, + }, + }, + dataCell: { + width: isPolar ? DEFAULT_MEASURE_SIZE : DEFAULT_DIMENSION_SIZE, + height: DEFAULT_MEASURE_SIZE, + }, + }, + }; + } + + protected getRuntimeFixedOptions(): S2Options { + const { valueInCols = true } = this.dataCfg.fields ?? {}; + + if (valueInCols) { + return { + interaction: { + resize: { + rowResizeType: ResizeType.CURRENT, + colResizeType: ResizeType.ALL, + }, + }, + }; + } + + return { + interaction: { + resize: { + rowResizeType: ResizeType.ALL, + colResizeType: ResizeType.CURRENT, + }, + }, + }; + } + + isPolarCoordinate(options: S2Options | null = this.options) { + return options?.chart?.coordinate === 'polar'; + } + + enableAsyncExport() { + return new Error("pivot chart doesn't support export all data"); + } +} diff --git a/packages/s2-core/src/extends/pivot-chart/utils/chart-options.ts b/packages/s2-core/src/extends/pivot-chart/utils/chart-options.ts new file mode 100644 index 0000000000..72acaea16c --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/utils/chart-options.ts @@ -0,0 +1,151 @@ +import type { AxisComponent, G2Spec } from '@antv/g2'; +import { + EXTRA_FIELD, + G2_THEME_TYPE, + type InternalFullyCellTheme, + type Node, + type SpreadSheet, + type ViewMeta, +} from '@antv/s2'; +import { map, unary } from 'lodash'; +import { X_FIELD_FORMATTER } from '../constant'; +import type { PivotChartSheet } from '../pivot-chart-sheet'; + +export function getTheme(s2: SpreadSheet): Pick { + const themeName = s2.getThemeName(); + + return { + theme: { + type: G2_THEME_TYPE[themeName] ?? 'light', + }, + }; +} + +export function getAxisStyle(cellStyle: InternalFullyCellTheme): AxisComponent { + return { + // title + titleSpacing: 0, + titleFontSize: cellStyle.bolderText.fontSize, + titleFontFamily: cellStyle.bolderText.fontFamily, + titleFontWeight: cellStyle.bolderText.fontWeight, + titleFill: cellStyle.bolderText.fill, + titleFillOpacity: cellStyle.bolderText.opacity, + + // label + labelAlign: 'horizontal', + labelAutoRotate: false, + labelFontSize: cellStyle.text.fontSize, + labelFontFamily: cellStyle.text.fontFamily, + labelFontWeight: cellStyle.text.fontWeight, + labelFill: cellStyle.text.fill, + labelFillOpacity: cellStyle.text.opacity, + labelStroke: cellStyle.text.fill, + labelStrokeOpacity: cellStyle.text.fill, + + // tick + tick: true, + tickStroke: cellStyle.text.fill, + tickStrokeOpacity: cellStyle.text.opacity, + + // line + line: false, + lineStroke: cellStyle.text.fill, + lineStrokeOpacity: cellStyle.text.opacity, + + // grid + grid: false, + }; +} + +export function getCoordinate(s2: SpreadSheet): Pick { + if ((s2 as PivotChartSheet).isPolarCoordinate?.()) { + return {}; + } + + return { + coordinate: { + transform: s2.isValueInCols() ? [{ type: 'transpose' }] : undefined, + }, + }; +} + +export function getAxisXOptions(meta: Node, s2: SpreadSheet): AxisComponent { + const domain = map(meta.children, (child) => { + const formatter = s2.dataSet.getFieldFormatter(child.field); + + return !child.isTotalRoot && formatter + ? formatter(child.value, undefined, child) + : child.value; + }); + + return { + type: 'axisX', + scale: { + x: { + type: 'band', + domain, + range: [0, 1], + }, + }, + }; +} + +export function getScaleY( + value: string, + s2: SpreadSheet, +): Pick { + if ((s2 as PivotChartSheet).isPolarCoordinate?.()) { + return {}; + } + + const range = s2.dataSet.getValueRangeByField(value); + + return { + scale: { + y: { + type: 'linear', + domain: [range.minValue, range.maxValue], + range: [1, 0], + }, + }, + }; +} + +export function getAxisYOptions(meta: Node, s2: SpreadSheet): AxisComponent { + const { field, value } = meta; + + const formatter = s2.dataSet.getFieldFormatter(value); + + return { + type: 'axisY', + ...getScaleY(value, s2), + labelFormatter: unary(formatter), + title: s2.dataSet.getFieldFormatter(field)?.(value), + }; +} + +export function getTooltip( + viewMeta: ViewMeta, + s2: SpreadSheet, +): Pick { + const { xField, yField } = viewMeta; + const dataSet = s2.dataSet; + + return { + tooltip: { + title: (data: any) => { + return data[X_FIELD_FORMATTER] + ? dataSet.getFieldFormatter(xField!)?.(data[xField!]) + : data[xField!]; + }, + items: [ + (data: any) => { + return { + name: dataSet.getFieldFormatter(EXTRA_FIELD)?.(yField), + value: dataSet.getFieldFormatter(yField!)?.(data[yField!]), + }; + }, + ], + }, + }; +} diff --git a/packages/s2-core/src/extends/pivot-chart/utils/frozen.ts b/packages/s2-core/src/extends/pivot-chart/utils/frozen.ts new file mode 100644 index 0000000000..d8763b8297 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/utils/frozen.ts @@ -0,0 +1,92 @@ +import { Node, type FrozenFacet } from '@antv/s2'; + +function getNodesByRange( + nodes: Node[], + key: 'rowIndex' | 'colIndex', + minIndex: number, + maxIndex: number, +) { + return nodes.filter((node) => node[key] >= minIndex && node[key] <= maxIndex); +} + +export function getExtraFrozenColAxisNodes(facet: FrozenFacet, nodes: Node[]) { + const extraNodes: Node[] = []; + + const { colCount, trailingColCount } = facet.getFrozenOptions(); + + if (colCount) { + const frozenLeafNodes = getNodesByRange( + nodes, + 'colIndex', + 0, + colCount - 1, + )!; + + frozenLeafNodes.forEach((leafNode) => { + const newLeafNode = leafNode.clone(); + + newLeafNode.isFrozenHead = true; + extraNodes.push(newLeafNode); + }); + } + + if (trailingColCount) { + const total = nodes.length; + const frozenLeafNodes = getNodesByRange( + nodes, + 'colIndex', + total - trailingColCount, + total - 1, + )!; + + frozenLeafNodes.forEach((leafNode) => { + const newLeafNode = leafNode.clone(); + + newLeafNode.isFrozenTrailing = true; + extraNodes.push(newLeafNode); + }); + } + + return extraNodes; +} + +export function getExtraFrozenRowAxisNodes(facet: FrozenFacet, nodes: Node[]) { + const extraNodes: Node[] = []; + + const { start, end } = facet.getCellRange(); + const { rowCount, trailingRowCount } = facet.getFrozenOptions(); + + if (rowCount) { + const frozenLeafNodes = getNodesByRange( + nodes, + 'rowIndex', + start, + start + rowCount - 1, + )!; + + frozenLeafNodes.forEach((leafNode) => { + const newLeafNode = leafNode.clone(); + + newLeafNode.isFrozenHead = true; + extraNodes.push(newLeafNode); + }); + } + + if (trailingRowCount) { + const frozenLeafNodes = getNodesByRange( + nodes, + 'rowIndex', + end - trailingRowCount + 1, + end, + )!; + + frozenLeafNodes.forEach((leafNode) => { + const newLeafNode = leafNode.clone(); + + newLeafNode.isFrozenTrailing = true; + extraNodes.push(newLeafNode); + }); + } + + return extraNodes; +} diff --git a/packages/s2-core/src/extends/pivot-chart/utils/handle-interaction.ts b/packages/s2-core/src/extends/pivot-chart/utils/handle-interaction.ts new file mode 100644 index 0000000000..07bd873b8f --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/utils/handle-interaction.ts @@ -0,0 +1,55 @@ +import { InteractionStateName, type ViewMeta } from '../../../common'; +import type { SpreadSheet } from '../../../sheet-type'; +import { updateAllHeaderCellState } from '../../../utils'; +import type { PivotChartFacet } from '../facet/pivot-chart-facet'; + +function updateDataCellRelevantAxisRowCells( + stateName: InteractionStateName, + meta: ViewMeta, + spreadsheet: SpreadSheet, +) { + const { rowId } = meta; + const { facet, interaction } = spreadsheet; + const { rowHeader } = + stateName === InteractionStateName.HOVER + ? interaction.getHoverHighlight() + : interaction.getSelectedCellHighlight(); + + if (rowHeader && rowId) { + updateAllHeaderCellState( + rowId, + (facet as PivotChartFacet).getAxisRowCells(), + stateName, + ); + } +} + +function updateDataCellRelevantAxisColCells( + stateName: InteractionStateName, + meta: ViewMeta, + spreadsheet: SpreadSheet, +) { + const { colId } = meta; + const { facet, interaction } = spreadsheet; + const { colHeader } = + stateName === InteractionStateName.HOVER + ? interaction.getHoverHighlight() + : interaction.getSelectedCellHighlight(); + + if (colHeader && colId) { + updateAllHeaderCellState( + colId, + (facet as PivotChartFacet).getAxisColCells(), + stateName, + ); + } +} + +export function updateDataCellRelevantHeaderCells( + stateName: InteractionStateName, + meta: ViewMeta, + spreadsheet: SpreadSheet, +) { + updateDataCellRelevantAxisRowCells(stateName, meta, spreadsheet); + updateDataCellRelevantAxisColCells(stateName, meta, spreadsheet); +} diff --git a/packages/s2-core/src/extends/pivot-chart/utils/separate-axis.ts b/packages/s2-core/src/extends/pivot-chart/utils/separate-axis.ts new file mode 100644 index 0000000000..d9c4c69974 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/utils/separate-axis.ts @@ -0,0 +1,393 @@ +import type { Pick } from '@antv/g2/lib/data'; +import { + EXTRA_FIELD, + Hierarchy, + Node, + ROOT_NODE_ID, + SpreadSheet, + generateId, + type LayoutResult, + type NodeProperties, + type Query, +} from '@antv/s2'; +import { + forEach, + head, + includes, + initial, + isEmpty, + last, + merge, + reduce, + uniq, +} from 'lodash'; +import { PLACEHOLDER_FIELD } from '../constant'; + +export function getAxisLeafNodes(hierarchy: Hierarchy) { + const axisLeafNodes = hierarchy.getLeaves().reduce((acc, leaf) => { + const parent = leaf.parent; + + if (parent) { + acc.push(parent); + } + + return acc; + }, [] as Node[]); + + return uniq(axisLeafNodes); +} + +type Index = 'rowIndex' | 'colIndex'; + +/** + * 需要考虑的场景: + * 1. 数值置于行头、列头 + * 2. 单指标、多指标 + * 3. 总计、小计分组 + */ + +function getMeasureValue(query: Query = {}, s2: SpreadSheet) { + return query[EXTRA_FIELD] ?? s2.dataSet.fields.values?.[0]; +} + +function createHierarchy() { + const axisHierarchy = new Hierarchy(); + + axisHierarchy.maxLevel = 0; + + return axisHierarchy; +} + +function pushAxisIndexNode( + axisHierarchy: Hierarchy, + axisNode: Node, + key: Index, +) { + axisNode.isLeaf = true; + + axisHierarchy.pushNode(axisNode); + axisHierarchy.pushIndexNode(axisNode); + axisNode[key] = axisHierarchy.getIndexNodes().length - 1; + + if (!axisNode.isTotals && isEmpty(axisHierarchy.sampleNodesForAllLevels)) { + axisHierarchy.sampleNodesForAllLevels.push(axisNode); + axisHierarchy.sampleNodeForLastLevel = axisNode; + } +} + +function pushIndexNode(hierarchy: Hierarchy, node: Node, key: Index) { + node.isLeaf = true; + node.children = []; + + hierarchy.pushIndexNode(node); + node[key] = hierarchy.getIndexNodes().length - 1; +} + +function shrinkHierarchy(hierarchy: Hierarchy) { + hierarchy.maxLevel--; + hierarchy.sampleNodesForAllLevels = initial( + hierarchy.sampleNodesForAllLevels, + ); + + hierarchy.sampleNodeForLastLevel = + last(hierarchy.sampleNodesForAllLevels) ?? null; +} + +function convertToMeasurePlaceholderHierarchy( + hierarchy: Hierarchy, + s2: SpreadSheet, +) { + const placeholderNode = new Node({ + id: generateId(ROOT_NODE_ID, PLACEHOLDER_FIELD), + field: PLACEHOLDER_FIELD, + value: s2.dataSet.getFieldName(hierarchy.sampleNodeForLastLevel!.field), + level: 0, + isLeaf: false, + parent: hierarchy.rootNode, + children: hierarchy.rootNode.children, + }); + + hierarchy.rootNode.children.forEach((child) => { + child.parent = placeholderNode; + child.level++; + }); + + hierarchy.rootNode.children = [placeholderNode]; + + hierarchy.pushNode(placeholderNode); + + hierarchy.sampleNodesForAllLevels = [placeholderNode]; + hierarchy.sampleNodeForLastLevel = placeholderNode; +} + +function separateMeasureNodes( + hierarchy: Hierarchy, + key: Index, + s2: SpreadSheet, +) { + const axisHierarchy = createHierarchy(); + + forEach(hierarchy.getLeaves(), (leaf: Node) => { + const axisNode = leaf.clone(); + + leaf.relatedNode = axisNode; + + leaf.hideColCellHorizontalResize = true; + leaf.hideRowCellVerticalResize = true; + + if (axisNode.field !== EXTRA_FIELD) { + // 总计、小计单指标时不展示 + axisNode.field = EXTRA_FIELD; + axisNode.value = getMeasureValue(axisNode.query, s2); + } + + axisNode.children = [axisNode]; + + pushAxisIndexNode(axisHierarchy, axisNode, key); + }); + + if (hierarchy.maxLevel === 0) { + convertToMeasurePlaceholderHierarchy(hierarchy, s2); + } else { + shrinkHierarchy(hierarchy); + } + + return { + axisHierarchy, + hierarchy, + }; +} + +function separateRowMeasureNodes( + rowsHierarchy: Hierarchy, + s2: SpreadSheet, +): Pick { + const { axisHierarchy, hierarchy } = separateMeasureNodes( + rowsHierarchy, + 'rowIndex', + s2, + ); + + return { + rowsHierarchy: hierarchy, + axisRowsHierarchy: axisHierarchy, + }; +} + +function separateColMeasureNodes( + colsHierarchy: Hierarchy, + s2: SpreadSheet, +): Pick { + const { axisHierarchy, hierarchy } = separateMeasureNodes( + colsHierarchy, + 'colIndex', + s2, + ); + + return { + colsHierarchy: hierarchy, + axisColsHierarchy: axisHierarchy, + }; +} + +function createDimensionPlaceholderHierarchy(nodeProperties: NodeProperties) { + const hierarchy = createHierarchy(); + + hierarchy.isPlaceholder = true; + + const placeholderNode = new Node({ + id: generateId(ROOT_NODE_ID, PLACEHOLDER_FIELD), + field: PLACEHOLDER_FIELD, + level: 0, + isLeaf: true, + rowIndex: 0, + colIndex: 0, + parent: hierarchy.rootNode, + ...nodeProperties, + }); + + hierarchy.rootNode.children = [placeholderNode]; + hierarchy.pushNode(placeholderNode); + hierarchy.pushIndexNode(placeholderNode); + + hierarchy.sampleNodesForAllLevels = [placeholderNode]; + hierarchy.sampleNodeForLastLevel = placeholderNode; + + return hierarchy; +} + +function separateDimensionNodes( + hierarchy: Hierarchy, + key: Index, + s2: SpreadSheet, +) { + const axisHierarchy = createHierarchy(); + + const sampleNodeForLastLevel = hierarchy.sampleNodeForLastLevel!; + + // 只有一个维度层级时,会被全部收敛到坐标轴中 + // 再给一个Node用于占位 + if (hierarchy.maxLevel === 0) { + const root = hierarchy.rootNode.clone(); + + root.id = generateId(ROOT_NODE_ID, PLACEHOLDER_FIELD); + root.field = sampleNodeForLastLevel.field; + pushAxisIndexNode(axisHierarchy, root, key); + + const value = s2.dataSet.getFieldName(sampleNodeForLastLevel.field); + + hierarchy = createDimensionPlaceholderHierarchy( + merge( + { + value, + relatedNode: root, + }, + key === 'rowIndex' && { field: sampleNodeForLastLevel.field }, + ), + ); + + return { + hierarchy, + axisHierarchy, + }; + } + + const leafNodeParentMapping = reduce( + hierarchy.getLeaves(), + (acc, leaf) => { + const parent = leaf.parent; + + if (!parent) { + return acc; + } + + if (!acc.get(parent)) { + acc.set(parent, []); + } + + const exist = acc.get(parent)!; + + exist.push(leaf); + + return acc; + }, + new Map(), + ); + + hierarchy.indexNode = []; + leafNodeParentMapping.forEach((children, parent) => { + let axisNode; + + // 总计、小计跨多行展示时,会出现不一致的情况下,只需要将 leaf 节点复制一份,无需剔除 + if (parent.children.length !== children.length) { + children.forEach((leaf) => { + axisNode = leaf.clone(); + axisNode.children = [axisNode]; + leaf.relatedNode = axisNode; + pushIndexNode(hierarchy, leaf, key); + }); + } else { + axisNode = parent.clone(); + + parent.relatedNode = axisNode; + pushIndexNode(hierarchy, parent, key); + axisNode.field = head(children)!.field; + + hierarchy.allNodesWithoutRoot = hierarchy.allNodesWithoutRoot.filter( + (node) => !includes(children, node), + ); + } + + if (axisNode) { + pushAxisIndexNode(axisHierarchy, axisNode, key); + } + }); + + shrinkHierarchy(hierarchy); + + return { + axisHierarchy, + hierarchy, + }; +} + +function separateRowDimensionNodes( + rowsHierarchy: Hierarchy, + s2: SpreadSheet, +): Pick { + const { axisHierarchy, hierarchy } = separateDimensionNodes( + rowsHierarchy, + 'rowIndex', + s2, + ); + + return { + axisRowsHierarchy: axisHierarchy, + rowsHierarchy: hierarchy, + }; +} + +function separateColDimensionNodes( + colsHierarchy: Hierarchy, + s2: SpreadSheet, +): Pick { + const { axisHierarchy, hierarchy } = separateDimensionNodes( + colsHierarchy, + 'colIndex', + s2, + ); + + return { + axisColsHierarchy: axisHierarchy, + colsHierarchy: hierarchy, + }; +} + +function separateRowNodesToAxis(hierarchy: Hierarchy, s2: SpreadSheet) { + if (hierarchy.maxLevel === -1) { + return null; + } + + const isValueInCols = s2.isValueInCols?.(); + + const { rowsHierarchy, axisRowsHierarchy } = isValueInCols + ? separateRowDimensionNodes(hierarchy, s2) + : separateRowMeasureNodes(hierarchy, s2); + + return { + rowsHierarchy, + rowLeafNodes: rowsHierarchy.getLeaves(), + axisRowsHierarchy, + }; +} + +function separateColNodesToAxis(hierarchy: Hierarchy, s2: SpreadSheet) { + if (hierarchy.maxLevel === -1) { + return null; + } + + const isValueInCols = s2.isValueInCols?.(); + + const { colsHierarchy, axisColsHierarchy } = isValueInCols + ? separateColMeasureNodes(hierarchy, s2) + : separateColDimensionNodes(hierarchy, s2); + + return { + colsHierarchy, + colLeafNodes: colsHierarchy.getLeaves(), + axisColsHierarchy, + }; +} + +export function separateRowColLeafNodes( + layoutResult: LayoutResult, + s2: SpreadSheet, +): LayoutResult { + const { rowsHierarchy, colsHierarchy } = layoutResult; + + return { + ...layoutResult, + ...separateRowNodesToAxis(rowsHierarchy, s2), + ...separateColNodesToAxis(colsHierarchy, s2), + }; +} diff --git a/packages/s2-core/src/extends/pivot-chart/utils/theme.ts b/packages/s2-core/src/extends/pivot-chart/utils/theme.ts new file mode 100644 index 0000000000..1dc4c1aa60 --- /dev/null +++ b/packages/s2-core/src/extends/pivot-chart/utils/theme.ts @@ -0,0 +1,25 @@ +import { + SpreadSheet, + getColCellTheme, + getCornerCellTheme, + getRowCellTheme, + type S2Theme, + type SimplePalette, +} from '@antv/s2'; +import { merge } from 'lodash'; +import { AxisCellType } from '../cell/cell-type'; + +export const getCustomTheme = ( + palette: SimplePalette, + spreadsheet?: SpreadSheet, +): S2Theme => { + return { + [AxisCellType.AXIS_CORNER_CELL]: getCornerCellTheme(palette), + [AxisCellType.AXIS_ROW_CELL]: getRowCellTheme(palette, spreadsheet), + [AxisCellType.AXIS_COL_CELL]: merge(getColCellTheme(palette), { + measureText: { + textAlign: 'center', + }, + }), + }; +}; diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index 6ef813d852..df23062ccc 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -15,6 +15,8 @@ import { filter, find, get, + includes, + isArray, isEmpty, isFunction, isNil, @@ -87,6 +89,7 @@ import type { import { PanelScrollGroup } from '../group/panel-scroll-group'; import type { SpreadSheet } from '../sheet-type'; import { ScrollBar, ScrollType } from '../ui/scrollbar'; +import type { SelectedIds } from '../utils'; import { getAdjustedRowScrollX, getAdjustedScrollOffset } from '../utils/facet'; import { getAllChildCells } from '../utils/get-all-child-cells'; import { getColsForGrid, getRowsForGrid } from '../utils/grid'; @@ -260,7 +263,7 @@ export abstract class BaseFacet { this.initForegroundGroup(); } - private initForegroundGroup() { + protected initForegroundGroup() { this.foregroundGroup = this.spreadsheet.container.appendChild( new Group({ name: KEY_GROUP_FORE_GROUND, @@ -269,7 +272,7 @@ export abstract class BaseFacet { ); } - private initBackgroundGroup() { + protected initBackgroundGroup() { this.backgroundGroup = this.spreadsheet.container.appendChild( new Group({ name: KEY_GROUP_BACK_GROUND, @@ -293,8 +296,8 @@ export abstract class BaseFacet { this.panelGroup.appendChild(this.panelScrollGroup); } - protected getCellCustomSize(node: Node | null, size: CellCustomSize) { - return isFunction(size) ? size?.(node) : size; + public getCellCustomSize(node: Node | null, customSize: CellCustomSize) { + return isFunction(customSize) ? customSize(node) : customSize; } protected getRowCellDraggedWidth(node: Node): number | undefined { @@ -509,6 +512,7 @@ export abstract class BaseFacet { colsHierarchy.height += levelSampleNode.height; }); + colsHierarchy.rootNode.height = colsHierarchy.height; } hideScrollBar = () => { @@ -759,7 +763,7 @@ export abstract class BaseFacet { } }; - private unbindEvents = () => { + protected unbindEvents = () => { const canvas = this.spreadsheet.getCanvasElement(); canvas?.removeEventListener('wheel', this.onWheel); @@ -796,9 +800,9 @@ export abstract class BaseFacet { this.cornerBBox = new CornerBBox(this, true); } - protected calculatePanelBBox = () => { + protected calculatePanelBBox() { this.panelBBox = new PanelBBox(this, true); - }; + } getRealWidth = (): number => last(this.viewCellWidths) || 0; @@ -924,7 +928,7 @@ export abstract class BaseFacet { this.dynamicRenderCell(skipScrollEvent); }; - private getRendererHeight = () => { + protected getRendererHeight = () => { const { start, end } = this.getCellRange(); return ( @@ -933,7 +937,7 @@ export abstract class BaseFacet { ); }; - private getAdjustedScrollOffset = ({ + protected getAdjustedScrollOffset = ({ scrollX, scrollY, rowHeaderScrollX, @@ -956,7 +960,7 @@ export abstract class BaseFacet { }; }; - private renderRowScrollBar = (rowHeaderScrollX: number) => { + protected renderRowScrollBar(rowHeaderScrollX: number) { if ( this.spreadsheet.isFrozenRowHeader() && this.cornerBBox.width < this.cornerBBox.originalWidth @@ -1028,12 +1032,13 @@ export abstract class BaseFacet { ); this.foregroundGroup.appendChild(this.hRowScrollBar); } - }; + } - getValidScrollBarOffset = (offset: number, maxOffset: number) => - clamp(offset, 0, maxOffset); + getValidScrollBarOffset(offset: number, maxOffset: number) { + return clamp(offset, 0, maxOffset); + } - renderHScrollBar = (width: number, realWidth: number, scrollX: number) => { + renderHScrollBar(width: number, realWidth: number, scrollX: number) { if (floor(width) < floor(realWidth)) { const halfScrollSize = this.scrollBarSize / 2; const { maxY } = this.getScrollbarPosition(); @@ -1092,7 +1097,7 @@ export abstract class BaseFacet { this.foregroundGroup.appendChild(this.hScrollBar); } - }; + } protected getScrollbarPosition() { const { maxX, maxY } = this.panelBBox; @@ -1107,7 +1112,7 @@ export abstract class BaseFacet { }; } - renderVScrollBar = (height: number, realHeight: number, scrollY: number) => { + renderVScrollBar(height: number, realHeight: number, scrollY: number) { if (height < realHeight) { const { scrollBar } = this.spreadsheet.theme; const thumbLen = Math.max( @@ -1149,7 +1154,7 @@ export abstract class BaseFacet { this.foregroundGroup.appendChild(this.vScrollBar); } - }; + } // (滑动 offset / 最大 offset(滚动对象真正长度 - 轨道长)) = (滑块 offset / 最大滑动距离(轨道长 - 滑块长)) getScrollBarOffset = (offset: number, scrollbar: ScrollBar) => { @@ -1311,7 +1316,7 @@ export abstract class BaseFacet { * 2. none => 临近滚动区域不受到滚动链影响,而且默认的滚动到边界的表现也被阻止 * 所以只要不为 `auto`, 或者表格内, 都需要阻止外部容器滚动 */ - private stopScrollChainingIfNeeded = (event: WheelEvent) => { + protected stopScrollChainingIfNeeded = (event: WheelEvent) => { const { interaction } = this.spreadsheet.options; if (interaction?.overscrollBehavior !== 'auto') { @@ -1320,7 +1325,7 @@ export abstract class BaseFacet { } }; - private stopScrollChaining = (event: WheelEvent) => { + protected stopScrollChaining = (event: WheelEvent) => { if (event?.cancelable) { event?.preventDefault?.(); } @@ -1779,7 +1784,7 @@ export abstract class BaseFacet { this.onAfterScroll(); } - private emitScrollEvent(position: CellScrollPosition) { + protected emitScrollEvent(position: CellScrollPosition) { this.spreadsheet.emit(S2Event.GLOBAL_SCROLL, position); } @@ -2223,6 +2228,31 @@ export abstract class BaseFacet { ); } + protected filterCells( + cells: S2CellType[], + filterIds?: string[] | SelectedIds, + ) { + if (isEmpty(filterIds)) { + return cells; + } + + if (isArray(filterIds)) { + return cells.filter((cell) => { + return includes(filterIds, cell.getMeta().id); + }); + } + + return cells.filter((cell) => { + const ids = filterIds[cell.cellType]; + + if (!ids) { + return false; + } + + return ids.includes(cell.getMeta().id); + }); + } + /** * 获取序号单元格 (不含可视区域) */ @@ -2235,7 +2265,9 @@ export abstract class BaseFacet { * @example 获取全部: facet.getHeaderCells() * @example 获取一组 facet.getHeaderCells(['root[&]浙江省[&]宁波市', 'root[&]浙江省[&]杭州市']) */ - public getHeaderCells(cellIds?: string[]): S2CellType[] { + public getHeaderCells( + cellIds?: string[] | SelectedIds, + ): S2CellType[] { const headerCells = concat( this.getCornerCells(), this.getSeriesNumberCells(), @@ -2243,11 +2275,7 @@ export abstract class BaseFacet { this.getColCells(), ); - if (!cellIds) { - return headerCells; - } - - return headerCells.filter((cell) => cellIds.includes(cell.getMeta().id)); + return this.filterCells(headerCells, cellIds); } /** diff --git a/packages/s2-core/src/facet/bbox/corner-bbox.ts b/packages/s2-core/src/facet/bbox/corner-bbox.ts index 6aebf3f4b8..57c1574c63 100644 --- a/packages/s2-core/src/facet/bbox/corner-bbox.ts +++ b/packages/s2-core/src/facet/bbox/corner-bbox.ts @@ -5,8 +5,8 @@ import { BaseBBox } from './base-bbox'; export class CornerBBox extends BaseBBox { calculateBBox() { - const width = this.getCornerBBoxWidth(); - const height = this.getCornerBBoxHeight(); + const width = this.getWidth(); + const height = this.getHeight(); this.width = width; this.height = height; @@ -14,7 +14,7 @@ export class CornerBBox extends BaseBBox { this.maxY = height; } - private getCornerBBoxOriginalHeight() { + protected calculateOriginalHeight() { const { colsHierarchy } = this.layoutResult; const { colCell } = this.spreadsheet.options.style!; @@ -24,34 +24,38 @@ export class CornerBBox extends BaseBBox { * 2. 配置了 rows, values, 此时存在一级列头 (即 EXTRA_FIELD 数值节点), 但是隐藏了数值 (hideMeasureColumn), 此时列头为空 */ if (!colsHierarchy.sampleNodeForLastLevel) { - return colCell?.height; + this.originalHeight = + this.facet.getCellCustomSize(null, colCell?.height) ?? 0; + } else { + this.originalHeight = floor(colsHierarchy.height); } - - return floor(colsHierarchy.height); - } - - private getCornerBBoxHeight() { - this.originalHeight = this.getCornerBBoxOriginalHeight() as number; - - return this.originalHeight; } - private getCornerBBoxWidth() { + protected calculateOriginWidth() { const { rowsHierarchy } = this.layoutResult; this.originalWidth = floor( rowsHierarchy.width + this.facet.getSeriesNumberWidth(), ); + } + + protected getHeight() { + this.calculateOriginalHeight(); + + return this.originalHeight; + } + protected getWidth() { + this.calculateOriginWidth(); // 在行头固定时,需对角头 BBox 进行裁剪 if (this.spreadsheet.isFrozenRowHeader()) { - return this.adjustCornerBBoxWidth(); + return this.adjustWidth(); } return this.originalWidth; } - private adjustCornerBBoxWidth() { + protected adjustWidth() { const { colsHierarchy } = this.layoutResult; const { width: canvasWidth, frozen } = this.spreadsheet.options; @@ -62,7 +66,7 @@ export class CornerBBox extends BaseBBox { const maxCornerBBoxWidth = canvasWidth! * ratio; const colsHierarchyWidth = colsHierarchy?.width; - const panelWidthWidthUnClippedCorner = canvasWidth! - this.originalWidth; + const panelWidthWithoutUnClippedCorner = canvasWidth! - this.originalWidth; /* * 不需要裁剪条件: @@ -71,7 +75,7 @@ export class CornerBBox extends BaseBBox { */ if ( this.originalWidth <= maxCornerBBoxWidth || - colsHierarchyWidth <= panelWidthWidthUnClippedCorner + colsHierarchyWidth <= panelWidthWithoutUnClippedCorner ) { return this.originalWidth; } @@ -83,7 +87,7 @@ export class CornerBBox extends BaseBBox { if (colsHierarchyWidth <= maxPanelWidth) { clippedWidth = this.originalWidth - - (colsHierarchyWidth - panelWidthWidthUnClippedCorner); + (colsHierarchyWidth - panelWidthWithoutUnClippedCorner); } else { clippedWidth = maxCornerBBoxWidth; } diff --git a/packages/s2-core/src/facet/bbox/panel-bbox.ts b/packages/s2-core/src/facet/bbox/panel-bbox.ts index b76af3b444..a4a72be5e9 100644 --- a/packages/s2-core/src/facet/bbox/panel-bbox.ts +++ b/packages/s2-core/src/facet/bbox/panel-bbox.ts @@ -4,8 +4,8 @@ import { BaseBBox } from './base-bbox'; export class PanelBBox extends BaseBBox { calculateBBox() { - this.originalWidth = this.facet.getRealWidth(); - this.originalHeight = this.facet.getRealHeight(); + this.calculateOriginWidth(); + this.calculateOriginalHeight(); const { cornerBBox } = this.facet; const cornerPosition = { @@ -20,22 +20,38 @@ export class PanelBBox extends BaseBBox { this.minX = this.x; this.minY = this.y; - const scrollBarSize = this.spreadsheet.theme.scrollBar!.size; - const { width: canvasWidth, height: canvasHeight } = - this.spreadsheet.options; - - const panelWidth = Math.max(0, canvasWidth! - this.x); - const panelHeight = Math.max(0, canvasHeight! - this.y - scrollBarSize!); - - this.width = panelWidth; - this.height = panelHeight; + this.width = this.getPanelWidth(); + this.height = this.getPanelHeight(); this.viewportHeight = Math.abs( - floor(Math.min(panelHeight, this.originalHeight)), + floor(Math.min(this.height, this.originalHeight)), ); this.viewportWidth = Math.abs( - floor(Math.min(panelWidth, this.originalWidth)), + floor(Math.min(this.width, this.originalWidth)), ); this.maxX = this.x + this.viewportWidth; this.maxY = this.y + this.viewportHeight; } + + protected calculateOriginalHeight() { + this.originalHeight = this.facet.getRealHeight(); + } + + protected calculateOriginWidth() { + this.originalWidth = this.facet.getRealWidth(); + } + + protected getPanelWidth() { + const { width: canvasWidth } = this.spreadsheet.options; + const panelWidth = Math.max(0, canvasWidth! - this.x); + + return panelWidth; + } + + protected getPanelHeight() { + const scrollBarSize = this.spreadsheet.theme.scrollBar!.size; + const { height: canvasHeight } = this.spreadsheet.options; + const panelHeight = Math.max(0, canvasHeight! - this.y - scrollBarSize!); + + return panelHeight; + } } diff --git a/packages/s2-core/src/facet/frozen-facet.ts b/packages/s2-core/src/facet/frozen-facet.ts index 720a983589..e68fc1cfbf 100644 --- a/packages/s2-core/src/facet/frozen-facet.ts +++ b/packages/s2-core/src/facet/frozen-facet.ts @@ -1,7 +1,7 @@ import { Group, Rect, type LineStyleProps } from '@antv/g'; import { last } from 'lodash'; import type { DataCell } from '../cell'; -import type { S2BaseFrozenOptions } from '../common'; +import type { S2BaseFrozenOptions, SplitLine } from '../common'; import { FRONT_GROUND_GROUP_FROZEN_Z_INDEX, FrozenGroupArea, @@ -18,7 +18,11 @@ import type { } from '../common/interface/frozen'; import type { SimpleBBox } from '../engine'; import { FrozenGroup } from '../group/frozen-group'; -import { getValidFrozenOptions, renderLine } from '../utils'; +import { + getValidFrozenOptions, + renderLine, + waitForCellMounted, +} from '../utils'; import { getColsForGrid, getFrozenRowsForGrid, @@ -260,10 +264,10 @@ export abstract class FrozenFacet extends BaseFacet { this.frozenGroups[frozenGroupType].appendChild(cell); } - setTimeout(() => { + waitForCellMounted(() => { this.spreadsheet.emit(S2Event.DATA_CELL_RENDER, cell); this.spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); - }, 100); + }); }; addFrozenCell = (colIndex: number, rowIndex: number, group: Group) => { @@ -413,29 +417,9 @@ export abstract class FrozenFacet extends BaseFacet { // eslint-disable-next-line max-lines-per-function protected renderFrozenGroupSplitLine = (scrollX: number, scrollY: number) => { - const { - viewportWidth, - viewportHeight, - x: panelBBoxStartX, - y: panelBBoxStartY, - } = this.panelBBox; - - const cellRange = this.getCellRange(); - const { rowCount, colCount, trailingColCount, trailingRowCount } = - this.getFrozenOptions(); - // 在分页条件下需要额外处理 Y 轴滚动值 const relativeScrollY = Math.floor(scrollY - this.getPaginationScrollY()); - // scroll boundary - const maxScrollX = Math.max(0, last(this.viewCellWidths)! - viewportWidth); - const maxScrollY = Math.max( - 0, - this.viewCellHeights.getCellOffsetY(cellRange.end + 1) - - this.viewCellHeights.getCellOffsetY(cellRange.start) - - viewportHeight, - ); - // remove previous split line group this.foregroundGroup.getElementById(KEY_GROUP_FROZEN_SPLIT_LINE)?.remove(); @@ -461,6 +445,54 @@ export abstract class FrozenFacet extends BaseFacet { opacity: splitLine?.horizontalBorderColorOpacity, }; + this.renderFrozenColSplitLine( + splitLineGroup, + splitLine, + verticalBorderStyle, + scrollX, + ); + + this.renderFrozenTrailingColSplitLine( + splitLineGroup, + splitLine, + verticalBorderStyle, + scrollX, + ); + this.renderFrozenRowSplitLine( + splitLineGroup, + splitLine, + horizontalBorderStyle, + relativeScrollY, + ); + + this.renderFrozenTrailingRowSplitLine( + splitLineGroup, + splitLine, + horizontalBorderStyle, + relativeScrollY, + ); + }; + + protected getFrozenColSplitLineSize() { + const { viewportHeight, y: panelBBoxStartY } = this.panelBBox; + + const height = viewportHeight + panelBBoxStartY; + + return { + y: 0, + height, + }; + } + + protected renderFrozenColSplitLine( + splitLineGroup: Group, + splitLine: SplitLine, + verticalBorderStyle: Partial, + scrollX: number, + ) { + const { colCount } = this.getFrozenOptions(); + const { x: panelBBoxStartX } = this.panelBBox; + if (colCount > 0) { const cornerWidth = this.cornerBBox.width; const colOffset = getFrozenColOffset(this, cornerWidth, scrollX); @@ -469,14 +501,14 @@ export abstract class FrozenFacet extends BaseFacet { this.frozenGroupAreas[FrozenGroupArea.Col].width - colOffset; - const height = viewportHeight + panelBBoxStartY; + const { y, height } = this.getFrozenColSplitLineSize(); renderLine(splitLineGroup, { ...verticalBorderStyle, x1: x, x2: x, - y1: 0, - y2: height, + y1: y, + y2: y + height, }); if ( @@ -488,7 +520,7 @@ export abstract class FrozenFacet extends BaseFacet { new Rect({ style: { x, - y: 0, + y, width: splitLine?.shadowWidth!, height, fill: this.getShadowFill(0), @@ -497,6 +529,16 @@ export abstract class FrozenFacet extends BaseFacet { ); } } + } + + protected renderFrozenTrailingColSplitLine( + splitLineGroup: Group, + splitLine: SplitLine, + verticalBorderStyle: Partial, + scrollX: number, + ) { + const { trailingColCount } = this.getFrozenOptions(); + const { viewportWidth, x: panelBBoxStartX } = this.panelBBox; if (trailingColCount > 0) { const x = @@ -504,14 +546,19 @@ export abstract class FrozenFacet extends BaseFacet { this.frozenGroupAreas[FrozenGroupArea.TrailingCol].width + panelBBoxStartX; - const height = viewportHeight + panelBBoxStartY; + const { y, height } = this.getFrozenColSplitLineSize(); + + const maxScrollX = Math.max( + 0, + last(this.viewCellWidths)! - viewportWidth, + ); renderLine(splitLineGroup, { ...verticalBorderStyle, x1: x, x2: x, - y1: 0, - y2: height, + y1: y, + y2: y + height, }); if (splitLine?.showShadow && floor(scrollX) < floor(maxScrollX)) { @@ -519,7 +566,7 @@ export abstract class FrozenFacet extends BaseFacet { new Rect({ style: { x: x - splitLine.shadowWidth!, - y: 0, + y, width: splitLine.shadowWidth!, height, fill: this.getShadowFill(180), @@ -528,25 +575,45 @@ export abstract class FrozenFacet extends BaseFacet { ); } } + } + + protected getFrozenRowSplitLineSize() { + const { viewportWidth, x: panelBBoxStartX } = this.panelBBox; + const width = panelBBoxStartX + viewportWidth; + + return { + x: 0, + width, + }; + } + + protected renderFrozenRowSplitLine( + splitLineGroup: Group, + splitLine: SplitLine, + horizontalBorderStyle: Partial, + scrollY: number, + ) { + const { rowCount } = this.getFrozenOptions(); + const { y: panelBBoxStartY } = this.panelBBox; if (rowCount > 0) { const y = panelBBoxStartY + this.frozenGroupAreas[FrozenGroupArea.Row].height; - const width = panelBBoxStartX + viewportWidth; + const { x, width } = this.getFrozenRowSplitLineSize(); renderLine(splitLineGroup, { ...horizontalBorderStyle, - x1: 0, - x2: width, + x1: x, + x2: x + width, y1: y, y2: y, }); - if (splitLine?.showShadow && relativeScrollY > 0) { + if (splitLine?.showShadow && scrollY > 0) { splitLineGroup.appendChild( new Rect({ style: { - x: 0, + x, y, width, height: splitLine?.shadowWidth!, @@ -556,26 +623,46 @@ export abstract class FrozenFacet extends BaseFacet { ); } } + } + + protected renderFrozenTrailingRowSplitLine( + splitLineGroup: Group, + splitLine: SplitLine, + horizontalBorderStyle: Partial, + scrollY: number, + ) { + const { trailingRowCount } = this.getFrozenOptions(); + const { viewportHeight } = this.panelBBox; if (trailingRowCount > 0) { const y = this.panelBBox.maxY - this.frozenGroupAreas[FrozenGroupArea.TrailingRow].height; - const width = panelBBoxStartX + viewportWidth; + + const { x, width } = this.getFrozenRowSplitLineSize(); + + const cellRange = this.getCellRange(); + // scroll boundary + const maxScrollY = Math.max( + 0, + this.viewCellHeights.getCellOffsetY(cellRange.end + 1) - + this.viewCellHeights.getCellOffsetY(cellRange.start) - + viewportHeight, + ); renderLine(splitLineGroup, { ...horizontalBorderStyle, - x1: 0, - x2: width, + x1: x, + x2: x + width, y1: y, y2: y, }); - if (splitLine?.showShadow && relativeScrollY < floor(maxScrollY)) { + if (splitLine?.showShadow && scrollY < floor(maxScrollY)) { splitLineGroup.appendChild( new Rect({ style: { - x: 0, + x, y: y - splitLine.shadowWidth!, width, height: splitLine.shadowWidth!, @@ -585,7 +672,7 @@ export abstract class FrozenFacet extends BaseFacet { ); } } - }; + } public render() { this.calculateFrozenGroupInfo(); diff --git a/packages/s2-core/src/facet/header/base.ts b/packages/s2-core/src/facet/header/base.ts index ed1eb601c9..4fe9e0dd4a 100644 --- a/packages/s2-core/src/facet/header/base.ts +++ b/packages/s2-core/src/facet/header/base.ts @@ -44,7 +44,7 @@ export abstract class BaseHeader extends Group { } // start render header - public render(type: string): void { + public render(type?: string): void { // clear resize group this.clearResizeAreaGroup(type); // clear self first @@ -63,7 +63,7 @@ export abstract class BaseHeader extends Group { * @param scrollY hScrollBar vertical offset * @param type */ - public onScrollXY(scrollX: number, scrollY: number, type: string): void { + public onScrollXY(scrollX: number, scrollY: number, type?: string): void { this.headerConfig.scrollX = scrollX; this.headerConfig.scrollY = scrollY; this.render(type); @@ -74,7 +74,7 @@ export abstract class BaseHeader extends Group { * @param rowHeaderScrollX hRowScrollbar horizontal offset * @param type */ - public onRowScrollX(rowHeaderScrollX: number, type: string): void { + public onRowScrollX(rowHeaderScrollX: number, type?: string): void { this.headerConfig.scrollX = rowHeaderScrollX; this.render(type); } @@ -83,7 +83,11 @@ export abstract class BaseHeader extends Group { * 清空热区,为重绘做准备,防止热区重复渲染 * @param type 当前重绘的header类型 */ - protected clearResizeAreaGroup(type: string) { + protected clearResizeAreaGroup(type?: string) { + if (!type) { + return; + } + const foregroundGroup = this.parentNode as Group; const resizerGroup = foregroundGroup?.getElementById(type); diff --git a/packages/s2-core/src/facet/header/col.ts b/packages/s2-core/src/facet/header/col.ts index 79754c95f5..2ef58f91ef 100644 --- a/packages/s2-core/src/facet/header/col.ts +++ b/packages/s2-core/src/facet/header/col.ts @@ -12,7 +12,7 @@ import { } from '../../common/constant'; import type { FrozenFacet } from '../frozen-facet'; import type { Node } from '../layout/node'; -import { translateGroupX } from '../utils'; +import { translateGroup } from '../utils'; import { BaseHeader } from './base'; import type { ColHeaderConfig } from './interface'; import { @@ -110,7 +110,7 @@ export class ColHeader extends BaseHeader { * @param cornerWidth only has real meaning when scroll contains rowCell * @param type */ - public onColScroll(scrollX: number, type: string) { + public onColScroll(scrollX: number, type?: string) { if (this.headerConfig.scrollX !== scrollX) { this.headerConfig.scrollX = scrollX; this.render(type); @@ -207,14 +207,18 @@ export class ColHeader extends BaseHeader { cornerWidth, } = this.getHeaderConfig(); - translateGroupX(this.scrollGroup, position.x - scrollX); + translateGroup(this.scrollGroup, position.x - scrollX, position.y); const facet = spreadsheet.facet as FrozenFacet; const colOffset = getFrozenColOffset(facet, cornerWidth, scrollX); const trailingColOffset = getFrozenTrailingColOffset(facet, viewportWidth); - translateGroupX(this.frozenGroup, position.x - colOffset); - translateGroupX(this.frozenTrailingGroup, position.x - trailingColOffset); + translateGroup(this.frozenGroup, position.x - colOffset, position.y); + translateGroup( + this.frozenTrailingGroup, + position.x - trailingColOffset, + position.y, + ); } } diff --git a/packages/s2-core/src/facet/header/corner.ts b/packages/s2-core/src/facet/header/corner.ts index 386e0e6baf..b58dd2f708 100644 --- a/packages/s2-core/src/facet/header/corner.ts +++ b/packages/s2-core/src/facet/header/corner.ts @@ -3,10 +3,11 @@ import { includes } from 'lodash'; import { CornerCell } from '../../cell/corner-cell'; import { S2Event } from '../../common'; import { CornerNodeType } from '../../common/interface/node'; +import type { SpreadSheet } from '../../sheet-type'; import type { CornerBBox } from '../bbox/corner-bbox'; import type { PanelBBox } from '../bbox/panel-bbox'; import { Node } from '../layout/node'; -import { translateGroupX } from '../utils'; +import { translateGroup } from '../utils'; import { FRONT_GROUND_GROUP_SCROLL_Z_INDEX, KEY_GROUP_CORNER_SCROLL, @@ -89,8 +90,7 @@ export class CornerHeader extends BaseHeader { }); } - public static getTreeCornerText(options: BaseCornerOptions) { - const { spreadsheet } = options; + public static getTreeCornerText(spreadsheet: SpreadSheet) { const { rows = [] } = spreadsheet.dataSet.fields; const { cornerText: defaultCornerText } = spreadsheet.options; @@ -153,7 +153,7 @@ export class CornerHeader extends BaseHeader { } if (spreadsheet.isHierarchyTreeType()) { - const cornerText = this.getTreeCornerText(options); + const cornerText = this.getTreeCornerText(spreadsheet); const cornerNode: Node = new Node({ id: cornerText, field: '', @@ -191,7 +191,9 @@ export class CornerHeader extends BaseHeader { cornerNode.x = rowNode.x + seriesNumberWidth; cornerNode.y = leafNode?.y ?? 0; cornerNode.width = rowNode.width; - cornerNode.height = leafNode?.height! ?? (colCell?.height as number); + cornerNode.height = + leafNode?.height! ?? + spreadsheet.facet.getCellCustomSize(null, colCell?.height); cornerNode.isPivotMode = true; cornerNode.cornerType = CornerNodeType.Row; cornerNode.spreadsheet = spreadsheet; @@ -234,7 +236,7 @@ export class CornerHeader extends BaseHeader { * Make cornerHeader scroll with hScrollBar * @param scrollX */ - public onCorScroll(scrollX: number, type: string): void { + public onCorScroll(scrollX: number, type?: string): void { this.headerConfig.scrollX = scrollX; this.render(type); } @@ -259,18 +261,18 @@ export class CornerHeader extends BaseHeader { } protected offset() { - const { scrollX = 0 } = this.getHeaderConfig(); + const { position, scrollX = 0 } = this.getHeaderConfig(); - translateGroupX(this.scrollGroup, -scrollX); + translateGroup(this.scrollGroup, position.x - scrollX, position.y); } protected clip(): void { - const { width, height } = this.getHeaderConfig(); + const { width, height, position } = this.getHeaderConfig(); this.scrollGroup.style.clipPath = new Rect({ style: { - x: 0, - y: 0, + x: position.x, + y: position.y, width, height, }, diff --git a/packages/s2-core/src/facet/header/frame.ts b/packages/s2-core/src/facet/header/frame.ts index 39d69b618b..53ad65bda7 100644 --- a/packages/s2-core/src/facet/header/frame.ts +++ b/packages/s2-core/src/facet/header/frame.ts @@ -87,13 +87,22 @@ export class Frame extends Group { this.render(); } - private addCornerRightBorder() { - const { cornerWidth, cornerHeight, viewportHeight, position, spreadsheet } = - this.cfg; + protected getCornerRightBorderSizeForPivotMode() { + const { cornerHeight, viewportHeight, position, spreadsheet } = this.cfg; + + const { horizontalBorderWidth } = spreadsheet.theme?.splitLine!; + + const y = position.y; + const height = cornerHeight + horizontalBorderWidth! + viewportHeight; + + return { y, height }; + } + + protected addCornerRightHeadBorder() { + const { cornerWidth, cornerHeight, position, spreadsheet } = this.cfg; const { verticalBorderColor, verticalBorderColorOpacity } = spreadsheet.theme?.splitLine!; const frameVerticalWidth = Frame.getVerticalBorderWidth(spreadsheet); - const frameHorizontalWidth = Frame.getHorizontalBorderWidth(spreadsheet); const x = position.x + cornerWidth + frameVerticalWidth! / 2; // 表头和表身的单元格背景色不同, 分割线不能一条线拉通, 不然视觉不协调. @@ -129,6 +138,20 @@ export class Frame extends Group { strokeOpacity, }); }); + } + + protected addCornerRightBorder() { + const { cornerWidth, cornerHeight, viewportHeight, position, spreadsheet } = + this.cfg; + const { verticalBorderColor, verticalBorderColorOpacity } = + spreadsheet.theme?.splitLine!; + const frameVerticalWidth = Frame.getVerticalBorderWidth(spreadsheet); + const frameHorizontalWidth = Frame.getHorizontalBorderWidth(spreadsheet); + const x = position.x + cornerWidth + frameVerticalWidth! / 2; + + // 表头和表身的单元格背景色不同, 分割线不能一条线拉通, 不然视觉不协调. + // 分两条线绘制, 默认和分割线所在区域对应的单元格边框颜色保持一致 + this.addCornerRightHeadBorder(); const { verticalBorderColor: cellVerticalBorderColor, @@ -160,7 +183,7 @@ export class Frame extends Group { }); } - private addCornerBottomBorder() { + protected addCornerBottomBorder() { const cfg = this.cfg; const { cornerWidth, @@ -205,7 +228,7 @@ export class Frame extends Group { }); } - private addSplitLineShadow() { + protected addSplitLineShadow() { const cfg = this.cfg; const { spreadsheet } = cfg; const splitLine = spreadsheet.theme?.splitLine; @@ -222,7 +245,7 @@ export class Frame extends Group { this.addSplitLineRightShadow(); } - private addSplitLineLeftShadow() { + protected addSplitLineLeftShadow() { if (!this.cfg.showViewportLeftShadow) { return; } @@ -248,28 +271,21 @@ export class Frame extends Group { ); } - private addSplitLineRightShadow() { + protected addSplitLineRightShadow() { if (!this.cfg.showViewportRightShadow) { return; } - const { - cornerWidth, - cornerHeight, - viewportHeight, - viewportWidth, - position, - spreadsheet, - } = this.cfg; - const { shadowColors, shadowWidth, horizontalBorderWidth } = - spreadsheet.theme?.splitLine!; + const { cornerWidth, viewportWidth, position, spreadsheet } = this.cfg; + const { shadowColors, shadowWidth } = spreadsheet.theme?.splitLine!; const x = position.x + cornerWidth + Frame.getVerticalBorderWidth(spreadsheet)! + viewportWidth - shadowWidth!; - const y = position.y; + + const { y, height } = this.getCornerRightBorderSizeForPivotMode(); this.appendChild( new Rect({ @@ -277,7 +293,7 @@ export class Frame extends Group { x, y, width: shadowWidth!, - height: cornerHeight + horizontalBorderWidth! + viewportHeight, + height, fill: `l (0) 0:${shadowColors?.right} 1:${shadowColors?.left}`, }, }), diff --git a/packages/s2-core/src/facet/header/index.ts b/packages/s2-core/src/facet/header/index.ts index 3001a3417d..00ae2b6947 100644 --- a/packages/s2-core/src/facet/header/index.ts +++ b/packages/s2-core/src/facet/header/index.ts @@ -5,3 +5,4 @@ export { RowHeader } from './row'; export { SeriesNumberHeader } from './series-number'; export * from './interface'; +export * from './util'; diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index b221f7cfdd..88774c00f3 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -5,8 +5,8 @@ import { FRONT_GROUND_GROUP_FROZEN_Z_INDEX, FRONT_GROUND_GROUP_SCROLL_Z_INDEX, FrozenGroupArea, - KEY_GROUP_ROW_HEADER_FROZEN, - KEY_GROUP_ROW_HEADER_FROZEN_TRAILING, + KEY_GROUP_ROW_FROZEN, + KEY_GROUP_ROW_FROZEN_TRAILING, KEY_GROUP_ROW_SCROLL, S2Event, } from '../../common'; @@ -31,13 +31,13 @@ export class RowHeader extends BaseHeader { this.frozenGroup = this.appendChild( new Group({ - name: KEY_GROUP_ROW_HEADER_FROZEN, + name: KEY_GROUP_ROW_FROZEN, style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, }), ); this.frozenTrailingGroup = this.appendChild( new Group({ - name: KEY_GROUP_ROW_HEADER_FROZEN_TRAILING, + name: KEY_GROUP_ROW_FROZEN_TRAILING, style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, }), ); diff --git a/packages/s2-core/src/facet/index.ts b/packages/s2-core/src/facet/index.ts index 432f387965..7ad8b0873c 100644 --- a/packages/s2-core/src/facet/index.ts +++ b/packages/s2-core/src/facet/index.ts @@ -3,5 +3,9 @@ export { FrozenFacet } from './frozen-facet'; export { PivotFacet } from './pivot-facet'; export { TableFacet } from './table-facet'; +export * from './bbox/corner-bbox'; +export * from './bbox/panel-bbox'; + export * from './header'; export * from './layout'; +export * from './utils'; diff --git a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts index eeae45bc32..c0a2517d94 100644 --- a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts @@ -107,7 +107,6 @@ const buildNormalGridHierarchy = (params: GridHeaderParams) => { fieldValues.push(...((arrangedValues as FieldValue[]) || [])); // add skeleton for empty data - if (isEmpty(fieldValues) && currentField) { if (currentField === EXTRA_FIELD) { fieldValues.push(...values); diff --git a/packages/s2-core/src/facet/layout/build-header-hierarchy.ts b/packages/s2-core/src/facet/layout/build-header-hierarchy.ts index 5a6732d0e3..4f1e5613ae 100644 --- a/packages/s2-core/src/facet/layout/build-header-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-header-hierarchy.ts @@ -5,17 +5,16 @@ import { type CustomTreeNode, } from '../../common'; import type { PivotDataSet } from '../../data-set'; +import type { + BuildHeaderParams, + BuildHeaderResult, + HeaderParams, +} from '../layout/interface'; import { buildGridHierarchy } from './build-gird-hierarchy'; import { buildCustomTreeHierarchy } from './build-row-custom-tree-hierarchy'; import { buildRowTreeHierarchy } from './build-row-tree-hierarchy'; import { buildTableHierarchy } from './build-table-hierarchy'; import { Hierarchy } from './hierarchy'; -import type { - BuildHeaderParams, - BuildHeaderResult, - HeaderParams, -} from './interface'; -import { Node } from './node'; const handleCustomTreeHierarchy = (params: HeaderParams) => { const { rootNode, hierarchy, fields, spreadsheet, isRowHeader } = params; @@ -168,7 +167,6 @@ export const buildHeaderHierarchy = ( const { rows = [], columns = [] } = spreadsheet.dataSet.fields; const isValueInCols = spreadsheet.isValueInCols(); const moreThanOneValue = spreadsheet.dataSet.moreThanOneValue(); - const rootNode = Node.rootNode(); const hierarchy = new Hierarchy(); const fields = isRowHeader ? rows : columns; const isCustomTreeFields = spreadsheet.isCustomHeaderFields( @@ -178,7 +176,7 @@ export const buildHeaderHierarchy = ( const headerParams: HeaderParams = { isValueInCols, moreThanOneValue, - rootNode, + rootNode: hierarchy.rootNode, hierarchy, spreadsheet, fields, diff --git a/packages/s2-core/src/facet/layout/hierarchy.ts b/packages/s2-core/src/facet/layout/hierarchy.ts index 876b4eae07..809560de3e 100644 --- a/packages/s2-core/src/facet/layout/hierarchy.ts +++ b/packages/s2-core/src/facet/layout/hierarchy.ts @@ -1,4 +1,4 @@ -import type { Node } from './node'; +import { Node } from './node'; /** * Row and Column hierarchy to handle all contained nodes @@ -16,6 +16,8 @@ export class Hierarchy { // just a mark to get node from each level public maxLevel = -1; + public rootNode: Node; + // each level's first node public sampleNodesForAllLevels: Node[] = []; @@ -23,10 +25,16 @@ export class Hierarchy { public sampleNodeForLastLevel: Node | null = null; // all nodes in this hierarchy - private allNodesWithoutRoot: Node[] = []; + public allNodesWithoutRoot: Node[] = []; // all nodes in the lastLevel - private indexNode: Node[] = []; + public indexNode: Node[] = []; + + public isPlaceholder = false; + + constructor() { + this.rootNode = Node.rootNode(); + } // get all leaf nodes public getLeaves(): Node[] { diff --git a/packages/s2-core/src/facet/layout/node.ts b/packages/s2-core/src/facet/layout/node.ts index 6e396bb895..4f4bd01592 100644 --- a/packages/s2-core/src/facet/layout/node.ts +++ b/packages/s2-core/src/facet/layout/node.ts @@ -1,4 +1,4 @@ -import { head, isEmpty } from 'lodash'; +import { assign, head, isEmpty } from 'lodash'; import { SERIES_NUMBER_FIELD } from '../../common'; import { ROOT_NODE_ID } from '../../common/constant/node'; import type { @@ -10,46 +10,11 @@ import type { Query } from '../../data-set'; import type { SpreadSheet } from '../../sheet-type'; import type { Hierarchy } from './hierarchy'; -export interface BaseNodeConfig { - /** - * id 只在行头、列头 node 以及 hierarchy 中有用,是当前 node query 的拼接产物 - */ - id: string; - - /** - * 当前 node 的 field 属性, 在角头、行列头中 node 使用,和 dataCfg.fields 对应 - */ - field: string; - value: string; - level?: number; - rowIndex?: number; - colIndex?: number; - parent?: Node; - isTotals?: boolean; - isSubTotals?: boolean; - isCollapsed?: boolean | null; - isGrandTotals?: boolean; - isTotalRoot?: boolean; - hierarchy?: Hierarchy; - isPivotMode?: boolean; - seriesNumberWidth?: number; - spreadsheet?: SpreadSheet; - query?: Record; - belongsCell?: S2CellType; - isTotalMeasure?: boolean; - inCollapseNode?: boolean; - isLeaf?: boolean; - x?: number; - y?: number; - width?: number; - height?: number; - padding?: number; - children?: Node[]; - hiddenColumnsInfo?: HiddenColumnsInfo | null; - // 额外的节点信息 - extra?: Record; -} - +export type NodeProperties = { + [K in keyof Node as Node[K] extends (...args: any[]) => any + ? never + : K]?: Node[K]; +}; /** * Node for cornerHeader, colHeader, rowHeader */ @@ -84,7 +49,7 @@ export class Node { public rowIndex: number; // node's parent node - public parent: Node | undefined; + public parent?: Node; // check if node is leaf(the max level in tree) public isLeaf = false; @@ -92,13 +57,11 @@ export class Node { // node is grand total or subtotal(not normal node) public isTotals: boolean; - public colId: string; - // node represent total measure public isTotalMeasure: boolean; // node is collapsed - public isCollapsed: boolean; + public isCollapsed: boolean | null; // node's children public children: Node[] = []; @@ -116,7 +79,8 @@ export class Node { public seriesNumberWidth: number; /** - * 给序号列单元格用,标识该序号单元格对应了行头节点,有了关联关系后,就可以在行头冻结时做区分 + * 1. 给序号列单元格用,标识该序号单元格对应了行头节点,有了关联关系后,就可以在行头冻结时做区分 + * 2. 给 pivot chart sheet 用,关联当前格子和拆分的 axis 的格子 */ public relatedNode: Node; @@ -126,10 +90,12 @@ export class Node { // node self's query condition(represent where node stay) public query?: Query; - public belongsCell?: S2CellType | null | undefined; + public belongsCell?: S2CellType | null; public inCollapseNode?: boolean; + public hiddenColumnsInfo?: HiddenColumnsInfo | null; + public cornerType?: CornerNodeType; public isGrandTotals?: boolean; @@ -146,6 +112,12 @@ export class Node { public shallowRender?: boolean; + /** 是否不绘制 col cell 水平 resize 热区 */ + public hideColCellHorizontalResize?: boolean; + + /** 是否不绘制 row cell 竖直 resize 热区 */ + public hideRowCellVerticalResize?: boolean; + public extra?: { description?: string; isCustomNode?: boolean; @@ -154,52 +126,8 @@ export class Node { [key: string]: any; - constructor(cfg: BaseNodeConfig) { - const { - id, - field, - value, - parent, - level, - rowIndex, - isTotals, - isGrandTotals, - isSubTotals, - isCollapsed, - isTotalRoot, - hierarchy, - isPivotMode, - seriesNumberWidth, - spreadsheet, - query, - belongsCell, - inCollapseNode, - isTotalMeasure, - isLeaf, - extra, - } = cfg; - - this.id = id; - this.field = field; - this.value = value; - this.parent = parent; - this.level = level!; - this.rowIndex = rowIndex!; - this.isTotals = isTotals!; - this.isCollapsed = isCollapsed!; - this.hierarchy = hierarchy!; - this.isPivotMode = isPivotMode!; - this.seriesNumberWidth = seriesNumberWidth!; - this.spreadsheet = spreadsheet!; - this.query = query; - this.inCollapseNode = inCollapseNode; - this.isTotalMeasure = isTotalMeasure!; - this.isLeaf = isLeaf!; - this.isGrandTotals = isGrandTotals; - this.isSubTotals = isSubTotals; - this.belongsCell = belongsCell; - this.isTotalRoot = isTotalRoot; - this.extra = extra; + constructor(cfg: NodeProperties) { + assign(this, cfg); } /** @@ -273,11 +201,16 @@ export class Node { * get a branch's all nodes(c1~c4, b1, b2) * @param node */ - public static getAllChildrenNodes(node: Node): Node[] { + public static getAllChildrenNodes( + node: Node, + push: (node: Node) => Node[] = (node) => [node], + ): Node[] { const all: Node[] = []; if (node.isLeaf) { - return [node]; + all.push(...push(node)); + + return all; } // current root node children @@ -285,7 +218,7 @@ export class Node { let current = nodes.shift(); while (current) { - all.push(current); + all.push(...push(current)); nodes.unshift(...current.children); current = nodes.shift(); } @@ -367,7 +300,7 @@ export class Node { } public clone() { - return Object.create(this) as Node; + return new Node({ ...this }); } public get isFrozen() { diff --git a/packages/s2-core/src/facet/layout/total-class.ts b/packages/s2-core/src/facet/layout/total-class.ts index 719687895f..1467cb620e 100644 --- a/packages/s2-core/src/facet/layout/total-class.ts +++ b/packages/s2-core/src/facet/layout/total-class.ts @@ -29,4 +29,8 @@ export class TotalClass { this.isGrandTotals = isGrandTotals; this.isTotalRoot = isTotalRoot; } + + static isTotalClassInstance(value: unknown): value is TotalClass { + return value instanceof TotalClass; + } } diff --git a/packages/s2-core/src/facet/layout/total-measure.ts b/packages/s2-core/src/facet/layout/total-measure.ts index 3d341d78b9..f5447b5740 100644 --- a/packages/s2-core/src/facet/layout/total-measure.ts +++ b/packages/s2-core/src/facet/layout/total-measure.ts @@ -4,4 +4,8 @@ export class TotalMeasure { public constructor(label: string) { this.label = label; } + + static isTotalMeasureInstance(value: unknown): value is TotalMeasure { + return value instanceof TotalMeasure; + } } diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index 1960a57af9..3a0f4bdace 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -43,7 +43,7 @@ import { getAllChildCells } from '../utils/get-all-child-cells'; import { floor } from '../utils/math'; import { getCellWidth } from '../utils/text'; import { FrozenFacet } from './frozen-facet'; -import { Frame } from './header'; +import { CornerHeader, Frame } from './header'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; import type { Hierarchy } from './layout/hierarchy'; import { layoutCoordinate } from './layout/layout-hooks'; @@ -83,7 +83,7 @@ export class PivotFacet extends FrozenFacet { }; } - private buildAllHeaderHierarchy() { + protected buildAllHeaderHierarchy() { const { leafNodes: rowLeafNodes, hierarchy: rowsHierarchy } = buildHeaderHierarchy({ isRowHeader: true, @@ -185,7 +185,7 @@ export class PivotFacet extends FrozenFacet { return options.layoutCellMeta?.(cellMeta) ?? cellMeta; } - private getPreLevelSampleNode(colNode: Node, colsHierarchy: Hierarchy) { + protected getPreLevelSampleNode(colNode: Node, colsHierarchy: Hierarchy) { // 之前是采样每一级第一个节点, 现在 sampleNodesForAllLevels 是采样每一级高度最大的节点 // 但是初始化布局时只有第一个节点有值, 所以这里需要适配下 return colsHierarchy @@ -193,12 +193,12 @@ export class PivotFacet extends FrozenFacet { .find((node) => !node.isTotals); } - private calculateHeaderNodesCoordinate(layoutResult: LayoutResult) { + protected calculateHeaderNodesCoordinate(layoutResult: LayoutResult) { this.calculateRowNodesCoordinate(layoutResult); this.calculateColNodesCoordinate(layoutResult); } - private calculateColNodesCoordinate(layoutResult: LayoutResult) { + protected calculateColNodesCoordinate(layoutResult: LayoutResult) { const { colLeafNodes, colsHierarchy } = layoutResult; // 1. 计算叶子节点宽度 @@ -222,7 +222,7 @@ export class PivotFacet extends FrozenFacet { protected calculateRowOffsets(): void {} - private adjustColTotalNodesCoordinate(colsHierarchy: Hierarchy) { + protected adjustColTotalNodesCoordinate(colsHierarchy: Hierarchy) { if (!isEmpty(this.spreadsheet.options.totals?.col)) { this.adjustTotalNodesCoordinate({ hierarchy: colsHierarchy, @@ -237,7 +237,7 @@ export class PivotFacet extends FrozenFacet { } } - private calculateColLeafNodesWidth(layoutResult: LayoutResult) { + protected calculateColLeafNodesWidth(layoutResult: LayoutResult) { const { rowLeafNodes, colLeafNodes, rowsHierarchy, colsHierarchy } = layoutResult; let preLeafNode = Node.blankNode(); @@ -245,7 +245,7 @@ export class PivotFacet extends FrozenFacet { colsHierarchy.getLeaves().forEach((currentNode) => { currentNode.colIndex = currentColIndex; - currentColIndex += 1; + currentColIndex++; currentNode.x = preLeafNode.x + preLeafNode.width; currentNode.width = this.getColLeafNodesWidth( currentNode, @@ -258,7 +258,7 @@ export class PivotFacet extends FrozenFacet { }); } - private calculateColNodesHeight(colsHierarchy: Hierarchy) { + protected calculateColNodesHeight(colsHierarchy: Hierarchy) { const colNodes = colsHierarchy.getNodes(); colNodes.forEach((currentNode) => { @@ -292,7 +292,7 @@ export class PivotFacet extends FrozenFacet { } // please read README-adjustTotalNodesCoordinate.md to understand this function - private getMultipleMap( + protected getMultipleMap( hierarchy: Hierarchy, isRowHeader?: boolean, isSubTotal?: boolean, @@ -313,7 +313,7 @@ export class PivotFacet extends FrozenFacet { for (let level = maxLevel; level > 0; level--) { const currentField = fields![level] as string; - // 若不符合【分组维度包含此维度】或【者指标维度下非单指标维度】,此表头单元格为空,将宽高合并到上级单元格 + // 若不符合【分组维度包含此维度】或者【指标维度下多指标维度】,此表头单元格为空,将宽高合并到上级单元格 const existValueField = currentField === EXTRA_FIELD && moreThanOneValue; if (!(dimensionGroup.includes(currentField) || existValueField)) { @@ -326,7 +326,7 @@ export class PivotFacet extends FrozenFacet { } // please read README-adjustTotalNodesCoordinate.md to understand this function - private adjustTotalNodesCoordinate(params: { + protected adjustTotalNodesCoordinate(params: { hierarchy: Hierarchy; isRowHeader?: boolean; isSubTotal?: boolean; @@ -371,7 +371,7 @@ export class PivotFacet extends FrozenFacet { * Auto column no-leaf node's width and x coordinate * @param colLeafNodes */ - private calculateColNodeWidthAndX(colLeafNodes: Node[]) { + protected calculateColNodeWidthAndX(colLeafNodes: Node[]) { let prevColParent: Node | null = null; let i = 0; @@ -400,7 +400,7 @@ export class PivotFacet extends FrozenFacet { } } - private getColLeafNodesWidth( + protected getColLeafNodesWidth( colNode: Node, colLeafNodes: Node[], rowLeafNodes: Node[], @@ -436,10 +436,10 @@ export class PivotFacet extends FrozenFacet { } // 4.2 网格自定义 - return this.getAdaptGridColWidth(colLeafNodes, rowHeaderWidth); + return this.getAdaptGridColWidth(colLeafNodes, colNode, rowHeaderWidth); } - private getRowNodeHeight(rowNode: Node): number { + protected getRowNodeHeight(rowNode: Node): number { if (!rowNode) { return 0; } @@ -468,7 +468,7 @@ export class PivotFacet extends FrozenFacet { * @param iconStyle 图标样式 * @returns 宽度 */ - private getExpectedCellIconWidth( + protected getExpectedCellIconWidth( cellType: CellType, useDefaultIcon: boolean, iconStyle: IconTheme, @@ -505,7 +505,7 @@ export class PivotFacet extends FrozenFacet { : 0; } - private calculateRowNodesAllLevelSampleNodes(layoutResult: LayoutResult) { + protected calculateRowNodesAllLevelSampleNodes(layoutResult: LayoutResult) { const { rowsHierarchy, colLeafNodes } = layoutResult; const isTree = this.spreadsheet.isHierarchyTreeType(); @@ -528,16 +528,39 @@ export class PivotFacet extends FrozenFacet { levelSample.x = preLevelSample?.x + preLevelSample?.width; }); } + + rowsHierarchy.rootNode.width = rowsHierarchy.width; + } + + protected getRowLeafNodeHeight(rowLeafNode: Node) { + const { rowCell: rowCellStyle } = this.spreadsheet.options.style!; + const isEnableHeightAdaptive = + rowCellStyle?.maxLines! > 1 && rowCellStyle?.wordWrap; + + const currentBranchNodeHeights = isEnableHeightAdaptive + ? Node.getBranchNodes(rowLeafNode).map((rowNode) => + this.getRowNodeHeight(rowNode), + ) + : []; + + const defaultHeight = this.getRowNodeHeight(rowLeafNode); + // 父节点的高度是叶子节点的高度之和, 由于存在多行文本, 叶子节点的高度以当前路径下节点高度最大的为准: https://github.com/antvis/S2/issues/2678 + // 自定义高度除外: https://github.com/antvis/S2/issues/2594 + const nodeHeight = this.isCustomRowCellHeight(rowLeafNode) + ? defaultHeight + : max(currentBranchNodeHeights) ?? defaultHeight; + + return nodeHeight; } - private calculateRowNodesBBox(rowsHierarchy: Hierarchy) { + protected calculateRowNodesBBox(rowsHierarchy: Hierarchy) { const isTree = this.spreadsheet.isHierarchyTreeType(); const sampleNodeByLevel = rowsHierarchy.sampleNodesForAllLevels || []; let preLeafNode = Node.blankNode(); const rowNodes = rowsHierarchy.getNodes(); - rowNodes.forEach((currentNode, i) => { + rowNodes.forEach((currentNode) => { // 树状模式都按叶子处理节点 const isLeaf = isTree || (!isTree && currentNode.isLeaf); @@ -554,21 +577,11 @@ export class PivotFacet extends FrozenFacet { if (isLeaf) { // 2.1. 普通树状结构, 叶子节点各占一行, 2.2. 自定义树状结构 (平铺模式) const rowIndex = (preLeafNode?.rowIndex ?? -1) + 1; - const currentBranchNodeHeights = Node.getBranchNodes(currentNode).map( - (rowNode) => this.getRowNodeHeight(rowNode), - ); - - const defaultHeight = this.getRowNodeHeight(currentNode); - // 父节点的高度是叶子节点的高度之和, 由于存在多行文本, 叶子节点的高度以当前路径下节点高度最大的为准: https://github.com/antvis/S2/issues/2678 - // 自定义高度除外: https://github.com/antvis/S2/issues/2594 - const nodeHeight = this.isCustomRowCellHeight(currentNode) - ? defaultHeight - : max(currentBranchNodeHeights) ?? defaultHeight; + // 文本超过 1 行时再自适应单元格高度, 不然会频繁触发 GC, 导致性能降低: https://github.com/antvis/S2/issues/2693 currentNode.rowIndex ??= rowIndex; - currentNode.colIndex ??= i; currentNode.y = preLeafNode.y + preLeafNode.height; - currentNode.height = nodeHeight; + currentNode.height = this.getRowLeafNodeHeight(currentNode); preLeafNode = currentNode; // mark row hierarchy's height rowsHierarchy.height += currentNode.height; @@ -586,7 +599,7 @@ export class PivotFacet extends FrozenFacet { }); } - private calculateRowNodesCoordinate(layoutResult: LayoutResult) { + protected calculateRowNodesCoordinate(layoutResult: LayoutResult) { const { rowsHierarchy, rowLeafNodes } = layoutResult; const isTree = this.spreadsheet.isHierarchyTreeType(); @@ -609,7 +622,7 @@ export class PivotFacet extends FrozenFacet { } } - private adjustRowTotalNodesCoordinate(rowsHierarchy: Hierarchy) { + protected adjustRowTotalNodesCoordinate(rowsHierarchy: Hierarchy) { if (!isEmpty(this.spreadsheet.options.totals?.row)) { this.adjustTotalNodesCoordinate({ hierarchy: rowsHierarchy, @@ -628,7 +641,7 @@ export class PivotFacet extends FrozenFacet { * @description Auto calculate row no-leaf node's height and y coordinate * @param rowLeafNodes */ - private calculateRowNodeHeightAndY(rowLeafNodes: Node[]) { + protected calculateRowNodeHeightAndY(rowLeafNodes: Node[]) { // 3、in grid type, all no-leaf node's height, y are auto calculated let prevRowParent: Node | null = null; let i = 0; @@ -652,7 +665,7 @@ export class PivotFacet extends FrozenFacet { /** * 计算 grid 模式下 node 宽度 */ - private getGridRowNodesWidth(node: Node, colLeafNodes: Node[]): number { + protected getGridRowNodesWidth(node: Node, colLeafNodes: Node[]): number { const { rowCell } = this.spreadsheet.options.style!; const cellDraggedWidth = this.getRowCellDraggedWidth(node); @@ -679,7 +692,7 @@ export class PivotFacet extends FrozenFacet { /** * 计算树状模式等宽条件下的列宽 */ - private getAdaptTreeColWidth( + protected getAdaptTreeColWidth( col: Node, colLeafNodes: Node[], rowLeafNodes: Node[], @@ -704,7 +717,7 @@ export class PivotFacet extends FrozenFacet { ); } - private getColLabelLength(col: Node, rowLeafNodes: Node[]) { + protected getColLabelLength(col: Node, rowLeafNodes: Node[]) { // 如果 label 字段形如 "["xx","xxx"]",直接获取其长度 const labels = safeJsonParse(col?.value); @@ -756,7 +769,12 @@ export class PivotFacet extends FrozenFacet { /** * 计算平铺模式等宽条件下的列宽 */ - private getAdaptGridColWidth(colLeafNodes: Node[], rowHeaderWidth?: number) { + protected getAdaptGridColWidth( + colLeafNodes: Node[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + colNode?: Node, + rowHeaderWidth?: number, + ) { const { rows = [] } = this.spreadsheet.dataSet.fields; const { dataCell } = this.spreadsheet.options.style!; const rowHeaderColSize = rows.length; @@ -782,9 +800,8 @@ export class PivotFacet extends FrozenFacet { /** * 计算树状结构行头宽度 */ - private getTreeRowHeaderWidth(): number { + protected getTreeRowHeaderWidth(): number { const { rowCell } = this.spreadsheet.options.style!; - const { rows = [] } = this.spreadsheet.dataSet.fields; // 1. 用户拖拽或手动指定的行头宽度优先级最高 const customRowCellWidth = this.getCellCustomSize(null, rowCell?.width!); @@ -794,9 +811,7 @@ export class PivotFacet extends FrozenFacet { } // 2. 然后是计算 (+ icon province/city/level) - const treeHeaderLabel = rows - .map((field) => this.spreadsheet.dataSet.getFieldName(field)) - .join('/'); + const treeHeaderLabel = CornerHeader.getTreeCornerText(this.spreadsheet); const { bolderText: cornerCellTextStyle, icon: cornerIconStyle } = this.spreadsheet.theme.cornerCell!; @@ -830,7 +845,7 @@ export class PivotFacet extends FrozenFacet { * | label - icon | * | label - icon | */ - private getCompactGridRowNodeWidth(node: Node): number { + protected getCompactGridRowNodeWidth(node: Node): number { const { bolderText: rowTextStyle, icon: rowIconStyle, @@ -889,7 +904,7 @@ export class PivotFacet extends FrozenFacet { return Math.max(rowNodeWidth, fieldNameNodeWidth); } - private getCompactGridColNodeWidth(colNode: Node, rowLeafNodes: Node[]) { + protected getCompactGridColNodeWidth(colNode: Node, rowLeafNodes: Node[]) { const { bolderText: colCellTextStyle, cell: colCellStyle, diff --git a/packages/s2-core/src/index.ts b/packages/s2-core/src/index.ts index 11823b302d..b7b9384f7b 100644 --- a/packages/s2-core/src/index.ts +++ b/packages/s2-core/src/index.ts @@ -1,6 +1,7 @@ export { FederatedPointerEvent as GEvent } from '@antv/g'; - -export { getTheme } from './theme'; +export { buildTableHierarchy } from './facet/layout/build-table-hierarchy'; +export { Hierarchy } from './facet/layout/hierarchy'; +export { Node, type NodeProperties } from './facet/layout/node'; export * from './cell'; export * from './common'; @@ -9,5 +10,7 @@ export * from './facet'; export * from './interaction'; export * from './shared'; export * from './sheet-type'; +export * from './theme'; +export * from './ui/scrollbar'; export * from './ui/tooltip'; export * from './utils'; diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index 321639fc34..0615e9839f 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -90,6 +90,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { InteractionStateName.SELECTED, meta, ); + this.spreadsheet.emit(S2Event.DATA_CELL_CLICK_TRIGGERED_PRIVATE, cell); }); } diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts index 67d0819c33..51487aef9e 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts @@ -34,7 +34,7 @@ import { import type { ViewMeta } from './../../../common/interface/basic'; export class RowColumnClick extends BaseEvent implements BaseEventImplement { - private isMultiSelection = false; + protected isMultiSelection = false; public bindEvents() { this.bindKeyboardDown(); @@ -50,7 +50,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { this.spreadsheet.interaction.removeIntercepts([InterceptType.CLICK]); } - private bindKeyboardDown() { + protected bindKeyboardDown() { this.spreadsheet.on( S2Event.GLOBAL_KEYBOARD_DOWN, (event: KeyboardEvent) => { @@ -61,7 +61,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { ); } - private bindKeyboardUp() { + protected bindKeyboardUp() { this.spreadsheet.on(S2Event.GLOBAL_KEYBOARD_UP, (event: KeyboardEvent) => { if (isMultiSelectionKey(event)) { this.reset(); @@ -69,7 +69,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { }); } - private bindMouseMove() { + protected bindMouseMove() { this.spreadsheet.on(S2Event.GLOBAL_MOUSE_MOVE, (event) => { // 当快捷键被系统拦截后,按需补充调用一次 reset if (this.isMultiSelection && !isMouseEventWithMeta(event)) { @@ -78,19 +78,19 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { }); } - private bindRowCellClick() { + protected bindRowCellClick() { this.spreadsheet.on(S2Event.ROW_CELL_CLICK, (event: CanvasEvent) => { this.handleRowColClick(event); }); } - private bindColCellClick() { + protected bindColCellClick() { this.spreadsheet.on(S2Event.COL_CELL_CLICK, (event: CanvasEvent) => { this.handleRowColClick(event); }); } - private handleRowColClick = (event: CanvasEvent) => { + protected handleRowColClick = (event: CanvasEvent) => { event.stopPropagation(); if (this.isLinkFieldText(event.target)) { @@ -132,7 +132,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { } }; - private showTooltip(event: CanvasEvent) { + protected showTooltip(event: CanvasEvent) { const { operation, enable: showTooltip } = getTooltipOptions( this.spreadsheet, event, @@ -155,7 +155,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { }); } - private getTooltipOperator( + protected getTooltipOperator( event: CanvasEvent, operation: TooltipOperation, ): TooltipOperatorOptions { @@ -191,13 +191,13 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { }); } - private bindTableColExpand() { + protected bindTableColExpand() { this.spreadsheet.on(S2Event.COL_CELL_EXPANDED, (node) => { this.handleExpandIconClick(node); }); } - private getHideColumnField = (node: Node | ViewMeta) => { + protected getHideColumnField = (node: Node | ViewMeta) => { if ((node as Node).extra?.isCustomNode) { return node.id; } @@ -230,7 +230,7 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { await hideColumnsByThunkGroup(this.spreadsheet, selectedColumnFields, true); } - private async handleExpandIconClick(node: Node) { + protected async handleExpandIconClick(node: Node) { const lastHiddenColumnsDetail = this.spreadsheet.store.get( 'hiddenColumnsDetail', [], diff --git a/packages/s2-core/src/interaction/base-interaction/hover.ts b/packages/s2-core/src/interaction/base-interaction/hover.ts index 663af4dad1..07c66584f1 100644 --- a/packages/s2-core/src/interaction/base-interaction/hover.ts +++ b/packages/s2-core/src/interaction/base-interaction/hover.ts @@ -1,5 +1,6 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; import { isBoolean, isEmpty } from 'lodash'; +import { DataCell } from '../../cell'; import { S2Event } from '../../common/constant'; import { HOVER_FOCUS_DURATION, @@ -25,7 +26,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { this.bindHeaderCellHover(); } - private changeStateToHoverFocus(cell: S2CellType, event: CanvasEvent) { + private changeStateToHoverFocus(cell: DataCell, event: CanvasEvent) { if (!cell) { return; } @@ -60,6 +61,8 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { ); } + this.spreadsheet.emit(S2Event.DATA_CELL_HOVER_TRIGGERED_PRIVATE, cell); + const data = this.getCellData(meta, onlyShowCellText); this.spreadsheet.showTooltipWithInfo(event, data, options); @@ -85,7 +88,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { * @description handle the row or column header hover state * @param event */ - private handleHeaderHover(event: CanvasEvent) { + protected handleHeaderHover(event: CanvasEvent) { const cell = this.spreadsheet.getCell(event.target) as S2CellType; if (isEmpty(cell)) { @@ -163,21 +166,22 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { public bindDataCellHover() { this.spreadsheet.on(S2Event.DATA_CELL_HOVER, (event: CanvasEvent) => { // FIXME: 趋势分析表 hover 的时候拿到的 event target 是错误的 - const cell = this.spreadsheet.getCell(event.target); + const cell = this.spreadsheet.getCell(event.target); if (isEmpty(cell)) { return; } const { interaction, options } = this.spreadsheet; - const { interaction: interactionOptions } = options; - const meta = cell?.getMeta() as ViewMeta; // 避免在同一单元格内鼠标移动造成的多次渲染 if (interaction.isActiveCell(cell)) { return; } + const { interaction: interactionOptions } = options; + const meta = cell?.getMeta() as ViewMeta; + interaction.changeState({ cells: [getCellMeta(cell)], stateName: InteractionStateName.HOVER, @@ -193,6 +197,8 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { if (interactionOptions?.hoverFocus) { this.changeStateToHoverFocus(cell, event); } + + this.spreadsheet.emit(S2Event.DATA_CELL_HOVER_TRIGGERED_PRIVATE, cell); }); } diff --git a/packages/s2-core/src/interaction/event-controller.ts b/packages/s2-core/src/interaction/event-controller.ts index c42ae9ea4e..595f8968ef 100644 --- a/packages/s2-core/src/interaction/event-controller.ts +++ b/packages/s2-core/src/interaction/event-controller.ts @@ -21,7 +21,6 @@ import type { SpreadSheet } from '../sheet-type'; import { getSelectedData } from '../utils/export/copy'; import { keyEqualTo } from '../utils/export/method'; import { getAppendInfo } from '../utils/interaction/common'; -import { isMobile } from '../utils/is-mobile'; import { verifyTheElementInTooltip } from '../utils/tooltip'; interface EventListener { @@ -78,7 +77,6 @@ export class EventController { this.clearAllEvents(); // canvas events - this.addCanvasEvent(OriginEventType.CLICK, this.onCanvasClick); this.addCanvasEvent(OriginEventType.MOUSE_DOWN, this.onCanvasMousedown); this.addCanvasEvent(OriginEventType.TOUCH_START, (event) => { this.target = event.target; @@ -86,6 +84,7 @@ export class EventController { this.addCanvasEvent(OriginEventType.POINTER_MOVE, this.onCanvasMousemove); this.addCanvasEvent(OriginEventType.MOUSE_OUT, this.onCanvasMouseout); this.addCanvasEvent(OriginEventType.POINTER_UP, this.onCanvasMouseup); + this.addCanvasEvent(OriginEventType.CLICK, this.onCanvasDoubleClick); /** * 如果监听 G Canvas, 右键对应的是 rightup/rightdown 事件, 如需禁用右键菜单 (preventDefault), 需要监听 DOM * https://g.antv.antgroup.com/api/event/faq#%E7%A6%81%E7%94%A8%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95 @@ -493,6 +492,26 @@ export class EventController { if (cell) { const cellType = cell.cellType; + // 通用的 mouseup 事件 + switch (cellType) { + case CellType.DATA_CELL: + this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_UP, event); + break; + case CellType.ROW_CELL: + this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_UP, event); + break; + case CellType.COL_CELL: + this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_UP, event); + break; + case CellType.CORNER_CELL: + this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_UP, event); + break; + case CellType.MERGED_CELL: + this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_UP, event); + break; + default: + break; + } // target 相同,说明是一个 cell 内的 click 事件 if (this.target === event.target) { // 屏蔽 actionIcons 的点击,字段标记增加的 icon 除外. @@ -503,6 +522,8 @@ export class EventController { return; } + this.spreadsheet.emit(S2Event.GLOBAL_CLICK, event); + switch (cellType) { case CellType.DATA_CELL: this.spreadsheet.emit(S2Event.DATA_CELL_CLICK, event); @@ -523,49 +544,16 @@ export class EventController { break; } } - - // 通用的 mouseup 事件 - switch (cellType) { - case CellType.DATA_CELL: - this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_UP, event); - break; - case CellType.ROW_CELL: - this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_UP, event); - break; - case CellType.COL_CELL: - this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_UP, event); - break; - case CellType.CORNER_CELL: - this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_UP, event); - break; - case CellType.MERGED_CELL: - this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_UP, event); - break; - default: - break; - } - } - }; - - private onCanvasClick = (event: CanvasEvent) => { - this.spreadsheet.emit(S2Event.GLOBAL_CLICK, event); - if (isMobile()) { - this.onCanvasMouseup(event); - } - - // 双击的 detail 是 2 - if ( - event.detail === 2 || - event.nativeEvent?.detail === 2 || - event.originalEvent?.detail === 2 - ) { - this.onCanvasDoubleClick(event); } }; private onCanvasDoubleClick = (event: CanvasEvent) => { const spreadsheet = this.spreadsheet; + if (event.detail !== 2) { + return; + } + if (this.isResizeArea(event)) { spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_UP, event); diff --git a/packages/s2-core/src/interaction/range-selection.ts b/packages/s2-core/src/interaction/range-selection.ts index 7db8d194c3..2e6450f116 100644 --- a/packages/s2-core/src/interaction/range-selection.ts +++ b/packages/s2-core/src/interaction/range-selection.ts @@ -11,7 +11,11 @@ import { } from '../common/constant'; import type { S2CellType, ViewMeta } from '../common/interface'; import type { Node } from '../facet/layout/node'; -import { getCellMeta, getRangeIndex } from '../utils/interaction/select-event'; +import { + getCellMeta, + getRangeIndex, + groupSelectedCells, +} from '../utils/interaction/select-event'; import { getCellsTooltipData } from '../utils/tooltip'; import { BaseEvent, type BaseEventImplement } from './base-interaction'; @@ -182,9 +186,10 @@ export class RangeSelection extends BaseEvent implements BaseEventImplement { cells: selectedCells, stateName: InteractionStateName.SELECTED, }); - const selectedCellIds = selectedCells.map(({ id }) => id); + const selectedCellIds = groupSelectedCells(selectedCells); interaction.updateCells(facet.getHeaderCells(selectedCellIds)); + interaction.emitSelectEvent({ targetCell: cell, interactionName: InteractionName.RANGE_SELECTION, diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index e950e67e5e..2502dddaa0 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -39,12 +39,13 @@ import { customMerge } from '../utils'; import { hideColumnsByThunkGroup } from '../utils/hide-columns'; import { getActiveHoverHeaderCells, - updateAllColHeaderCellState, + updateAllHeaderCellState, } from '../utils/interaction/hover-event'; import { mergeCell, unmergeCell } from '../utils/interaction/merge-cell'; import { getCellMeta, getRowCellForSelectedCell, + groupSelectedCells, } from '../utils/interaction/select-event'; import { clearState, setState } from '../utils/interaction/state-controller'; import { isMobile } from '../utils/is-mobile'; @@ -269,9 +270,16 @@ export class RootInteraction { * @example s2.interaction.isActiveCell(cell) */ public isActiveCell(cell: S2CellType): boolean { - return !!this.getCells().find((meta) => cell.getMeta().id === meta.id); + return !!this.getCells().find( + (meta) => cell.getMeta().id === meta.id && cell.cellType === meta.type, + ); } + public shouldForbidHeaderCellSelected = (selectedCells: CellMeta[]) => { + // 禁止跨单元格选择, 这样计算出来的数据和交互没有任何意义 + return unionBy(selectedCells, 'type').length > 1; + }; + /** * 是否是选中的单元格 * @example s2.interaction.isSelectedCell(cell) @@ -627,8 +635,7 @@ export class RootInteraction { return; } - // 禁止跨单元格选择, 这样计算出来的数据和交互没有任何意义. - if (unionBy(selectedCells, 'type').length > 1) { + if (this.shouldForbidHeaderCellSelected(selectedCells)) { return; } @@ -643,7 +650,7 @@ export class RootInteraction { stateName, }); - const selectedCellIds = selectedCells.map(({ id }) => id); + const selectedCellIds = groupSelectedCells(selectedCells); this.updateCells(this.spreadsheet.facet.getHeaderCells(selectedCellIds)); @@ -1101,7 +1108,7 @@ export class RootInteraction { : interaction.getSelectedCellHighlight(); if (colHeader && colId) { - updateAllColHeaderCellState(colId, facet.getColCells(), stateName); + updateAllHeaderCellState(colId, facet.getColCells(), stateName); } } diff --git a/packages/s2-core/src/shared/icons/dot-icon.svg b/packages/s2-core/src/shared/icons/dot-icon.svg deleted file mode 100644 index 7f66ea2d2a..0000000000 --- a/packages/s2-core/src/shared/icons/dot-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/packages/s2-core/src/shared/icons/sort-icon.svg b/packages/s2-core/src/shared/icons/sort-icon.svg deleted file mode 100644 index 705b29c1a1..0000000000 --- a/packages/s2-core/src/shared/icons/sort-icon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/packages/s2-core/src/shared/icons/switcher-icon.svg b/packages/s2-core/src/shared/icons/switcher-icon.svg deleted file mode 100644 index 051059343d..0000000000 --- a/packages/s2-core/src/shared/icons/switcher-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/packages/s2-core/src/shared/interface.ts b/packages/s2-core/src/shared/interface.ts index 3e901b69dc..18b10d2780 100644 --- a/packages/s2-core/src/shared/interface.ts +++ b/packages/s2-core/src/shared/interface.ts @@ -50,6 +50,7 @@ export type SheetType = | 'pivot' | 'table' | 'chart' + | 'pivotChart' | 'gridAnalysis' | 'strategy' | 'editable'; diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index 28b888d1d1..14e48d5816 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -45,6 +45,7 @@ import type { S2RenderOptions, S2Theme, SimpleData, + SimplePalette, SortMethod, ThemeCfg, ThemeName, @@ -71,6 +72,8 @@ import { isMobile } from '../utils/is-mobile'; import { customMerge, setupDataConfig, setupOptions } from '../utils/merge'; import { injectThemeVars } from '../utils/theme'; import { getTooltipData, getTooltipOptions } from '../utils/tooltip'; +import type { PivotSheet } from './pivot-sheet'; +import type { TableSheet } from './table-sheet'; export abstract class SpreadSheet extends EE { public themeName: ThemeName; @@ -104,7 +107,9 @@ export abstract class SpreadSheet extends EE { public abstract getDataSet(): BaseDataSet; - public abstract isPivotMode(): boolean; + public abstract isPivotMode(): this is PivotSheet; + + public abstract isTableMode(): this is TableSheet; public abstract isCustomRowFields(): boolean; @@ -112,8 +117,6 @@ export abstract class SpreadSheet extends EE { public abstract isFrozenRowHeader(): boolean; - public abstract isTableMode(): boolean; - public abstract isValueInCols(): boolean; protected abstract buildFacet(): void; @@ -134,8 +137,8 @@ export abstract class SpreadSheet extends EE { options: S2Options | null, ) { super(); - this.dataCfg = setupDataConfig(dataCfg); - this.options = setupOptions(options); + this.setupDataConfig(dataCfg); + this.setupOptions(options); this.dataSet = this.getDataSet(); this.setDebug(); this.initTooltip(); @@ -149,6 +152,14 @@ export abstract class SpreadSheet extends EE { this.mountSheetInstance(); } + protected setupDataConfig(dataCfg: S2DataConfig) { + this.dataCfg = setupDataConfig(dataCfg); + } + + protected setupOptions(options: S2Options | null | undefined) { + this.options = setupOptions(options); + } + public isCustomHeaderFields( fieldType?: keyof Pick, ): boolean { @@ -197,7 +208,7 @@ export abstract class SpreadSheet extends EE { DebuggerUtil.getInstance().setDebug(this.options.debug!); } - private initTheme() { + protected initTheme() { // When calling spreadsheet directly, there is no theme and initialization is required this.setThemeCfg({ name: 'default', @@ -221,7 +232,7 @@ export abstract class SpreadSheet extends EE { } } - private initInteraction() { + protected initInteraction() { this.interaction?.destroy?.(); this.interaction = new RootInteraction(this); } @@ -376,7 +387,7 @@ export abstract class SpreadSheet extends EE { this.hideTooltip(); if (reset) { - this.options = setupOptions(options); + this.setupOptions(options); } else { this.options = customMerge(this.options, options); } @@ -517,13 +528,23 @@ export abstract class SpreadSheet extends EE { removeOffscreenCanvas(); } - private setThemeName(name: ThemeName) { + protected setThemeName(name: ThemeName) { this.themeName = name; } - public setThemeCfg(themeCfg: ThemeCfg = {}) { + public setThemeCfg( + themeCfg: ThemeCfg = {}, + getCustomTheme?: ( + palette: SimplePalette, + spreadsheet?: SpreadSheet, + ) => S2Theme, + ) { const theme = themeCfg?.theme || {}; - const newTheme = getTheme({ ...themeCfg, spreadsheet: this }); + const newTheme = getTheme({ + ...themeCfg, + spreadsheet: this, + getCustomTheme, + }); this.theme = customMerge(newTheme, theme); this.setThemeName(themeCfg?.name!); @@ -897,4 +918,8 @@ export abstract class SpreadSheet extends EE { return text ?? getDefaultSeriesNumberText(); } + + public enableAsyncExport(): Error | true { + return true; + } } diff --git a/packages/s2-core/src/theme/index.ts b/packages/s2-core/src/theme/index.ts index a76f7f0d00..77c8333c15 100644 --- a/packages/s2-core/src/theme/index.ts +++ b/packages/s2-core/src/theme/index.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines-per-function */ +import { merge } from 'lodash'; import { CELL_PADDING, FONT_FAMILY, @@ -6,37 +7,354 @@ import { } from '../common/constant'; import type { DefaultCellTheme, - Padding, S2Theme, + SimplePalette, ThemeCfg, } from '../common/interface'; import type { SpreadSheet } from '../sheet-type'; import { isMobile, isWindows } from '../utils/is-mobile'; import { getPalette } from '../utils/theme'; -/** - * @describe generate the theme according to the type - * @param themeCfg - */ -export const getTheme = ( - themeCfg: Omit & { spreadsheet?: SpreadSheet }, -): S2Theme => { - const { - basicColors, - semanticColors, - others: otherColors, - } = themeCfg?.palette || getPalette(themeCfg?.name); +export const getCornerCellTheme = ( + palette: SimplePalette, + spreadsheet?: SpreadSheet, +): DefaultCellTheme => { + const { basicColors, others: otherColors } = palette; + + const isTable = spreadsheet?.isTableMode(); + const boldTextDefaultFontWeight = isWindows() ? 'bold' : 700; + + return { + text: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: boldTextDefaultFontWeight, + fill: basicColors[0], + opacity: 1, + textAlign: isTable ? 'center' : 'left', + textBaseline: 'middle', + }, + bolderText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: boldTextDefaultFontWeight, + fill: basicColors[0], + opacity: 1, + textAlign: isTable ? 'center' : 'right', + textBaseline: 'middle', + }, + measureText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: boldTextDefaultFontWeight, + fill: basicColors[0], + opacity: 1, + textAlign: 'left', + textBaseline: 'middle', + }, + cell: { + // ----------- background color ----------- + backgroundColor: basicColors[3], + backgroundColorOpacity: 1, + // ----------- border color -------------- + horizontalBorderColor: basicColors[10], + horizontalBorderColorOpacity: 1, + verticalBorderColor: basicColors[10], + verticalBorderColorOpacity: 1, + // ----------- border width -------------- + horizontalBorderWidth: 1, + verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], + // -------------- layout ----------------- + padding: { + top: CELL_PADDING, + right: CELL_PADDING, + bottom: CELL_PADDING, + left: CELL_PADDING, + }, + + /* ---------- interaction state ----------- */ + interactionState: { + // -------------- hover ------------------- + hover: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- selected ------------------- + selected: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- unselected ------------------- + unselected: { + backgroundOpacity: 0.3, + textOpacity: 0.3, + opacity: 0.3, + }, + // -------------- prepare select -------------- + prepareSelect: { + borderColor: basicColors[14], + borderOpacity: 1, + borderWidth: 1, + }, + // -------------- searchResult ------------------- + searchResult: { + backgroundColor: otherColors?.results ?? basicColors[2], + backgroundOpacity: 1, + }, + // -------------- highlight ------------------- + highlight: { + backgroundColor: otherColors?.highlight ?? basicColors[6], + backgroundOpacity: 1, + }, + }, + }, + icon: { + fill: basicColors[0], + size: 10, + margin: { + right: 4, + left: 4, + }, + }, + }; +}; + +export const getRowCellTheme = ( + palette: SimplePalette, + spreadsheet?: SpreadSheet, +): DefaultCellTheme => { + const { basicColors, others: otherColors } = palette; + + const isTable = spreadsheet?.isTableMode(); + const boldTextDefaultFontWeight = isWindows() ? 'bold' : 700; + + return { + seriesText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[14], + linkTextFill: basicColors[6], + opacity: 1, + textBaseline: 'middle', + textAlign: 'center', + }, + measureText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[14], + linkTextFill: basicColors[6], + opacity: 1, + textAlign: isTable ? 'center' : 'left', + textBaseline: 'middle', + }, + bolderText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: boldTextDefaultFontWeight, + fill: basicColors[14], + linkTextFill: basicColors[6], + opacity: 1, + textAlign: isTable ? 'center' : 'left', + textBaseline: 'middle', + }, + text: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[14], + linkTextFill: basicColors[6], + opacity: 1, + textBaseline: 'middle', + // default align center for row cell in table mode + textAlign: isTable ? 'center' : 'left', + }, + cell: { + // ----------- background color ----------- + backgroundColor: basicColors[1], + backgroundColorOpacity: 1, + // ----------- bottom border color -------------- + horizontalBorderColor: basicColors[9], + horizontalBorderColorOpacity: 1, + verticalBorderColor: basicColors[9], + verticalBorderColorOpacity: 1, + // ----------- bottom border width -------------- + horizontalBorderWidth: 1, + verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], + // -------------- layout ----------------- + padding: { + top: CELL_PADDING, + right: CELL_PADDING, + bottom: CELL_PADDING, + left: CELL_PADDING, + }, + /* ---------- interaction state ----------- */ + interactionState: { + // -------------- hover ------------------- + hover: { + backgroundColor: basicColors[2], + backgroundOpacity: 0.6, + }, + // -------------- selected ------------------- + selected: { + backgroundColor: basicColors[2], + backgroundOpacity: 0.6, + }, + // -------------- unselected ------------------- + unselected: { + backgroundOpacity: 0.3, + textOpacity: 0.3, + opacity: 0.3, + }, + // -------------- prepare select -------------- + prepareSelect: { + borderColor: basicColors[14], + borderOpacity: 1, + borderWidth: 1, + }, + // -------------- searchResult ------------------- + searchResult: { + backgroundColor: otherColors?.results ?? basicColors[2], + backgroundOpacity: 1, + }, + // -------------- highlight ------------------- + highlight: { + backgroundColor: otherColors?.highlight ?? basicColors[6], + backgroundOpacity: 1, + }, + }, + }, + icon: { + fill: basicColors[14], + size: 10, + margin: { + right: 4, + left: 4, + }, + }, + seriesNumberWidth: 80, + }; +}; + +export const getColCellTheme = (palette: SimplePalette): DefaultCellTheme => { + const { basicColors, others: otherColors } = palette; - const isTable = themeCfg?.spreadsheet?.isTableMode(); const boldTextDefaultFontWeight = isWindows() ? 'bold' : 700; - const cellPadding: Padding = { - top: CELL_PADDING, - right: CELL_PADDING, - bottom: CELL_PADDING, - left: CELL_PADDING, + + return { + measureText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[0], + opacity: 1, + // 默认列头的数值字段和 dataCell 数值对齐 + textAlign: 'right', + textBaseline: 'middle', + linkTextFill: basicColors[6], + }, + bolderText: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: boldTextDefaultFontWeight, + fill: basicColors[0], + opacity: 1, + textAlign: 'center', + textBaseline: 'middle', + linkTextFill: basicColors[6], + }, + text: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[0], + opacity: 1, + textAlign: 'center', + textBaseline: 'middle', + linkTextFill: basicColors[6], + }, + cell: { + // ----------- background color ----------- + backgroundColor: basicColors[3], + backgroundColorOpacity: 1, + // ----------- border color -------------- + horizontalBorderColor: basicColors[10], + horizontalBorderColorOpacity: 1, + verticalBorderColor: basicColors[10], + verticalBorderColorOpacity: 1, + // ----------- border width -------------- + horizontalBorderWidth: 1, + verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], + // -------------- layout ----------------- + padding: { + top: CELL_PADDING, + right: CELL_PADDING, + bottom: CELL_PADDING, + left: CELL_PADDING, + }, + + /* ---------- interaction state ----------- */ + interactionState: { + // -------------- hover ------------------- + hover: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- selected ------------------- + selected: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- unselected ------------------- + unselected: { + backgroundOpacity: 0.3, + textOpacity: 0.3, + opacity: 0.3, + }, + // -------------- prepare select -------------- + prepareSelect: { + borderColor: basicColors[14], + borderOpacity: 1, + borderWidth: 1, + }, + // -------------- searchResult ------------------- + searchResult: { + backgroundColor: otherColors?.results ?? basicColors[2], + backgroundOpacity: 1, + }, + // -------------- highlight ------------------- + highlight: { + backgroundColor: otherColors?.highlight ?? basicColors[6], + backgroundOpacity: 1, + }, + }, + }, + icon: { + fill: basicColors[0], + size: 10, + margin: { + top: 6, + right: 4, + bottom: 6, + left: 4, + }, + }, }; +}; + +export const getDataCellTheme = (palette: SimplePalette): DefaultCellTheme => { + const { basicColors, others: otherColors, semanticColors } = palette; - const getDataCell = (): DefaultCellTheme => ({ + const boldTextDefaultFontWeight = isWindows() ? 'bold' : 700; + + return { bolderText: { fontFamily: FONT_FAMILY, fontSize: 12, @@ -71,8 +389,12 @@ export const getTheme = ( horizontalBorderWidth: 1, verticalBorderWidth: 1, // -------------- layout ----------------- - padding: cellPadding, - + padding: { + top: CELL_PADDING, + right: CELL_PADDING, + bottom: CELL_PADDING, + left: CELL_PADDING, + }, /* ---------- interaction state ----------- */ interactionState: { // -------------- hover ------------------- @@ -172,378 +494,114 @@ export const getTheme = ( left: 4, }, }, - }); + }; +}; - return { - // ------------- Headers ------------------- - cornerCell: { - text: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: boldTextDefaultFontWeight, - fill: basicColors[0], - opacity: 1, - textAlign: isTable ? 'center' : 'left', - textBaseline: 'middle', - }, - bolderText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: boldTextDefaultFontWeight, - fill: basicColors[0], - opacity: 1, - textAlign: isTable ? 'center' : 'right', - textBaseline: 'middle', - }, - measureText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: boldTextDefaultFontWeight, - fill: basicColors[0], - opacity: 1, - textAlign: 'left', - textBaseline: 'middle', - }, - cell: { - // ----------- background color ----------- - backgroundColor: basicColors[3], - backgroundColorOpacity: 1, - // ----------- border color -------------- - horizontalBorderColor: basicColors[10], - horizontalBorderColorOpacity: 1, - verticalBorderColor: basicColors[10], - verticalBorderColorOpacity: 1, - // ----------- border width -------------- - horizontalBorderWidth: 1, - verticalBorderWidth: 1, - // -------------- border dash ----------------- - borderDash: [], - // -------------- layout ----------------- - padding: cellPadding, +/** + * @describe generate the theme according to the type + * @param themeCfg + */ +export const getTheme = ( + themeCfg: Omit & { + spreadsheet?: SpreadSheet; + getCustomTheme?: ( + palette: SimplePalette, + spreadsheet?: SpreadSheet, + ) => S2Theme; + }, +): S2Theme => { + const palette = themeCfg?.palette || getPalette(themeCfg?.name); + const { basicColors } = palette; - /* ---------- interaction state ----------- */ - interactionState: { - // -------------- hover ------------------- - hover: { - backgroundColor: basicColors[4], - backgroundOpacity: 0.6, - }, - // -------------- selected ------------------- - selected: { - backgroundColor: basicColors[4], - backgroundOpacity: 0.6, - }, - // -------------- unselected ------------------- - unselected: { - backgroundOpacity: 0.3, - textOpacity: 0.3, - opacity: 0.3, - }, - // -------------- prepare select -------------- - prepareSelect: { - borderColor: basicColors[14], - borderOpacity: 1, - borderWidth: 1, - }, - // -------------- searchResult ------------------- - searchResult: { - backgroundColor: otherColors?.results ?? basicColors[2], - backgroundOpacity: 1, - }, - // -------------- highlight ------------------- - highlight: { - backgroundColor: otherColors?.highlight ?? basicColors[6], - backgroundOpacity: 1, - }, - }, - }, - icon: { - fill: basicColors[0], - size: 10, - margin: { - right: 4, - left: 4, - }, - }, - }, - rowCell: { - seriesText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[14], - linkTextFill: basicColors[6], - opacity: 1, - textBaseline: 'middle', - textAlign: 'center', - }, - measureText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[14], - linkTextFill: basicColors[6], - opacity: 1, - textAlign: isTable ? 'center' : 'left', - textBaseline: 'middle', - }, - bolderText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: boldTextDefaultFontWeight, - fill: basicColors[14], - linkTextFill: basicColors[6], - opacity: 1, - textAlign: isTable ? 'center' : 'left', - textBaseline: 'middle', - }, - text: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[14], - linkTextFill: basicColors[6], - opacity: 1, - textBaseline: 'middle', - // default align center for row cell in table mode - textAlign: isTable ? 'center' : 'left', - }, - cell: { - // ----------- background color ----------- - backgroundColor: basicColors[1], - backgroundColorOpacity: 1, - // ----------- bottom border color -------------- - horizontalBorderColor: basicColors[9], - horizontalBorderColorOpacity: 1, - verticalBorderColor: basicColors[9], - verticalBorderColorOpacity: 1, - // ----------- bottom border width -------------- - horizontalBorderWidth: 1, - verticalBorderWidth: 1, - // -------------- border dash ----------------- - borderDash: [], - // -------------- layout ----------------- - padding: cellPadding, + const spreadsheet = themeCfg?.spreadsheet; + + const customTheme = themeCfg?.getCustomTheme?.(palette, spreadsheet); + + return merge( + { + // ------------- Headers ------------------- + cornerCell: getCornerCellTheme(palette, spreadsheet), + rowCell: getRowCellTheme(palette, spreadsheet), + colCell: getColCellTheme(palette), + // ------------- DataCell ------------------- + dataCell: getDataCellTheme(palette), + // ------------- MergedCell ------------------- + mergedCell: getDataCellTheme(palette), + // resize active area + resizeArea: { + size: 3, + background: basicColors[7], + backgroundOpacity: 0, + guideLineColor: basicColors[7], + guideLineDisableColor: 'rgba(0,0,0,0.25)', + guideLineDash: [3, 3], /* ---------- interaction state ----------- */ interactionState: { - // -------------- hover ------------------- hover: { - backgroundColor: basicColors[2], - backgroundOpacity: 0.6, - }, - // -------------- selected ------------------- - selected: { - backgroundColor: basicColors[2], - backgroundOpacity: 0.6, - }, - // -------------- unselected ------------------- - unselected: { - backgroundOpacity: 0.3, - textOpacity: 0.3, - opacity: 0.3, - }, - // -------------- prepare select -------------- - prepareSelect: { - borderColor: basicColors[14], - borderOpacity: 1, - borderWidth: 1, - }, - // -------------- searchResult ------------------- - searchResult: { - backgroundColor: otherColors?.results ?? basicColors[2], - backgroundOpacity: 1, - }, - // -------------- highlight ------------------- - highlight: { - backgroundColor: otherColors?.highlight ?? basicColors[6], + backgroundColor: basicColors[7], backgroundOpacity: 1, }, }, }, - icon: { - fill: basicColors[14], - size: 10, - margin: { - right: 4, - left: 4, - }, + // ------------- scrollBar ------------------- + scrollBar: { + trackColor: 'rgba(0,0,0,0.01)', + thumbHoverColor: 'rgba(0,0,0,0.25)', + thumbColor: 'rgba(0,0,0,0.15)', + thumbHorizontalMinSize: 32, + thumbVerticalMinSize: 32, + size: isMobile() ? 3 : 6, + hoverSize: isMobile() ? 4 : 8, + lineCap: 'round', }, - seriesNumberWidth: 80, - }, - colCell: { - measureText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[0], - linkTextFill: basicColors[6], - opacity: 1, - // 默认列头的数值字段和 dataCell 数值对齐 - textAlign: 'right', - textBaseline: 'middle', + // ------------- split line ----------------- + splitLine: { + horizontalBorderColor: basicColors[12], + horizontalBorderColorOpacity: 0.2, + horizontalBorderWidth: 2, + verticalBorderColor: basicColors[11], + verticalBorderColorOpacity: 0.25, + verticalBorderWidth: 2, + showShadow: true, + shadowWidth: 8, + shadowColors: { + left: 'rgba(0,0,0,0.1)', + right: 'rgba(0,0,0,0)', + }, + borderDash: [], }, - bolderText: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: boldTextDefaultFontWeight, - fill: basicColors[0], - linkTextFill: basicColors[6], - opacity: 1, - textAlign: 'center', - textBaseline: 'middle', + // ------------- prepareSelectMask ----------------- + prepareSelectMask: { + backgroundColor: basicColors[5], + backgroundOpacity: 0.3, }, - text: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[0], - linkTextFill: basicColors[6], + // ------------- canvas background + background: { + color: basicColors[8], opacity: 1, - textAlign: 'center', - textBaseline: 'middle', }, - cell: { - // ----------- background color ----------- - backgroundColor: basicColors[3], - backgroundColorOpacity: 1, - // ----------- border color -------------- - horizontalBorderColor: basicColors[10], - horizontalBorderColorOpacity: 1, - verticalBorderColor: basicColors[10], - verticalBorderColorOpacity: 1, - // ----------- border width -------------- - horizontalBorderWidth: 1, - verticalBorderWidth: 1, - // -------------- border dash ----------------- - borderDash: [], - // -------------- layout ----------------- - padding: cellPadding, - - /* ---------- interaction state ----------- */ - interactionState: { - // -------------- hover ------------------- - hover: { - backgroundColor: basicColors[4], - backgroundOpacity: 0.6, - }, - // -------------- selected ------------------- - selected: { - backgroundColor: basicColors[4], - backgroundOpacity: 0.6, - }, - // -------------- unselected ------------------- - unselected: { - backgroundOpacity: 0.3, - textOpacity: 0.3, - opacity: 0.3, - }, - // -------------- prepare select -------------- - prepareSelect: { - borderColor: basicColors[14], - borderOpacity: 1, - borderWidth: 1, + empty: { + icon: { + fill: '', + width: 64, + height: 41, + margin: { + top: 0, + right: 0, + bottom: 24, + left: 0, }, - // -------------- searchResult ------------------- - searchResult: { - backgroundColor: otherColors?.results ?? basicColors[2], - backgroundOpacity: 1, - }, - // -------------- highlight ------------------- - highlight: { - backgroundColor: otherColors?.highlight ?? basicColors[6], - backgroundOpacity: 1, - }, - }, - }, - icon: { - fill: basicColors[0], - size: 10, - margin: { - top: 6, - right: 4, - bottom: 6, - left: 4, }, - }, - }, - // ------------- DataCell ------------------- - dataCell: getDataCell(), - // ------------- MergedCell ------------------- - mergedCell: getDataCell(), - // resize active area - resizeArea: { - size: 3, - background: basicColors[7], - backgroundOpacity: 0, - guideLineColor: basicColors[7], - guideLineDisableColor: 'rgba(0,0,0,0.25)', - guideLineDash: [3, 3], - - /* ---------- interaction state ----------- */ - interactionState: { - hover: { - backgroundColor: basicColors[7], - backgroundOpacity: 1, - }, - }, - }, - // ------------- scrollBar ------------------- - scrollBar: { - trackColor: 'rgba(0,0,0,0.01)', - thumbHoverColor: 'rgba(0,0,0,0.25)', - thumbColor: 'rgba(0,0,0,0.15)', - thumbHorizontalMinSize: 32, - thumbVerticalMinSize: 32, - size: isMobile() ? 3 : 6, - hoverSize: isMobile() ? 4 : 8, - lineCap: 'round', - }, - // ------------- split line ----------------- - splitLine: { - horizontalBorderColor: basicColors[12], - horizontalBorderColorOpacity: 0.2, - horizontalBorderWidth: 2, - verticalBorderColor: basicColors[11], - verticalBorderColorOpacity: 0.25, - verticalBorderWidth: 2, - showShadow: true, - shadowWidth: 8, - shadowColors: { - left: 'rgba(0,0,0,0.1)', - right: 'rgba(0,0,0,0)', - }, - borderDash: [], - }, - // ------------- prepareSelectMask ----------------- - prepareSelectMask: { - backgroundColor: basicColors[5], - backgroundOpacity: 0.3, - }, - // ------------- canvas background - background: { - color: basicColors[8], - opacity: 1, - }, - empty: { - icon: { - fill: '', - width: 64, - height: 41, - margin: { - top: 0, - right: 0, - bottom: 24, - left: 0, + description: { + fontFamily: FONT_FAMILY, + fontSize: 12, + fontWeight: 'normal', + fill: basicColors[14], + opacity: 1, }, }, - description: { - fontFamily: FONT_FAMILY, - fontSize: 12, - fontWeight: 'normal', - fill: basicColors[14], - opacity: 1, - }, }, - }; + customTheme, + ); }; diff --git a/packages/s2-core/src/utils/export/copy/core.ts b/packages/s2-core/src/utils/export/copy/core.ts index a5f71ff55e..b17c4c474f 100644 --- a/packages/s2-core/src/utils/export/copy/core.ts +++ b/packages/s2-core/src/utils/export/copy/core.ts @@ -206,6 +206,14 @@ export const asyncProcessAllSelected = ( ): Promise => { const { sheetInstance } = params; + const check = sheetInstance.enableAsyncExport(); + + if (check instanceof Error) { + // eslint-disable-next-line no-console + console.warn(check); + throw check; + } + if (sheetInstance.isPivotMode()) { return asyncProcessSelectedAllPivot(params); } diff --git a/packages/s2-core/src/utils/index.ts b/packages/s2-core/src/utils/index.ts index 7f1c657c26..99004a5424 100644 --- a/packages/s2-core/src/utils/index.ts +++ b/packages/s2-core/src/utils/index.ts @@ -4,9 +4,12 @@ export * from './canvas'; export * from './cell'; export * from './color'; export * from './common'; +export * from './dataset/pivot-data-set'; export * from './export'; +export * from './facet'; export * from './g-mini-charts'; export * from './g-renders'; +export * from './get-all-child-cells'; export * from './get-classnames'; export * from './inject-css-text'; export * from './interaction'; @@ -14,6 +17,7 @@ export * from './is-mobile'; export * from './layout'; export * from './math'; export * from './merge'; +export * from './schedule'; export * from './sort-action'; export * from './text'; export * from './theme'; diff --git a/packages/s2-core/src/utils/interaction/hover-event.ts b/packages/s2-core/src/utils/interaction/hover-event.ts index 139983aa08..cca4c5d284 100644 --- a/packages/s2-core/src/utils/interaction/hover-event.ts +++ b/packages/s2-core/src/utils/interaction/hover-event.ts @@ -1,5 +1,5 @@ import { filter, forEach } from 'lodash'; -import type { ColCell, HeaderCell } from '../../cell'; +import type { HeaderCell } from '../../cell'; import { InteractionStateName, NODE_ID_SEPARATOR } from '../../common/constant'; import { generateId } from '../layout/generate-id'; @@ -33,13 +33,13 @@ export const getActiveHoverHeaderCells = ( return allHeaderCells; }; -export const updateAllColHeaderCellState = ( - colId: string | undefined, - colHeaderCells: ColCell[], +export const updateAllHeaderCellState = ( + id: string | undefined, + headerCells: HeaderCell[], stateName: InteractionStateName, ) => { - if (colId) { - const allColHeaderCells = getActiveHoverHeaderCells(colId, colHeaderCells); + if (id) { + const allColHeaderCells = getActiveHoverHeaderCells(id, headerCells); forEach(allColHeaderCells, (cell) => { cell.updateByState(stateName); diff --git a/packages/s2-core/src/utils/interaction/index.ts b/packages/s2-core/src/utils/interaction/index.ts index 813cf7b201..9a72c1e0f1 100644 --- a/packages/s2-core/src/utils/interaction/index.ts +++ b/packages/s2-core/src/utils/interaction/index.ts @@ -2,6 +2,7 @@ export * from './formatter'; export * from './hover-event'; export * from './link-field'; export * from './merge-cell'; +export * from './resize'; export * from './scroll'; export * from './select-event'; export * from './state-controller'; diff --git a/packages/s2-core/src/utils/interaction/select-event.ts b/packages/s2-core/src/utils/interaction/select-event.ts index d88c50b3d4..1259f9f9be 100644 --- a/packages/s2-core/src/utils/interaction/select-event.ts +++ b/packages/s2-core/src/utils/interaction/select-event.ts @@ -1,4 +1,4 @@ -import { reduce, uniqBy } from 'lodash'; +import { groupBy, map, mapValues, reduce, uniqBy } from 'lodash'; import { HeaderCell, TableSeriesNumberCell } from '../../cell'; import { CellType, InteractionKeyboardKey } from '../../common/constant'; import type { @@ -164,3 +164,9 @@ export const afterSelectDataCells: OnUpdateCells = (root, updateDataCells) => { updateDataCells(); }; + +export type SelectedIds = { [type in CellType]?: string[] }; + +export const groupSelectedCells = (selectedCells: CellMeta[]): SelectedIds => { + return mapValues(groupBy(selectedCells, 'type'), (cells) => map(cells, 'id')); +}; diff --git a/packages/s2-core/src/utils/layout/generate-header-nodes.ts b/packages/s2-core/src/utils/layout/generate-header-nodes.ts index 677ecafcf2..da200117c0 100644 --- a/packages/s2-core/src/utils/layout/generate-header-nodes.ts +++ b/packages/s2-core/src/utils/layout/generate-header-nodes.ts @@ -33,8 +33,8 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { const fieldValue = resolveNillString( originalFieldValue as string, ) as FieldValue; - const isTotals = fieldValue instanceof TotalClass; - const isTotalMeasure = fieldValue instanceof TotalMeasure; + const isTotals = TotalClass.isTotalClassInstance(fieldValue); + const isTotalMeasure = TotalMeasure.isTotalMeasureInstance(fieldValue); let value: string; let nodeQuery: Record; let isLeaf = false; @@ -44,14 +44,12 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { let adjustedField = currentField; if (isTotals) { - const totalClass = fieldValue as TotalClass; - - isGrandTotals = totalClass.isGrandTotals; - isSubTotals = totalClass.isSubTotals; - isTotalRoot = totalClass.isTotalRoot; - value = i18n((fieldValue as TotalClass).label); + isGrandTotals = fieldValue.isGrandTotals; + isSubTotals = fieldValue.isSubTotals; + isTotalRoot = fieldValue.isTotalRoot; + value = i18n(fieldValue.label); if (isTotalRoot) { - nodeQuery = query; + nodeQuery = { ...query }; } else { // root[&]四川[&]总计 => {province: '四川'} nodeQuery = { ...query, [currentField]: value }; @@ -64,7 +62,7 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { isLeaf = whetherLeafByLevel({ spreadsheet, level, fields }); } else if (isTotalMeasure) { - value = i18n((fieldValue as TotalMeasure).label); + value = i18n(fieldValue.label); // root[&]四川[&]总计[&]price => {province: '四川',EXTRA_FIELD: 'price' } nodeQuery = { ...query, [EXTRA_FIELD]: value }; adjustedField = EXTRA_FIELD; diff --git a/packages/s2-core/src/utils/merge.ts b/packages/s2-core/src/utils/merge.ts index ad42bae6ff..96ae47baf7 100644 --- a/packages/s2-core/src/utils/merge.ts +++ b/packages/s2-core/src/utils/merge.ts @@ -66,7 +66,7 @@ export const setupDataConfig = ( }; export const setupOptions = ( - options: Partial | null | undefined, + ...options: (Partial | null | undefined)[] ): S2Options => { - return customMerge(DEFAULT_OPTIONS, options); + return customMerge(DEFAULT_OPTIONS, ...options); }; diff --git a/packages/s2-core/src/utils/schedule.ts b/packages/s2-core/src/utils/schedule.ts new file mode 100644 index 0000000000..2ed0cce391 --- /dev/null +++ b/packages/s2-core/src/utils/schedule.ts @@ -0,0 +1,5 @@ +export function waitForCellMounted(cb: () => void) { + Promise.resolve().then(() => { + cb(); + }); +} diff --git a/packages/s2-core/tsconfig.build.json b/packages/s2-core/tsconfig.build.json index 5edc7028ae..8f867a0a66 100644 --- a/packages/s2-core/tsconfig.build.json +++ b/packages/s2-core/tsconfig.build.json @@ -2,6 +2,8 @@ "extends": "./tsconfig.json", "include": ["src/**/*", "./typings.d.ts", "../../global.d.ts"], "compilerOptions": { - "paths": {} + "paths": { + "@antv/s2": ["s2-core/src/index.ts"] + } } } diff --git a/packages/s2-core/tsconfig.json b/packages/s2-core/tsconfig.json index 197cba5581..264d5dd7d8 100644 --- a/packages/s2-core/tsconfig.json +++ b/packages/s2-core/tsconfig.json @@ -3,10 +3,12 @@ "compilerOptions": { "paths": { "@antv/s2": ["s2-core/src/index.ts"], + "@antv/s2/*":["s2-core/src/*"], "@/*": ["s2-core/src/*"], "tests/*": ["s2-core/__tests__/*"] }, }, + "exclude": ["node_modules", "coverage", "esm", "lib", "dist", "temp"], "include": ["src", "../../global.d.ts"] } diff --git a/packages/s2-react-components/src/components/common/icons/index.tsx b/packages/s2-react-components/src/components/common/icons/index.tsx index 6cfea3a12c..ed0201d507 100644 --- a/packages/s2-react-components/src/components/common/icons/index.tsx +++ b/packages/s2-react-components/src/components/common/icons/index.tsx @@ -1,9 +1,9 @@ -import { ReactComponent as CalendarIcon } from '@antv/s2/esm/shared/icons/calendar-icon.svg'; -import { ReactComponent as ColIcon } from '@antv/s2/esm/shared/icons/col-icon.svg'; -import { ReactComponent as LocationIcon } from '@antv/s2/esm/shared/icons/location-icon.svg'; -import { ReactComponent as RowIcon } from '@antv/s2/esm/shared/icons/row-icon.svg'; -import { ReactComponent as TextIcon } from '@antv/s2/esm/shared/icons/text-icon.svg'; -import { ReactComponent as ValueIcon } from '@antv/s2/esm/shared/icons/value-icon.svg'; import './index.less'; +import { ReactComponent as CalendarIcon } from './svg/calendar-icon.svg'; +import { ReactComponent as ColIcon } from './svg/col-icon.svg'; +import { ReactComponent as LocationIcon } from './svg/location-icon.svg'; +import { ReactComponent as RowIcon } from './svg/row-icon.svg'; +import { ReactComponent as TextIcon } from './svg/text-icon.svg'; +import { ReactComponent as ValueIcon } from './svg/value-icon.svg'; export { CalendarIcon, ColIcon, LocationIcon, RowIcon, TextIcon, ValueIcon }; diff --git a/packages/s2-core/src/shared/icons/calendar-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/calendar-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/calendar-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/calendar-icon.svg diff --git a/packages/s2-core/src/shared/icons/col-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/col-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/col-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/col-icon.svg diff --git a/packages/s2-core/src/shared/icons/location-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/location-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/location-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/location-icon.svg diff --git a/packages/s2-core/src/shared/icons/row-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/row-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/row-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/row-icon.svg diff --git a/packages/s2-core/src/shared/icons/text-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/text-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/text-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/text-icon.svg diff --git a/packages/s2-core/src/shared/icons/value-icon.svg b/packages/s2-react-components/src/components/common/icons/svg/value-icon.svg similarity index 100% rename from packages/s2-core/src/shared/icons/value-icon.svg rename to packages/s2-react-components/src/components/common/icons/svg/value-icon.svg diff --git a/packages/s2-react-components/src/components/frozen-panel/index.less b/packages/s2-react-components/src/components/frozen-panel/index.less index eddd64e553..bab751a27e 100644 --- a/packages/s2-react-components/src/components/frozen-panel/index.less +++ b/packages/s2-react-components/src/components/frozen-panel/index.less @@ -1,4 +1,4 @@ -@import '@antv/s2/src/styles/variables.less'; +@import '@antv/s2/esm/styles/variables.less'; .@{s2-cls-prefix}-frozen-panel { width: 400px; diff --git a/packages/s2-react-components/tsconfig.build.json b/packages/s2-react-components/tsconfig.build.json index 5edc7028ae..2664f3c426 100644 --- a/packages/s2-react-components/tsconfig.build.json +++ b/packages/s2-react-components/tsconfig.build.json @@ -2,6 +2,9 @@ "extends": "./tsconfig.json", "include": ["src/**/*", "./typings.d.ts", "../../global.d.ts"], "compilerOptions": { - "paths": {} + "paths": { + "@antv/s2": ["s2-react-components/node_modules/@antv/s2/esm/index.d.ts"], + "@antv/s2/*": ["s2-react-components/node_modules/@antv/s2/esm/*"] + } } } diff --git a/packages/s2-react-components/tsconfig.json b/packages/s2-react-components/tsconfig.json index 1eadbf6f53..d4336ed33e 100644 --- a/packages/s2-react-components/tsconfig.json +++ b/packages/s2-react-components/tsconfig.json @@ -5,6 +5,7 @@ "jsx": "react", "paths": { "@antv/s2":["s2-core/src/index.ts"], + "@antv/s2/*":["s2-core/src/*"], "@antv/s2/esm/shared": ["s2-core/src/shared/index.ts"], "@antv/s2/esm/shared/*": ["s2-core/src/shared/*"], "@antv/s2-react":["s2-react/src/index.ts"], diff --git a/packages/s2-react-components/vite.config.ts b/packages/s2-react-components/vite.config.ts index 597bac3a4c..7c02136935 100644 --- a/packages/s2-react-components/vite.config.ts +++ b/packages/s2-react-components/vite.config.ts @@ -8,7 +8,9 @@ import { defineConfig } from 'vite'; import svgr from 'vite-plugin-svgr'; import { getBaseConfig } from '../../build.config.base.mjs'; -const { getViteConfig, isDevMode } = getBaseConfig(); +const { getViteConfig, isDevMode } = getBaseConfig({ + aliasReact: true, +}); const root = path.join(__dirname, isDevMode ? 'playground' : ''); diff --git a/packages/s2-react/__tests__/spreadsheet/__snapshots__/pagination-spec.tsx.snap b/packages/s2-react/__tests__/spreadsheet/__snapshots__/pagination-spec.tsx.snap index 68e2cf9de6..ef442d23fe 100644 --- a/packages/s2-react/__tests__/spreadsheet/__snapshots__/pagination-spec.tsx.snap +++ b/packages/s2-react/__tests__/spreadsheet/__snapshots__/pagination-spec.tsx.snap @@ -16,7 +16,7 @@ exports[`Pagination Tests should render with antd component 1`] = />
  • component 2`] = />
    • Tests', () => { ); }; - const onDataCellRender = jest.fn((cell: DataCell) => { - const chartOptions = cell.getRenderChartOptions(); - - // https://g2.antv.antgroup.com/manual/extra-topics/bundle#g2stdlib - renderToMountedElement(chartOptions, { - group: cell, - library: stdlib(), - }); - }); - test('should default render empty text shape', async () => { renderChartSheet(null); @@ -66,13 +55,9 @@ describe(' Tests', () => { }); test('should trigger date cell render event', async () => { - renderChartSheet(null, { - onDataCellRender, - }); + renderChartSheet(null); await waitFor(() => { - expect(onDataCellRender).toHaveBeenCalledTimes(4); - s2.facet.getDataCells().forEach((cell) => { expect(cell.getActualText()).toBeUndefined(); expect(cell.getTextShapes()).toBeEmpty(); @@ -85,9 +70,7 @@ describe(' Tests', () => { .spyOn(console, 'error') .mockImplementationOnce(() => {}); - renderChartSheet(null, { - onDataCellRender, - }); + renderChartSheet(null); await waitFor(() => { expect(errorSpy).not.toHaveBeenCalledWith( diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap index c16b672988..e71c0c9f55 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap @@ -15,21 +15,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; -exports[` Tests StrategySheet Export Tests should export correct data 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -自定义节点A - -自定义节点A 指标A 377 3877 4324 42% - - 377 -自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 -自定义节点A 指标A 自定义节点B -自定义节点A 指标A 指标C 324 377 0 - - 324 -自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 -自定义节点A 自定义节点E -指标E 377 324 0.02 - - -指标E 自定义节点C -指标E 自定义节点D " -`; - exports[` Tests StrategySheet Export Tests should export correct data by {formatHeader: false, formatData: false} 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -45,21 +30,6 @@ measure-e custom-node-3 measure-e custom-node-4 " `; -exports[` Tests StrategySheet Export Tests should export correct data by {formatHeader: false, formatData: false} 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -custom-node-1 - -custom-node-1 measure-a 377 3877 4324 0.42 - - 377 -custom-node-1 measure-a measure-b 377 324 377 324 -0.02 - - 377 324 -custom-node-1 measure-a custom-node-2 -custom-node-1 measure-a measure-c 324 377 0 - - 324 -custom-node-1 measure-a measure-d 377 324 377 324 0.02 - - 377 324 -custom-node-1 custom-node-5 -measure-e 377 324 0.02 - - -measure-e custom-node-3 -measure-e custom-node-4 " -`; - exports[` Tests StrategySheet Export Tests should export correct data by {formatHeader: false, formatData: true} 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -75,21 +45,6 @@ measure-e custom-node-3 measure-e custom-node-4 " `; -exports[` Tests StrategySheet Export Tests should export correct data by {formatHeader: false, formatData: true} 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -custom-node-1 - -custom-node-1 measure-a 377 3877 4324 0.42 - - 377 -custom-node-1 measure-a measure-b 377 324 377 324 -0.02 - - 377 324 -custom-node-1 measure-a custom-node-2 -custom-node-1 measure-a measure-c 324 377 0 - - 324 -custom-node-1 measure-a measure-d 377 324 377 324 0.02 - - 377 324 -custom-node-1 custom-node-5 -measure-e 377 324 0.02 - - -measure-e custom-node-3 -measure-e custom-node-4 " -`; - exports[` Tests StrategySheet Export Tests should export correct data by custom separator 1`] = ` ",,日期,2022-09,,,2022-10,,2022-11,,,2021年净增完成度,趋势,2022, ,,指标,数值,环比,同比,数值,环比,数值,环比,同比,净增完成度,趋势,数值,环比 @@ -105,21 +60,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E,自定义节点D,,,,,,,,,,,,," `; -exports[` Tests StrategySheet Export Tests should export correct data by custom separator 2`] = ` -",,日期,2022-09,,,2022-10,,2022-11,,,2021年净增完成度,趋势,2022, -,,指标,数值,环比,同比,数值,环比,数值,环比,同比,净增完成度,趋势,数值,环比 -自定义节点A,,,,,,,,,,,,-,, -自定义节点A,指标A,,,,,377,,3877,4324,42%,-,-,377, -自定义节点A,指标A,指标B,,,,377,324,377,324,-0.02,-,-,377,324 -自定义节点A,指标A,自定义节点B,,,,,,,,,,,, -自定义节点A,指标A,指标C,,,,,324,377,0,,-,-,,324 -自定义节点A,指标A,指标D,,,,377,324,377,324,0.02,-,-,377,324 -自定义节点A,自定义节点E,,,,,,,,,,,,, -指标E,,,,,,,,377,324,0.02,-,-,, -指标E,自定义节点C,,,,,,,,,,,,, -指标E,自定义节点D,,,,,,,,,,,,," -`; - exports[` Tests StrategySheet Export Tests should export correct data for custom corner text 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -135,21 +75,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; -exports[` Tests StrategySheet Export Tests should export correct data for custom corner text 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -自定义节点A - -自定义节点A 指标A 377 3877 4324 42% - - 377 -自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 -自定义节点A 指标A 自定义节点B -自定义节点A 指标A 指标C 324 377 0 - - 324 -自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 -自定义节点A 自定义节点E -指标E 377 324 0.02 - - -指标E 自定义节点C -指标E 自定义节点D " -`; - exports[` Tests StrategySheet Export Tests should export correct data for default corner text 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -165,21 +90,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; -exports[` Tests StrategySheet Export Tests should export correct data for default corner text 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -自定义节点A - -自定义节点A 指标A 377 3877 4324 42% - - 377 -自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 -自定义节点A 指标A 自定义节点B -自定义节点A 指标A 指标C 324 377 0 - - 324 -自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 -自定义节点A 自定义节点E -指标E 377 324 0.02 - - -指标E 自定义节点C -指标E 自定义节点D " -`; - exports[` Tests StrategySheet Export Tests should export correct data for empty cell 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -195,21 +105,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; -exports[` Tests StrategySheet Export Tests should export correct data for empty cell 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -自定义节点A - -自定义节点A 指标A 377 3877 4324 42% - - 377 -自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 -自定义节点A 指标A 自定义节点B -自定义节点A 指标A 指标C 324 377 0 - - 324 -自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 -自定义节点A 自定义节点E -指标E 377 324 0.02 - - -指标E 自定义节点C -指标E 自定义节点D " -`; - exports[` Tests StrategySheet Export Tests should export correct data for multi different cycle compare data 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 @@ -225,21 +120,6 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; -exports[` Tests StrategySheet Export Tests should export correct data for multi different cycle compare data 2`] = ` -" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 - 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 -自定义节点A - -自定义节点A 指标A 377 3877 4324 42% - - 377 -自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 -自定义节点A 指标A 自定义节点B -自定义节点A 指标A 指标C 324 377 0 - - 324 -自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 -自定义节点A 自定义节点E -指标E 377 324 0.02 - - -指标E 自定义节点C -指标E 自定义节点D " -`; - exports[` Tests should render correctly row nodes 1`] = ` Array [ Object { @@ -284,48 +164,3 @@ Array [ }, ] `; - -exports[` Tests should render correctly row nodes 2`] = ` -Array [ - Object { - "field": "custom-node-1", - "value": "自定义节点A", - }, - Object { - "field": "measure-a", - "value": "指标A", - }, - Object { - "field": "measure-b", - "value": "指标B", - }, - Object { - "field": "custom-node-2", - "value": "自定义节点B", - }, - Object { - "field": "measure-c", - "value": "指标C", - }, - Object { - "field": "measure-d", - "value": "指标D", - }, - Object { - "field": "custom-node-5", - "value": "自定义节点E", - }, - Object { - "field": "measure-e", - "value": "指标E", - }, - Object { - "field": "custom-node-3", - "value": "自定义节点C", - }, - Object { - "field": "custom-node-4", - "value": "自定义节点D", - }, -] -`; diff --git a/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap index 2c0a0da635..4cd738dcbc 100644 --- a/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap @@ -100,7 +100,7 @@ exports[`Tooltip Common Components Tests render custom react component icon 1`] class="antv-s2-tooltip-operator" >