From 218ce4608dd7da7a5288bce1c6f95d71d8aa9c5f Mon Sep 17 00:00:00 2001 From: Jae Sung Park Date: Fri, 20 Sep 2024 19:29:43 +0900 Subject: [PATCH] feat(render): Add forced init option on lazy rendering Make to initialize when chart element isn't visible, but render.lazy=false is set Fix #3106 --- src/Chart/Chart.ts | 11 ++++++++++ src/ChartInternal/ChartInternal.ts | 6 +++--- src/config/Options/common/main.ts | 5 ++++- src/module/error.ts | 18 ++++++++++------- src/module/util.ts | 23 +++++++++++++++++++++ test/assets/module/util.ts | 1 + test/internals/bb-spec.ts | 32 +++++++++++++++++++++++++++--- 7 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/Chart/Chart.ts b/src/Chart/Chart.ts index f9ef2e8f0..f545c9836 100644 --- a/src/Chart/Chart.ts +++ b/src/Chart/Chart.ts @@ -110,6 +110,7 @@ export default class Chart { constructor(options) { const $$ = new ChartInternal(this); + // let hook = () => {}; this.internal = $$; @@ -120,6 +121,10 @@ export default class Chart { const isChild = target !== argThis; const isNotNil = notEmpty(fn[key]); const hasChild = isNotNil && Object.keys(fn[key]).length > 0; + // const hookFn = function(...params) { + // hook(); + // return fn[key].bind(argThis)(...params); + // } if (isFunc && ((!isChild && hasChild) || isChild)) { target[key] = fn[key].bind(argThis); @@ -137,6 +142,12 @@ export default class Chart { $$.beforeInit(); $$.init(); + + // if ($$.config.render.lazy !== false && hasStyle($$.$el.chart, {"display": "none", "visibility": "hidden"})) { + // hook = () => { + // logError(`The call of APIs won't work. Please, make sure if chart element is %cvisible.`); + // }; + // } } } diff --git a/src/ChartInternal/ChartInternal.ts b/src/ChartInternal/ChartInternal.ts index 1fb38f79e..be2fcc8e9 100644 --- a/src/ChartInternal/ChartInternal.ts +++ b/src/ChartInternal/ChartInternal.ts @@ -25,6 +25,7 @@ import { extend, getOption, getRandom, + hasStyle, isFunction, isObject, isString, @@ -253,10 +254,9 @@ export default class ChartInternal { initToRender(forced?: boolean): void { const $$ = this; const {config, state, $el: {chart}} = $$; - const isHidden = () => - chart.style("display") === "none" || chart.style("visibility") === "hidden"; + const isHidden = () => hasStyle(chart, {display: "none", visibility: "hidden"}); - const isLazy = config.render.lazy || isHidden(); + const isLazy = config.render.lazy === false ? false : config.render.lazy || isHidden(); const MutationObserver = window.MutationObserver; if (isLazy && MutationObserver && config.render.observe !== false && !forced) { diff --git a/src/config/Options/common/main.ts b/src/config/Options/common/main.ts index a021ea96c..bef0a5b27 100644 --- a/src/config/Options/common/main.ts +++ b/src/config/Options/common/main.ts @@ -350,7 +350,10 @@ export default { * @memberof Options * @type {object} * @property {object} [render] render object - * @property {boolean} [render.lazy=true] Make to not render at initialization (enabled by default when bind element's visibility is hidden). + * @property {boolean} [render.lazy=true] Make to not render at initialization. + * - **NOTE**: + * - Enabled by default when bind element's visibility is hidden. + * - When set to `false`, will initialize the chart regardless the bind element's visibility state, but in this case chart can't be guaranteed to be rendered properly. * @property {boolean} [render.observe=true] Observe bind element's visibility(`display` or `visiblity` inline css property or class value) & render when is visible automatically (for IEs, only works IE11+). When set to **false**, call [`.flush()`](./Chart.html#flush) to render. * @see [Demo](https://naver.github.io/billboard.js/demo/#ChartOptions.LazyRender) * @example diff --git a/src/module/error.ts b/src/module/error.ts index 25542588c..4b6c1fbae 100644 --- a/src/module/error.ts +++ b/src/module/error.ts @@ -34,26 +34,30 @@ function checkModuleImport(ctx) { type && logError(`Please, make sure if %c${camelize(type)}`, - "module has been imported and specified correctly."); + "module has been imported and specified correctly.", + "https://github.com/naver/billboard.js/wiki/CHANGELOG-v2#modularization-by-its-functionality"); } /** * Log error and throw error * @param {string} head Message header * @param {string} tail Message tail + * @param {string} info Info message * @private */ -function logError(head, tail) { +function logError(head, tail?: string, info?: string) { const prefix = "[billboard.js]"; - const info = - "https://github.com/naver/billboard.js/wiki/CHANGELOG-v2#modularization-by-its-functionality"; const hasConsole = window.console?.error; if (hasConsole) { + const tailMsg = tail ? + ["background:red;color:white;display:block;font-size:15px", tail] : + []; + console.error(`❌ ${prefix} ${head}`, - "background:red;color:white;display:block;font-size:15px", tail); - console.info("%cℹ️", "font-size:15px", info); + "background:red;color:white;display:block;font-size:15px", ...tailMsg); + info && console.info("%cℹ️", "font-size:15px", info); } - throw Error(`${prefix} ${head.replace(/\%c([a-z-]+)/i, "'$1' ")} ${tail}`); + throw Error(`${prefix} ${head.replace(/\%c([a-z-]+)/i, "'$1' ")} ${tail ?? ""}`); } diff --git a/src/module/util.ts b/src/module/util.ts index 5f24f443d..89281fc00 100644 --- a/src/module/util.ts +++ b/src/module/util.ts @@ -37,6 +37,7 @@ export { getTransformCTM, getTranslation, getUnique, + hasStyle, hasValue, hasViewBox, isArray, @@ -817,6 +818,28 @@ function hasViewBox(svg: d3Selection): boolean { return attr ? /(\d+(\.\d+)?){3}/.test(attr) : false; } +/** + * Determine if given node has the specified style + * @param {d3Selection|SVGElement} node Target node + * @param {object} condition Conditional style props object + * @param {boolean} all If true, all condition should be matched + * @returns {boolean} + */ +function hasStyle(node, condition: {[key: string]: string}, all = false): boolean { + const isD3Node = !!node.node; + let has = false; + + for (const [key, value] of Object.entries(condition)) { + has = isD3Node ? node.style(key) === value : node.style[key] === value; + + if (all === false && has) { + break; + } + } + + return has; +} + /** * Return if the current doc is visible or not * @returns {boolean} diff --git a/test/assets/module/util.ts b/test/assets/module/util.ts index 87040cd8f..f28ff7da1 100644 --- a/test/assets/module/util.ts +++ b/test/assets/module/util.ts @@ -35,6 +35,7 @@ export const { getTranslation, getTransformCTM, getUnique, + hasStyle, hasValue, hasViewBox, isArray, diff --git a/test/internals/bb-spec.ts b/test/internals/bb-spec.ts index a2b921ad6..cf586962c 100644 --- a/test/internals/bb-spec.ts +++ b/test/internals/bb-spec.ts @@ -518,7 +518,7 @@ describe("Interface & initialization", () => { })); it("check lazy rendering on callbacks", () => new Promise(done => { - const el: any = document.body.querySelector("#chart"); + const el = document.body.querySelector("#chart"); // hide to lazy render el.style.display = "none"; @@ -528,7 +528,9 @@ describe("Interface & initialization", () => { expect(el.innerHTML).to.be.empty; // onresize, resized shouldn't be called on resize - chart.resize({width: 500}); + expect( + chart.resize({width: 500}) + ).to.throw; for (let x in spy) { expect(spy[x].called).to.be.false; @@ -550,7 +552,7 @@ describe("Interface & initialization", () => { expect(spy.resized.called).to.be.true; done(1); - }, 300); + }, 300); }, 300); }), 4000); @@ -582,6 +584,30 @@ describe("Interface & initialization", () => { done(1); }, 300); })); + + it("should forcely linitialize even chart element visibility is hidden.", () =>{ + const el = document.body.querySelector("#chart"); + + // hide to lazy render + el.style.display = "none"; + + chart = util.generate({ + data: { + columns: [ + ["data1", 300, 350, 300, 0, 0, 0], + ["data2", 130, 100, 140, 200, 150, 50] + ], + type: "line" + }, + render: { + lazy: false + } + }); + + expect(chart.$.svg.node().innerHTML).to.be.not.empty; + + el.style.display = ""; + }); }); describe("check for background", () => {