diff --git a/src/echarts/addons/Gk0Wk/GitHubHeatMap/GitHubHeatMap.ts b/src/echarts/addons/Gk0Wk/GitHubHeatMap/GitHubHeatMap.ts index 53e45a1..f31b451 100644 --- a/src/echarts/addons/Gk0Wk/GitHubHeatMap/GitHubHeatMap.ts +++ b/src/echarts/addons/Gk0Wk/GitHubHeatMap/GitHubHeatMap.ts @@ -56,15 +56,29 @@ const checkIfDarkMode = () => 'color-scheme' ] === 'dark'; -const GitHubHeatMapAddon: IScriptAddon = { - shouldUpdate: (_, changedTiddlers) => $tw.utils.count(changedTiddlers) > 0, - onUpdate: (myChart, _state, addonAttributes) => { +const GitHubHeatMapAddon: IScriptAddon<{ filter: string }> = { + onMount: (_, attr) => { + return { + filter: attr.subfilter || '[all[tiddlers]!is[shadow]!is[system]]', + }; + }, + shouldUpdate: (state, changedTiddlers, changedAttributes, attributes) => { + state.filter = + attributes.subfilter || + state.filter || + '[all[tiddlers]!is[shadow]!is[system]]'; + const t = ($tw.wiki as any).makeTiddlerIterator( + Object.keys(changedTiddlers), + ); + return ( + $tw.utils.count($tw.wiki.filterTiddlers(state.filter, undefined, t)) > 0 + ); + }, + onUpdate: (myChart, state, addonAttributes) => { const year = parseInt(addonAttributes.year, 10) || new Date().getFullYear(); - const subfilter = - addonAttributes.subfilter || '[all[tiddlers]!is[shadow]!is[system]]'; /** Use subfilter to narrow down tiddler pool before the array.map on dates */ const tiddlerSourceIterator = ($tw.wiki as any).makeTiddlerIterator( - $tw.wiki.filterTiddlers(subfilter), + $tw.wiki.filterTiddlers(state.filter), ); const [data, total] = getData(year, tiddlerSourceIterator); const tooltipFormatter = (dateValue: string, count: number) => { diff --git a/src/echarts/addons/Gk0Wk/TheBrain/TheBrain.ts b/src/echarts/addons/Gk0Wk/TheBrain/TheBrain.ts index 1a97d14..6d4c81f 100755 --- a/src/echarts/addons/Gk0Wk/TheBrain/TheBrain.ts +++ b/src/echarts/addons/Gk0Wk/TheBrain/TheBrain.ts @@ -107,7 +107,19 @@ interface ITheBrainState { unmount: () => void; } -const TheBrainAddon: IScriptAddon = { +interface ITheBrainAttributes { + focussedTiddler?: string; + levels?: string; + graphTitle?: string; + aliasField?: string; + excludeFilter?: string; + previewDelay?: string; + focusBlur?: string; + previewTemplate?: string; + zoom?: string; +} + +const TheBrainAddon: IScriptAddon = { onMount: (myChart, attributes) => { myChart.on('click', { dataType: 'node' }, (event: any) => { new $tw.Story().navigateTiddler(event.data.name); @@ -202,35 +214,20 @@ const TheBrainAddon: IScriptAddon = { ); }, // eslint-disable-next-line complexity - onUpdate: ( - myCharts, - state, - addonAttributes: { - focussedTiddler?: string; - levels?: number; - graphTitle?: string; - aliasField?: string; - excludeFilter?: string; - previewDelay?: string; - focusBlur?: string; - previewTemplate?: string; - zoom?: string; - }, - ) => { + onUpdate: (myCharts, state, addonAttributes) => { /** 参数:focussedTiddler 是图的中央节点 */ let focussedTiddlers = new Set(); - if (addonAttributes.focussedTiddler) { - for (const title of $tw.wiki.filterTiddlers( - addonAttributes.focussedTiddler, - )) { - focussedTiddlers.add(title); - } - } else { - const t = $tw.wiki.getTiddlerText('$:/temp/focussedTiddler'); - if (t) { - focussedTiddlers.add(t); + const titles = addonAttributes.focussedTiddler + ? $tw.wiki.filterTiddlers(addonAttributes.focussedTiddler) + : [$tw.wiki.getTiddlerText('$:/temp/focussedTiddler') ?? '']; + for (const title of titles) { + // 跳过正在编辑的条目 + if ($tw.wiki.getTiddler(title)?.fields?.['draft.of']) { + continue; } + focussedTiddlers.add(title); } + if (focussedTiddlers.size === 0) { return; } diff --git a/src/echarts/plugin.info b/src/echarts/plugin.info index 4abbf75..526f659 100755 --- a/src/echarts/plugin.info +++ b/src/echarts/plugin.info @@ -1,5 +1,5 @@ { - "version": "0.2.6", + "version": "0.2.7", "type": "application/json", "title": "$:/plugins/Gk0Wk/echarts", "plugin-type": "plugin", diff --git a/src/echarts/scriptAddon.d.ts b/src/echarts/scriptAddon.d.ts index 9c0e832..b9ebdcf 100644 --- a/src/echarts/scriptAddon.d.ts +++ b/src/echarts/scriptAddon.d.ts @@ -1,5 +1,5 @@ import type { ECharts } from 'echarts'; -import type { Widget } from 'tiddlywiki'; +import type { Widget, IChangedTiddlers } from 'tiddlywiki'; export interface IScriptAddon< StateType = any, @@ -22,17 +22,17 @@ export interface IScriptAddon< * @param {StateType} state 组件的状态,就是onMount返回的那个 * @param {IChangedTiddlers} changedTiddlers 刷新是由TW系统监听到有条目发生变化才会触发的,这是一个包含所有变更条目标题的字符串数组 * @param {Record} changedAttributes 哪些参数被改变了,包括$开头的参数 + * @param {AttributesType} addonAttributes <$echarts> 控件传入的所有参数 * @return {boolean} 如果需要刷新就返回true,反之 * * shouldRefresh 也可以是一个字符串,那就和 echarts-refresh-trigger 字段一样 */ - shouldUpdate?: - | (( - state: StateType, - changedTiddlers: IChangedTiddlers, - changedAttributes: Record, - ) => boolean) - | boolean; + shouldUpdate?: ( + state: StateType, + changedTiddlers: IChangedTiddlers, + changedAttributes: Record, + addonAttributes: AttributesType, + ) => boolean; /** * 当组件被更新时调用的函数 * @param {ECharts} myChart echarts实例,详见echarts的API文档 diff --git a/src/echarts/widget/index.ts b/src/echarts/widget/index.ts index cbc6dc5..0c419be 100755 --- a/src/echarts/widget/index.ts +++ b/src/echarts/widget/index.ts @@ -9,7 +9,6 @@ import type { IScriptAddon } from '../scriptAddon'; import { widget as Widget } from '$:/core/modules/widgets/widget.js'; import * as ECharts from '$:/plugins/Gk0Wk/echarts/echarts.min.js'; -const echartWidgets: Set = new Set(); const Function_ = Function; if ($tw.browser) { // 总算明白了,node启动时,这个会被调用一遍,在浏览器又会调用一遍 @@ -35,7 +34,16 @@ if ($tw.browser) { } catch (error) { console.error(error); } - setInterval(() => { +} + +const echartWidgets: Set = new Set(); +let eChartsInstanceUnmountCheckTimer: NodeJS.Timer | undefined; +const registerInstance = (instance: EChartsWidget) => { + if (!$tw.browser || eChartsInstanceUnmountCheckTimer !== undefined) { + return; + } + echartWidgets.add(instance); + eChartsInstanceUnmountCheckTimer = setInterval(() => { const deletingWidget: EChartsWidget[] = []; for (const widget of echartWidgets) { if (!document.contains(widget.containerDom)) { @@ -48,12 +56,15 @@ if ($tw.browser) { deletingWidget.push(widget); } } - const len = deletingWidget.length; - for (let i = 0; i < len; i++) { - echartWidgets.delete(deletingWidget[i]); + for (const echartWidget of echartWidgets) { + echartWidgets.delete(echartWidget); + } + if (echartWidgets.size < 1) { + eChartsInstanceUnmountCheckTimer = undefined; + clearInterval(eChartsInstanceUnmountCheckTimer); } }, 1000); -} +}; const unmountAddon = ( title: string | undefined, @@ -95,7 +106,9 @@ class EChartsWidget extends Widget { resizeObserver?: ResizeObserver; echartsInstance?: ECharts.ECharts; timer?: NodeJS.Timeout; + tmpChangedTiddlers?: IChangedTiddlers; throttle!: number; + skipDraftTiddle: boolean = true; addon?: { init: () => void; @@ -136,10 +149,18 @@ class EChartsWidget extends Widget { this.renderer = this.getAttribute('$renderer', 'canvas') === 'svg' ? 'svg' : 'canvas'; this.text = this.getAttribute('$text', '').trim() || undefined; - this.throttle = Math.max( - ($tw.utils as any).getAnimationDuration() || 100, - 100, - ); + + // 设置去抖 + const throttleText = this.getAttribute('$throttle'); + if (throttleText) { + const t = parseInt(throttleText, 10); + this.throttle = Number.isSafeInteger(t) ? Math.max(0, 10) : 1000; + } else { + this.throttle = 1000; + } + + this.skipDraftTiddle = + this.getAttribute('$skipDraftTiddle', 'true') !== 'false'; } render(parent: HTMLElement, nextSibling: HTMLElement) { @@ -182,7 +203,7 @@ class EChartsWidget extends Widget { this.echartsInstance! as any ).renderToSVGString(); } else { - echartWidgets.add(this); + registerInstance(this); } } catch (error) { console.error(error); @@ -194,81 +215,102 @@ class EChartsWidget extends Widget { } refresh(changedTiddlers: IChangedTiddlers) { - // 去抖 - if (this.timer) { - clearTimeout(this.timer); + if (this.timer !== undefined) { + // 说明已经有一个正在节流的定时器了,那么就把这次的变更合并进去 + if (this.tmpChangedTiddlers !== undefined) { + this.tmpChangedTiddlers = { + ...this.tmpChangedTiddlers, + ...changedTiddlers, + }; + } else { + this.tmpChangedTiddlers = changedTiddlers; + } + return; } + // 先做一次 + this.refresh_(changedTiddlers); + // 然后节流 + let count = 5; this.timer = setTimeout(() => { - this.timer = undefined; - const oldAddonTitle = this.tiddlerTitle; - const changedAttributes = this.computeAttributes(); - let refreshFlag = 0; // 0: 不需要任何变更 1: 需要重新生成Option 2: 需要重新渲染 - // 先看一下参数的变化,这里分为几种: - // $tiddler变化的,说明要重新生成Option - // $theme、$fillSidebar 和 $renderer需要重新初始化实例 - // $class、$width 和 $height 只需要修改容器的尺寸就好了 - // 剩下的就是传给插件的参数了 - if ($tw.utils.count(changedAttributes) > 0) { - let counter = 0; - $tw.utils.each(['$theme', '$fillSidebar', '$renderer'], key => { - if (changedAttributes[key] !== undefined) { - counter++; - } - }); - if (counter > 0) { - refreshFlag |= 2; - } - if (changedAttributes.$class) { - counter++; - this.class = this.getAttribute('$class', 'gk0wk-echarts-body'); - this.containerDom.className = this.class; - } - if (changedAttributes.$width) { - counter++; - this.width = this.getAttribute('$width', '100%'); - this.containerDom.style.width = this.width; - } - if (changedAttributes.$height) { + if (count-- <= 0 && this.tmpChangedTiddlers === undefined) { + clearInterval(this.timer); + this.timer = undefined; + } + if (this.tmpChangedTiddlers !== undefined) { + this.refresh_(this.tmpChangedTiddlers); + this.tmpChangedTiddlers = undefined; + } + }, this.throttle); + } + + refresh_(changedTiddlers: IChangedTiddlers) { + const oldAddonTitle = this.tiddlerTitle; + const changedAttributes = this.computeAttributes(); + let refreshFlag = 0; // 0: 不需要任何变更 1: 需要重新生成Option 2: 需要重新渲染 + // 先看一下参数的变化,这里分为几种: + // $tiddler变化的,说明要重新生成Option + // $theme、$fillSidebar 和 $renderer需要重新初始化实例 + // $class、$width 和 $height 只需要修改容器的尺寸就好了 + // 剩下的就是传给插件的参数了 + if ($tw.utils.count(changedAttributes) > 0) { + let counter = 0; + $tw.utils.each(['$theme', '$fillSidebar', '$renderer'], key => { + if (changedAttributes[key] !== undefined) { counter++; - this.height = this.getAttribute('$height', '300px'); - this.containerDom.style.height = this.height; - } - if ($tw.utils.count(changedAttributes) > counter) { - refreshFlag |= 1; } + }); + if (counter > 0) { + refreshFlag |= 2; } - if ( - this.text === undefined && - !(refreshFlag & 1) && - ((this.tiddlerTitle && changedTiddlers[this.tiddlerTitle]) || - this.askForAddonUpdate(changedTiddlers, changedAttributes)) - ) { - refreshFlag |= 1; + if (changedAttributes.$class) { + counter++; + this.class = this.getAttribute('$class', 'gk0wk-echarts-body'); + this.containerDom.className = this.class; } - // 检查自动主题时,黑暗模式是否切换了 - const oldTheme = this.theme; - this.execute(); - if (oldTheme !== this.theme) { - refreshFlag |= 2; + if (changedAttributes.$width) { + counter++; + this.width = this.getAttribute('$width', '100%'); + this.containerDom.style.width = this.width; } - if (refreshFlag & 2) { - const oldOption = this.rebuildInstance(); - if (!oldOption || refreshFlag & 1) { - unmountAddon( - this.text !== undefined ? undefined : oldAddonTitle, - this.state, - this.echartsInstance!, - ); - this.initAddon(); - this.renderAddon(); - } else { - this.echartsInstance!.setOption(oldOption); - } - } else if (refreshFlag & 1) { + if (changedAttributes.$height) { + counter++; + this.height = this.getAttribute('$height', '300px'); + this.containerDom.style.height = this.height; + } + if ($tw.utils.count(changedAttributes) > counter) { + refreshFlag |= 1; + } + } + if ( + this.text === undefined && + !(refreshFlag & 1) && + ((this.tiddlerTitle && changedTiddlers[this.tiddlerTitle]) || + this.askForAddonUpdate(changedTiddlers, changedAttributes)) + ) { + refreshFlag |= 1; + } + // 检查自动主题时,黑暗模式是否切换了 + const oldTheme = this.theme; + this.execute(); + if (oldTheme !== this.theme) { + refreshFlag |= 2; + } + if (refreshFlag & 2) { + const oldOption = this.rebuildInstance(); + if (!oldOption || refreshFlag & 1) { + unmountAddon( + this.text !== undefined ? undefined : oldAddonTitle, + this.state, + this.echartsInstance!, + ); + this.initAddon(); this.renderAddon(); + } else { + this.echartsInstance!.setOption(oldOption); } - }, this.throttle); - return false; + } else if (refreshFlag & 1) { + this.renderAddon(); + } } askForAddonUpdate( @@ -280,6 +322,10 @@ class EChartsWidget extends Widget { return false; } const tiddler = $tw.wiki.getTiddler(this.tiddlerTitle)!.fields; + // 忽略草稿 + if (this.skipDraftTiddle && tiddler['draft.of']) { + return false; + } // 懒加载模式,还在加载,要等待 if ( '_is_skinny' in tiddler && @@ -288,22 +334,36 @@ class EChartsWidget extends Widget { return false; } const type = tiddler.type || 'text/vnd.tiddlywiki'; - if (type === 'text/vnd.tiddlywiki' || type === 'application/json') { + const typeInfo = $tw.config.contentTypeInfo[type] ?? {}; + const deserializerType = typeInfo.deserializerType ?? type; + if ( + deserializerType === 'text/vnd.tiddlywiki' || + deserializerType === 'application/json' + ) { this._state = JSON.stringify( $tw.wiki.filterTiddlers(tiddler['echarts-refresh-trigger'] as string), ); return this._state !== this.state; - } else if (type === 'application/javascript') { + } else if (deserializerType === 'application/javascript') { const _addon = require(this.tiddlerTitle); const addon = (_addon.default ?? _addon) as IScriptAddon; - const shouldUpdate = addon.shouldUpdate ?? (addon as any).shouldRefresh; + const shouldUpdate = + addon.shouldUpdate ?? + ((addon as any).shouldRefresh as + | IScriptAddon['shouldUpdate'] + | undefined); if (shouldUpdate === undefined) { return true; } else if (typeof shouldUpdate === 'string') { this._state = JSON.stringify($tw.wiki.filterTiddlers(shouldUpdate)); return this._state !== this.state; } else if (typeof shouldUpdate === 'function') { - return shouldUpdate(this.state, changedTiddlers, changedAttributes); + return shouldUpdate( + this.state, + changedTiddlers, + changedAttributes, + this.attributes, + ); } return true; } else { @@ -396,7 +456,12 @@ class EChartsWidget extends Widget { return; } const type = tiddler.type || 'text/vnd.tiddlywiki'; - if (type === 'text/vnd.tiddlywiki' || type === 'application/json') { + const typeInfo = $tw.config.contentTypeInfo[type] ?? {}; + const deserializerType = typeInfo.deserializerType ?? type; + if ( + deserializerType === 'text/vnd.tiddlywiki' || + deserializerType === 'application/json' + ) { this.state = this._state ?? JSON.stringify( @@ -405,7 +470,7 @@ class EChartsWidget extends Widget { ), ); this._state = undefined; - } else if (type === 'application/javascript') { + } else if (deserializerType === 'application/javascript') { const _addon = require(this.tiddlerTitle); const addon = (_addon.default ?? _addon) as IScriptAddon; const onMount = addon.onMount ?? (addon as any).onInit; @@ -444,7 +509,9 @@ class EChartsWidget extends Widget { return; } const type = tiddler.type || 'text/vnd.tiddlywiki'; - if (type === 'text/vnd.tiddlywiki') { + const typeInfo = $tw.config.contentTypeInfo[type] ?? {}; + const deserializerType = typeInfo.deserializerType ?? type; + if (deserializerType === 'text/vnd.tiddlywiki') { const plainTextContent = $tw.wiki.renderTiddler( 'text/plain', this.tiddlerTitle, @@ -457,11 +524,11 @@ class EChartsWidget extends Widget { `return (${plainTextContent})`, )(); this.echartsInstance.setOption(executedJSContent); - } else if (type === 'application/json') { + } else if (deserializerType === 'application/json') { this.echartsInstance.setOption( JSON.parse($tw.wiki.getTiddlerText(this.tiddlerTitle)!), ); - } else if (type === 'application/javascript') { + } else if (deserializerType === 'application/javascript') { const _addon = require(this.tiddlerTitle); const addon = (_addon.default ?? _addon) as IScriptAddon; addon.onUpdate(this.echartsInstance, this.state, this.attributes); diff --git a/wiki/tiddlers/How do I use the plugin_.tid b/wiki/tiddlers/How do I use the plugin_.tid index e72d0ee..5dbf3a1 100755 --- a/wiki/tiddlers/How do I use the plugin_.tid +++ b/wiki/tiddlers/How do I use the plugin_.tid @@ -25,6 +25,7 @@ Like other widgets, the `echarts` widget has a number of attributes. |$fillSidebar |Whether to automatically adjust the height to fill the sidebar when the wdiget is in the sidebar, default is `true` | |$theme |ECharts theme, `light` for bright theme, `dark` for dark theme, `auto` (default) for automatic judgment | |$renderer |Rendering method, `canvas` (default) or `svg` | +|$throttle | Integer, in milliseconds, used to control the refresh rate, used to prevent the browser from lagging due to too frequent refreshes, default is `1000` | |Others |If `$tiddler` points to an tiddler of type JS, it will be used as an argument to its `onUpdate` function | !! <$text text="$text"/>: The most beginner-friendly way diff --git "a/wiki/tiddlers/\346\210\221\350\257\245\345\246\202\344\275\225\344\275\277\347\224\250\350\257\245\346\217\222\344\273\266_.tid" "b/wiki/tiddlers/\346\210\221\350\257\245\345\246\202\344\275\225\344\275\277\347\224\250\350\257\245\346\217\222\344\273\266_.tid" index a7f29c9..6f165c6 100755 --- "a/wiki/tiddlers/\346\210\221\350\257\245\345\246\202\344\275\225\344\275\277\347\224\250\350\257\245\346\217\222\344\273\266_.tid" +++ "b/wiki/tiddlers/\346\210\221\350\257\245\345\246\202\344\275\225\344\275\277\347\224\250\350\257\245\346\217\222\344\273\266_.tid" @@ -25,6 +25,7 @@ type: text/vnd.tiddlywiki |$fillSidebar |当控件位于侧边栏时,是否自动调整高度以填满侧边栏,默认为`true` | |$theme |ECharts的主题,`light`为亮色主题,`dark`为暗色主题,`auto`(默认)为自动判断 | |$renderer |渲染方式,`canvas`(默认)或者`svg` | +|$throttle | 整数,单位为毫秒,用于控制刷新频率,用于防止刷新过于频繁导致浏览器卡顿,默认为`1000` | |其他参数 |如果`$tiddler`指向JS类型的条目,将会作为其`onUpdate`函数的参数 | !! <$text text="$text"/>: 对初学者最友好的方式 @@ -117,7 +118,7 @@ macro 也有`width`等可选参数,这里就不赘述了。 $tiddler="$:/plugins/Gk0Wk/echarts/addons/BrainMap" $height="500px" dblclick="(params, parentWidget) => { console.log(params); parentWidget.dispatchEvent({ type: 'tm-navigate', navigateTo: params.data.name }) }" -/> +/> ``` <$echarts