diff --git a/.github/workflows/source-release.yml b/.github/workflows/source-release.yml index b6d0e57e4b..eab4413df9 100644 --- a/.github/workflows/source-release.yml +++ b/.github/workflows/source-release.yml @@ -87,6 +87,7 @@ jobs: index.d.ts src/ extension-src/ + ssr/client/src/ licenses/ theme/ build/ diff --git a/.gitignore b/.gitignore index ba75419260..34c814925f 100644 --- a/.gitignore +++ b/.gitignore @@ -195,6 +195,7 @@ todo /index.blank.js /extension-esm /extension +/ssr/client/lib /core.js /core.d.ts /charts.js diff --git a/build/build.js b/build/build.js index d6e9cc7a44..fbf6e8820d 100755 --- a/build/build.js +++ b/build/build.js @@ -97,6 +97,12 @@ async function run() { ]; await build(cfgs); } + else if (buildType === 'ssr') { + const cfgs = [ + config.createSSRClient(opt) + ]; + await build(cfgs); + } else if (buildType === 'myTransform') { const cfgs = [ config.createMyTransform(opt) diff --git a/build/config.js b/build/config.js index f07f008058..092f7b23a5 100644 --- a/build/config.js +++ b/build/config.js @@ -174,3 +174,19 @@ exports.createMyTransform = function (opt) { ) }; }; + +exports.createSSRClient = function (opt) { + const input = nodePath.resolve(ecDir, `ssr/client/lib/index.js`); + + return { + plugins: [nodeResolvePlugin()], + input: input, + output: createOutputs( + nodePath.resolve(ecDir, `ssr/client/dist/index`), + opt, + { + name: 'echarts-ssr-client' + } + ) + }; +}; diff --git a/build/pre-publish.js b/build/pre-publish.js index 75d7c771a9..6975c2744b 100644 --- a/build/pre-publish.js +++ b/build/pre-publish.js @@ -67,6 +67,14 @@ const extensionSrcGlobby = { }; const extensionSrcDir = nodePath.resolve(ecDir, 'extension-src'); const extensionESMDir = nodePath.resolve(ecDir, 'extension'); +const ssrClientGlobby = { + patterns: [ + 'ssr/client/src/**/*.ts' + ], + cwd: ecDir +}; +const ssrClientSrcDir = nodePath.resolve(ecDir, 'ssr/client/src'); +const ssrClientESMDir = nodePath.resolve(ecDir, 'ssr/client/lib'); const typesDir = nodePath.resolve(ecDir, 'types'); const esmDir = 'lib'; @@ -135,6 +143,26 @@ const compileWorkList = [ after: async function () { await transformLibFiles(extensionESMDir, 'lib'); } + }, + { + logLabel: 'ssr client ts -> js-esm', + compilerOptionsOverride: { + module: 'ES2015', + declaration: false, + rootDir: ssrClientSrcDir, + outDir: ssrClientESMDir + }, + srcGlobby: ssrClientGlobby, + transformOptions: { + filesGlobby: {patterns: ['**/*.js'], cwd: ssrClientESMDir}, + transformDEV: true + }, + before: async function () { + fsExtra.removeSync(ssrClientESMDir); + }, + after: async function () { + await transformLibFiles(ssrClientESMDir, 'lib'); + } } ]; diff --git a/package-lock.json b/package-lock.json index 3bf8e92202..0e93d4063a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "tslib": "2.3.0", - "zrender": "5.4.4" + "zrender": "npm:zrender-nightly@^5.4.4-dev.20231116" }, "devDependencies": { "@babel/code-frame": "7.10.4", @@ -11098,9 +11098,10 @@ "optional": true }, "node_modules/zrender": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz", - "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==", + "name": "zrender-nightly", + "version": "5.4.4-dev.20231116", + "resolved": "https://registry.npmjs.org/zrender-nightly/-/zrender-nightly-5.4.4-dev.20231116.tgz", + "integrity": "sha512-KcmcgyJV2F9aCxQaxZjG87K0I2zGWyci/ZyGbeiEKGa1Co2NFG5ME5dr1SxLg892RN3FsTzHywbG+sbC5mSfNg==", "dependencies": { "tslib": "2.3.0" } @@ -20051,9 +20052,9 @@ } }, "zrender": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.4.tgz", - "integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==", + "version": "npm:zrender-nightly@5.4.4-dev.20231116", + "resolved": "https://registry.npmjs.org/zrender-nightly/-/zrender-nightly-5.4.4-dev.20231116.tgz", + "integrity": "sha512-KcmcgyJV2F9aCxQaxZjG87K0I2zGWyci/ZyGbeiEKGa1Co2NFG5ME5dr1SxLg892RN3FsTzHywbG+sbC5mSfNg==", "requires": { "tslib": "2.3.0" } diff --git a/package.json b/package.json index 9f9ee2d3ec..5155f4eeba 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,11 @@ "build:i18n": "node build/build-i18n.js", "build:lib": "node build/build.js --prepublish", "build:extension": "node build/build.js --type extension", + "build:ssr": "node build/build.js --type ssr", "dev:fast": "node build/build-i18n.js && node build/dev-fast.js", "dev": "npx -y concurrently -n build,server \"npm run dev:fast\" \"npx -y http-server -c-1 -s -o test\"", "prepare": "npm run build:lib && husky install", - "release": "npm run build:lib && npm run build:i18n && npm run build && npm run build:esm && npm run build:extension", + "release": "npm run build:lib && npm run build:i18n && npm run build && npm run build:esm && npm run build:extension && npm run build:ssr", "help": "node build/build.js --help", "test:visual": "node test/runTest/server.js", "test": "npx jest --config test/ut/jest.config.js", @@ -64,7 +65,7 @@ }, "dependencies": { "tslib": "2.3.0", - "zrender": "5.4.4" + "zrender": "npm:zrender-nightly@^5.4.4-dev.20231116" }, "devDependencies": { "@babel/code-frame": "7.10.4", diff --git a/src/component/legend/LegendView.ts b/src/component/legend/LegendView.ts index 9af5ffb664..e1c3bf7101 100644 --- a/src/component/legend/LegendView.ts +++ b/src/component/legend/LegendView.ts @@ -50,6 +50,7 @@ import {LineStyleProps} from '../../model/mixin/lineStyle'; import {createSymbol, ECSymbol} from '../../util/symbol'; import SeriesModel from '../../model/Series'; import { createOrUpdatePatternFromDecal } from '../../util/decal'; +import { getECData } from '../../util/innerStore'; const curry = zrUtil.curry; const each = zrUtil.each; @@ -225,6 +226,13 @@ class LegendView extends ComponentView { .on('mouseover', curry(dispatchHighlightAction, seriesModel.name, null, api, excludeSeriesId)) .on('mouseout', curry(dispatchDownplayAction, seriesModel.name, null, api, excludeSeriesId)); + itemGroup.eachChild(child => { + const ecData = getECData(child); + ecData.seriesIndex = seriesModel.seriesIndex; + ecData.dataIndex = dataIndex; + ecData.ssrType = 'legend'; + }); + legendDrawnMap.set(name, true); } else { @@ -269,6 +277,13 @@ class LegendView extends ComponentView { .on('mouseover', curry(dispatchHighlightAction, null, name, api, excludeSeriesId)) .on('mouseout', curry(dispatchDownplayAction, null, name, api, excludeSeriesId)); + itemGroup.eachChild(child => { + const ecData = getECData(child); + ecData.seriesIndex = seriesModel.seriesIndex; + ecData.dataIndex = dataIndex; + ecData.ssrType = 'legend'; + }); + legendDrawnMap.set(name, true); } @@ -430,7 +445,10 @@ class LegendView extends ComponentView { // Add a invisible rect to increase the area of mouse hover const hitRect = new graphic.Rect({ shape: itemGroup.getBoundingRect(), - invisible: true + style: { + // Cannot use 'invisible' because SVG SSR will miss the node + fill: 'transparent' + } }); const tooltipModel = diff --git a/src/core/echarts.ts b/src/core/echarts.ts index 7757dd22fc..aaf538c72e 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -427,6 +427,15 @@ class ECharts extends Eventful { } + zrender.registerSSRDataGetter(el => { + const ecData = getECData(el); + const hashMap = createHashMap(); + hashMap.set('series_index', ecData.seriesIndex); + hashMap.set('data_index', ecData.dataIndex); + hashMap.set('ssr_type', ecData.ssrType); + return hashMap; + }); + const zr = this._zr = zrender.init(dom, { renderer: opts.renderer || defaultRenderer, devicePixelRatio: opts.devicePixelRatio, diff --git a/src/util/innerStore.ts b/src/util/innerStore.ts index dbaaecf040..a1ecb2a78c 100644 --- a/src/util/innerStore.ts +++ b/src/util/innerStore.ts @@ -23,6 +23,9 @@ import { ComponentMainType, ComponentItemTooltipOption } from './types'; import { makeInner } from './model'; + +export type SSRItemType = 'chart' | 'legend'; + /** * ECData stored on graphic element */ @@ -34,6 +37,7 @@ export interface ECData { dataType?: SeriesDataType; focus?: InnerFocus; blurScope?: BlurScope; + ssrType?: SSRItemType; // Required by `tooltipConfig` and `focus`. componentMainType?: ComponentMainType; @@ -62,6 +66,7 @@ export const setCommonECData = (seriesIndex: number, dataType: SeriesDataType, d ecData.dataIndex = dataIdx; ecData.dataType = dataType; ecData.seriesIndex = seriesIndex; + ecData.ssrType = 'chart'; // TODO: not store dataIndex on children. if (el.type === 'group') { @@ -70,6 +75,7 @@ export const setCommonECData = (seriesIndex: number, dataType: SeriesDataType, d childECData.seriesIndex = seriesIndex; childECData.dataIndex = dataIdx; childECData.dataType = dataType; + childECData.ssrType === 'chart'; }); } } diff --git a/ssr/client/dist/index.js b/ssr/client/dist/index.js new file mode 100644 index 0000000000..818242b77c --- /dev/null +++ b/ssr/client/dist/index.js @@ -0,0 +1,109 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['echarts-ssr-client'] = {})); +}(this, (function (exports) { 'use strict'; + + /** + * AUTO-GENERATED FILE. DO NOT MODIFY. + */ + + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + function hydrate(dom, options) { + var svgRoot = dom.querySelector('svg'); + + if (!svgRoot) { + console.error('No SVG element found in the DOM.'); + return; + } + + var children = svgRoot.children; + + function getIndex(child, attr) { + var index = child.getAttribute(attr); + + if (index) { + return parseInt(index, 10); + } else { + return null; + } + } + + var events = options.on; + + if (events) { + var _loop_1 = function (eventName) { + if (typeof events[eventName] === 'function') { + var _loop_2 = function (i) { + var child = children[i]; + var type = child.getAttribute('ecmeta_ssr_type'); + var silent = child.getAttribute('ecmeta_silent') === 'true'; + + if (type && !silent) { + child.addEventListener(eventName, function (e) { + events[eventName]({ + type: eventName, + ssrType: type, + seriesIndex: getIndex(child, 'ecmeta_series_index'), + dataIndex: getIndex(child, 'ecmeta_data_index'), + event: e + }); + }); + } + }; + + for (var i = 0; i < children.length; i++) { + _loop_2(i); + } + } + }; + + for (var eventName in events) { + _loop_1(eventName); + } + } + } + + exports.hydrate = hydrate; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); +//# sourceMappingURL=index.js.map diff --git a/ssr/client/dist/index.js.map b/ssr/client/dist/index.js.map new file mode 100644 index 0000000000..e4fabbce7b --- /dev/null +++ b/ssr/client/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sources":["../lib/index.js"],"sourcesContent":["\n/**\n * AUTO-GENERATED FILE. DO NOT MODIFY.\n */\n\n/*\n* Licensed to the Apache Software Foundation (ASF) under one\n* or more contributor license agreements. See the NOTICE file\n* distributed with this work for additional information\n* regarding copyright ownership. The ASF licenses this file\n* to you under the Apache License, Version 2.0 (the\n* \"License\"); you may not use this file except in compliance\n* with the License. You may obtain a copy of the License at\n*\n* http://www.apache.org/licenses/LICENSE-2.0\n*\n* Unless required by applicable law or agreed to in writing,\n* software distributed under the License is distributed on an\n* \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n* KIND, either express or implied. See the License for the\n* specific language governing permissions and limitations\n* under the License.\n*/\nexport function hydrate(dom, options) {\n var svgRoot = dom.querySelector('svg');\n\n if (!svgRoot) {\n console.error('No SVG element found in the DOM.');\n return;\n }\n\n var children = svgRoot.children;\n\n function getIndex(child, attr) {\n var index = child.getAttribute(attr);\n\n if (index) {\n return parseInt(index, 10);\n } else {\n return null;\n }\n }\n\n var events = options.on;\n\n if (events) {\n var _loop_1 = function (eventName) {\n if (typeof events[eventName] === 'function') {\n var _loop_2 = function (i) {\n var child = children[i];\n var type = child.getAttribute('ecmeta_ssr_type');\n var silent = child.getAttribute('ecmeta_silent') === 'true';\n\n if (type && !silent) {\n child.addEventListener(eventName, function (e) {\n events[eventName]({\n type: eventName,\n ssrType: type,\n seriesIndex: getIndex(child, 'ecmeta_series_index'),\n dataIndex: getIndex(child, 'ecmeta_data_index'),\n event: e\n });\n });\n }\n };\n\n for (var i = 0; i < children.length; i++) {\n _loop_2(i);\n }\n }\n };\n\n for (var eventName in events) {\n _loop_1(eventName);\n }\n }\n}"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;EACA;EACA;EACA;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,SAAS,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE;EACtC,EAAE,IAAI,OAAO,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACzC;EACA,EAAE,IAAI,CAAC,OAAO,EAAE;EAChB,IAAI,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;EACtD,IAAI,OAAO;EACX,GAAG;AACH;EACA,EAAE,IAAI,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AAClC;EACA,EAAE,SAAS,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE;EACjC,IAAI,IAAI,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACzC;EACA,IAAI,IAAI,KAAK,EAAE;EACf,MAAM,OAAO,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;EACjC,KAAK,MAAM;EACX,MAAM,OAAO,IAAI,CAAC;EAClB,KAAK;EACL,GAAG;AACH;EACA,EAAE,IAAI,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;AAC1B;EACA,EAAE,IAAI,MAAM,EAAE;EACd,IAAI,IAAI,OAAO,GAAG,UAAU,SAAS,EAAE;EACvC,MAAM,IAAI,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,UAAU,EAAE;EACnD,QAAQ,IAAI,OAAO,GAAG,UAAU,CAAC,EAAE;EACnC,UAAU,IAAI,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;EAClC,UAAU,IAAI,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;EAC3D,UAAU,IAAI,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;AACtE;EACA,UAAU,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE;EAC/B,YAAY,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE;EAC3D,cAAc,MAAM,CAAC,SAAS,CAAC,CAAC;EAChC,gBAAgB,IAAI,EAAE,SAAS;EAC/B,gBAAgB,OAAO,EAAE,IAAI;EAC7B,gBAAgB,WAAW,EAAE,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;EACnE,gBAAgB,SAAS,EAAE,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;EAC/D,gBAAgB,KAAK,EAAE,CAAC;EACxB,eAAe,CAAC,CAAC;EACjB,aAAa,CAAC,CAAC;EACf,WAAW;EACX,SAAS,CAAC;AACV;EACA,QAAQ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;EAClD,UAAU,OAAO,CAAC,CAAC,CAAC,CAAC;EACrB,SAAS;EACT,OAAO;EACP,KAAK,CAAC;AACN;EACA,IAAI,KAAK,IAAI,SAAS,IAAI,MAAM,EAAE;EAClC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;EACzB,KAAK;EACL,GAAG;EACH;;;;;;"} \ No newline at end of file diff --git a/ssr/client/src/index.ts b/ssr/client/src/index.ts new file mode 100644 index 0000000000..d56a1e323d --- /dev/null +++ b/ssr/client/src/index.ts @@ -0,0 +1,74 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +export interface ECSSRClientEventParams {} + +export interface ECSSRClientOptions { + on?: { + mouseover?: (params: ECSSRClientEventParams) => void, + mouseout?: (params: ECSSRClientEventParams) => void, + click?: (params: ECSSRClientEventParams) => void + } +} + +export type ECSSREvent = 'mouseover' | 'mouseout' | 'click'; + +export function hydrate(dom: HTMLElement, options: ECSSRClientOptions) { + const svgRoot = dom.querySelector('svg'); + if (!svgRoot) { + console.error('No SVG element found in the DOM.'); + return; + } + + const children = svgRoot.children; + + function getIndex(child: Element, attr: string) { + const index = child.getAttribute(attr); + if (index) { + return parseInt(index, 10); + } + else { + return null; + } + } + + const events = options.on; + if (events) { + for (let eventName in events) { + if (typeof events[eventName as ECSSREvent] === 'function') { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const type = child.getAttribute('ecmeta_ssr_type'); + const silent = child.getAttribute('ecmeta_silent') === 'true'; + if (type && !silent) { + child.addEventListener(eventName, e => { + (events[eventName as ECSSREvent] as Function)({ + type: eventName, + ssrType: type, + seriesIndex: getIndex(child, 'ecmeta_series_index'), + dataIndex: getIndex(child, 'ecmeta_data_index'), + event: e, + }); + }); + } + } + } + } + } +} diff --git a/test/lib/config.js b/test/lib/config.js index bc36037e79..462b31420a 100644 --- a/test/lib/config.js +++ b/test/lib/config.js @@ -82,6 +82,7 @@ 'map': 'data/map', 'i18n': '../i18n', 'extension': '../dist/extension', + 'ssrClient': '../ssr/client/dist/index.js' } }); } diff --git a/test/ssr.html b/test/ssr.html new file mode 100644 index 0000000000..7516f7cbcb --- /dev/null +++ b/test/ssr.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + +

+ 1. It should have initial animation
+ 2. It should use lighter color when hovering on the bars of a and b series
+ 3. It should toggle the series when clicking the legend
+ 4. It should update the series when clicking the bars
+ 5. The third series is silent so it cannot be lighter when hovering
+

+
+ +

Big data render time

+
+ + + + + +