Wilson@思源

目 录

图表和数据库联动

see https://ld246.com/article/1722828811581
类似 https://ld246.com/article/1740558177875
类似 https://ld246.com/article/1741622377103 表格和数据库联动
当数据库更新数据时,图表会自动更新数据哦。
效果如下
原理:通过 echarts 中的脚本,动态获取数据库块的数据,然后把数据再格式化为图表的数据,就可以了。同时会监听数据库块的变化,当有数据更新时,图表会刷新并重新获取数据库的数据。
完整代码如下(代码输入到 echarts 块中)(由于链滴字数限制,代码只能放到 jsurn 了)
js
(async () => { // 关联的数据库块id,这里的id根据需要改成自己的 const avBlockId = '20240727220211-p1awn4b'; // 关联的图表块id,这里的id根据需要改成自己的 const chartBlockId = '20240805130000-vhz1aei'; // 自动刷新延迟,单位是毫秒,默认是1秒,0则不自动刷新 const autoFreshDelay = 1000; // 定义图表选项,这里的数据是什么都没关系,这里只是参考数据,下面的getAVDataByBlockId函数会覆盖这里的数据 let option = { "title": { "text": "网站访问统计", "left": "center", "top": "top" }, "tooltip": { "trigger": "axis" }, "xAxis": { "type": "category", "data": [ "第一季度", "第二季度", "第三季度", "第四季度" ], "axisLabel": { "rotate": 0 } }, "yAxis": { "type": "value" }, "series": [ { "data": [ 1495, 1678, 1750, 1096 ], "type": "bar", "barWidth": 30, "label": { "show": true, "position": "top" }, "name": "2020" }, { "data": [ 1208, 1225, 1098, 1326 ], "type": "bar", "barWidth": 30, "label": { "show": true, "position": "top" }, "name": "2021" }, { "data": [ 2548, 1574, 2534, 1038 ], "type": "bar", "barWidth": 30, "label": { "show": true, "position": "top" }, "name": "2022" } ], "legend": { "data": [ "2020", "2021", "2022" ], "left": "center", "top": "bottom" } } // 获取数据库信息并格式化数据,这里av是从数据库获取的数据 await getAVDataByBlockId(avBlockId, (av) => { // 修改选项标题 option.title.text = av.name; // 修改option x 轴文字 option.xAxis.data = av.keyValues[0].values.map(item => item.block.content); // 修改option series 数据 option.series = av.keyValues.slice(1).map(item => { const data = item.values.map(item => item[item.type].content); return { "data": data, "type": "bar", "barWidth": 30, "label": { "show": true, "position": "top" }, "name": item.key.name } }); }); ////////////////////////////////// 以下代码不涉及数据配置,一般不需要改动 //////////////////////////////////// // 监听av变化,当数据库块被修改时,重新获取数据 if(autoFreshDelay > 0 && !window['__chat_observe__' + avBlockId]) { window['__chat_observe__' + avBlockId] = observeDOMChanges(document.querySelector('.layout__center div[data-node-id="'+avBlockId+'"]'), ()=>{ freshChart(chartBlockId); }, autoFreshDelay, {attributes: false}); } // 输出运行状态,方便调试 console.log('render chart start'); // 获取数据库信息并格式化数据 async function getAVDataByBlockId(blockId, callback) { // 获取块信息 const block = await fetchSyncPost('/api/query/sql', {"stmt": `SELECT * FROM blocks WHERE id = '${blockId}'`}) const markdown = block.data[0]?.markdown; // 获取数据库信息 if(markdown){ // 获取数据库文件地址 const avId = getDataAvIdFromHtml(markdown); // 通过sy文件获取表格数据,按列排列,这里更合适 const av = await fetchSyncPost('/api/file/getFile', {"path":`/data/storage/av/${avId}.json`}); // 通过renderAttributeView获取表格数据,按行排列 //const av = await fetchSyncPost('/api/av/renderAttributeView', {"id":avId,"viewID":"","query":""}); // 格式化数据选项 if(av){ if(typeof callback === 'function') callback(av); } else { option = "未找到av-id=" + avId + "的数据库文件"; } } else { option = "未找到id=" + avBlockId + "的数据库块"; } } // 请求api async function fetchSyncPost (url, data) { const init = { method: "POST", }; if (data) { if (data instanceof FormData) { init.body = data; } else { init.body = JSON.stringify(data); } } const res = await fetch(url, init); const res2 = await res.json(); return res2; } // 获取avid function getDataAvIdFromHtml(htmlString) { // 使用正则表达式匹配data-av-id的值 const match = htmlString.match(/data-av-id="([^"]+)"/); if (match && match[1]) { return match[1]; // 返回匹配的值 } return ""; // 如果没有找到匹配项,则返回空 } // 刷新图表 async function freshChart(chartBlockId) { const ZWSP = "\u200b"; const looseJsonParse = (text) => { return Function(`"use strict";return (${text})`)(); }; const e = document.querySelector('.layout__center div[data-subtype="echarts"][data-node-id="'+chartBlockId+'"]') let width = undefined; if (e.firstElementChild.clientWidth === 0) { const tabElement = hasClosestByClassName(e, "layout-tab-container", true); if (tabElement) { Array.from(tabElement.children).find(item => { if (item.classList.contains("protyle") && !item.classList.contains("fn__none")) { width = item.querySelector(".protyle-wysiwyg").firstElementChild.clientWidth; return true; } }); } } const wysiswgElement = hasClosestByClassName(e, "protyle-wysiwyg", true); if (!e.firstElementChild.classList.contains("protyle-icons")) { e.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement)); } const renderElement = e.firstElementChild.nextElementSibling; try { renderElement.style.height = e.style.height; const option = await looseJsonParse(Lute.UnEscapeHTMLStr(e.getAttribute("data-content"))); window.echarts.init(renderElement, window.siyuan.config.appearance.mode === 1 ? "dark" : undefined, {width}).setOption(option); e.setAttribute("data-render", "true"); renderElement.classList.remove("ft__error"); if (!renderElement.textContent.endsWith(ZWSP)) { renderElement.firstElementChild.insertAdjacentText("beforeend", ZWSP); } } catch (error) { window.echarts.dispose(renderElement); renderElement.classList.add("ft__error"); renderElement.innerHTML = `echarts render error:
${error}`; } } function hasClosestByClassName(element, className, top = false) { if (!element) { return false; } if (element.nodeType === 3) { element = element.parentElement; } let e = element; let isClosest = false; while (e && !isClosest && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) { if (e.classList?.contains(className)) { isClosest = true; } else { e = e.parentElement; } } return isClosest && e; } function genIconHTML(element) { let enable = true; if (element) { const readonly = element.getAttribute("contenteditable"); if (typeof readonly === "string") { enable = element.getAttribute("contenteditable") === "true"; } else { return '
'; } } return `
`; } // 监听dom变化 let observeTimer = null; function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) { // 默认配置 const defaultOptions = { attributes: true, childList: true, subtree: true, }; // 合并默认配置与传入的配置 const config = Object.assign({}, defaultOptions, options); // 创建一个观察器实例 const observer = new MutationObserver((mutationsList) => { // 使用防抖函数确保单位时间内最多只调用一次回调 if(observeTimer) { clearTimeout(observeTimer); } observeTimer = setTimeout(() => { // 处理变化 callback(mutationsList); }, debounceTime); }); // 开始观察目标节点 observer.observe(targetNode, config); // 返回一个函数,以便在不需要时能够停止观察 return () => { observer.disconnect(); }; } return option; })()
注意:option 里的默认数据是没影响的,这里仅供自己参考用的,执行时,getAVDataByBlockId​ 函数会覆盖这里的数据。
使用:
1.
新建 chart 块,然后打开 chart 编辑对话框,把上述代码复制进去
2.
然后分别复制数据库块的 ID 和 Chart 块 ID,然后替换 avBlockId​ 变量和 chartBlockId​ 变量的值为刚才复制的块 ID 就好了。