数据库 Echart 使用摸索(萌新版)

一点 js 也不懂的纯萌新

参考了两位前辈的代码喂给了 AI,我用的 deepseek 帮写代码,稍微微调一下即可食用。

image.png


条目统计

(async function() { // =============== 用户配置区域 =============== const avBlockId = '20240822122302-wi2jx0n'; // 数据库块ID const chartBlockId = '20250530150522-1y6vetn'; // 图表块ID const chartTitle = '数据库条目统计'; // 图表标题 const autoFreshDelay = 1000; // 自动刷新延迟(毫秒) // 显示样式配置 const valueFontSize = 64; // 数字字体大小 const titleFontSize = 18; // 标题字体大小 const subtitleFontSize = 14; // 副标题字体大小 const chartColor = '#5470c6'; // 主色调 // =============== 图表配置 =============== let option = { title: [{ text: chartTitle, left: 'center', top: '10%', textStyle: { fontSize: titleFontSize, fontWeight: 'normal' } }, { text: '加载中...', left: 'center', top: '85%', textStyle: { fontSize: subtitleFontSize, color: '#999' } }], graphic: [{ type: 'circle', shape: { cx: 0.5, cy: 0.5, r: 100 }, style: { fill: 'transparent', stroke: chartColor, lineWidth: 2, opacity: 0.3 }, left: 'center', top: 'center' }, { type: 'text', left: 'center', top: 'center', style: { text: '0', fontSize: valueFontSize, fontWeight: 'bold', fill: chartColor, textAlign: 'center', textVerticalAlign: 'middle' } }] }; // =============== 数据处理逻辑 =============== await getAVDataByBlockId(avBlockId, (av) => { try { const totalCount = av.keyValues?.[0]?.values?.length || 0; // 更新数字显示 option.graphic[1].style.text = totalCount.toString(); // 更新副标题 option.title[1].text = `共 ${totalCount} 条记录`; console.log(`数据库条目统计完成,共 ${totalCount} 条记录`); } catch (e) { console.error('数据处理失败:', e); option.title[1].text = '数据加载失败: ' + e.message; option.title[1].textStyle.color = '#ff4d4f'; } }); // =============== 以下为通用函数 =============== // 获取数据库信息 async function getAVDataByBlockId(blockId, callback) { try { const block = await fetchSyncPost('/api/query/sql', { "stmt": `SELECT * FROM blocks WHERE id = '${blockId}'` }); const markdown = block.data[0]?.markdown; if (!markdown) throw new Error(`未找到ID为 ${blockId} 的数据库块`); const avId = getDataAvIdFromHtml(markdown); if (!avId) throw new Error(`在数据库块中未找到有效的av-id`); const av = await fetchSyncPost('/api/file/getFile', { "path": `/data/storage/av/${avId}.json` }); if (av && typeof callback === 'function') callback(av); else throw new Error(`未找到av-id=${avId}的数据库文件`); } catch (e) { console.error('获取数据库失败:', e); option.title[1].text = '数据库加载失败: ' + e.message; option.title[1].textStyle.color = '#ff4d4f'; } } // 获取avid function getDataAvIdFromHtml(htmlString) { const match = htmlString.match(/data-av-id="([^"]+)"/); return match && match[1] ? match[1] : ""; } // API请求 async function fetchSyncPost(url, data) { const init = { method: "POST" }; init.body = data instanceof FormData ? data : JSON.stringify(data); const res = await fetch(url, init); return await res.json(); } // 刷新图表 async function freshChart(chartBlockId) { const ZWSP = "\u200b"; const looseJsonParse = (text) => Function(`"use strict";return (${text})`)(); const chartElement = document.querySelector(`.layout__center div[data-subtype="echarts"][data-node-id="${chartBlockId}"]`); if (!chartElement) return; let width; if (chartElement.firstElementChild.clientWidth === 0) { const tabElement = hasClosestByClassName(chartElement, "layout-tab-container", true); if (tabElement) { const visibleTab = Array.from(tabElement.children).find( item => item.classList.contains("protyle") && !item.classList.contains("fn__none") ); if (visibleTab) { width = visibleTab.querySelector(".protyle-wysiwyg")?.firstElementChild?.clientWidth; } } } const wysiswgElement = hasClosestByClassName(chartElement, "protyle-wysiwyg", true); if (!chartElement.firstElementChild.classList.contains("protyle-icons")) { chartElement.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement)); } const renderElement = chartElement.firstElementChild.nextElementSibling; try { renderElement.style.height = chartElement.style.height; const chartOption = await looseJsonParse(Lute.UnEscapeHTMLStr(chartElement.getAttribute("data-content"))); window.echarts.init( renderElement, window.siyuan.config.appearance.mode === 1 ? "dark" : undefined, { width } ).setOption(chartOption); chartElement.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: <br>${error}`; } } // DOM辅助函数 function hasClosestByClassName(element, className, top = false) { if (!element) return false; if (element.nodeType === 3) element = element.parentElement; let e = element; while (e && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) { if (e.classList?.contains(className)) return e; e = e.parentElement; } return null; } function genIconHTML(element) { const enable = element?.getAttribute("contenteditable") === "true"; return `<div class="protyle-icons"> <span class="protyle-icon protyle-icon--first protyle-action__edit${enable ? '' : ' fn__none'}"> <svg><use xlink:href="#iconEdit"></use></svg> </span> <span class="protyle-icon protyle-action__menu protyle-icon--last${enable ? '' : ' fn__none'}"> <svg><use xlink:href="#iconMore"></use></svg> </span> </div>`; } // 监听数据库变化 if (autoFreshDelay > 0 && !window[`__chart_observe__${avBlockId}`]) { const targetNode = document.querySelector(`.layout__center div[data-node-id="${avBlockId}"]`); if (targetNode) { window[`__chart_observe__${avBlockId}`] = observeDOMChanges( targetNode, () => freshChart(chartBlockId), autoFreshDelay, { attributes: false, childList: true, subtree: true } ); } } // DOM变化监听器 function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) { const config = { attributes: false, childList: true, subtree: true, ...options }; const observer = new MutationObserver(() => { clearTimeout(observer.timer); observer.timer = setTimeout(callback, debounceTime); }); observer.observe(targetNode, config); return () => observer.disconnect(); } // 返回配置对象 return option; })()

统计多选项数量并生成饼图

(async function() { // =============== 用户配置区域 =============== const avBlockId = '20240822122302-wi2jx0n'; // 数据库块ID const tagColumn = '标签'; // 多选标签列名称 const chartBlockId = '20250530155754-eq67341'; // 图表块ID const chartTitle = '标签分布统计'; // 图表标题 const autoFreshDelay = 1000; // 自动刷新延迟(毫秒) // 饼图样式配置 const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4']; const labelFontSize = 14; // 标签字体大小 const valueFontSize = 16; // 数值字体大小 const legendFontSize = 12; // 图例字体大小 const minPercentage = 2; // 最小显示百分比(低于此值合并为"其他") // =============== 图表配置 =============== let option = { title: { text: chartTitle, left: 'center', textStyle: { fontSize: 18 } }, tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { fontSize: legendFontSize } }, series: [{ name: '标签分布', type: 'pie', radius: ['40%', '70%'], center: ['40%', '50%'], avoidLabelOverlap: true, itemStyle: { borderColor: '#fff', borderWidth: 2 }, label: { show: true, formatter: function(params) { return `${params.name}\n${params.value}次 (${params.percent}%)`; }, fontSize: labelFontSize, fontWeight: 'bold' }, emphasis: { label: { show: true, fontSize: valueFontSize, fontWeight: 'bold' } }, labelLine: { show: true }, data: [] // 将在数据处理后填充 }] }; // =============== 数据处理逻辑 =============== await getAVDataByBlockId(avBlockId, (av) => { try { // 1. 获取标签列数据 const tagColumnData = av.keyValues?.find(kv => kv.key?.name === tagColumn); if (!tagColumnData?.values) { throw new Error(`未找到标签列: ${tagColumn}`); } // 2. 统计标签频率 const tagFrequency = {}; let totalTags = 0; tagColumnData.values.forEach(tagSet => { if (tagSet?.mSelect?.length > 0) { tagSet.mSelect.forEach(tag => { const tagName = tag.content; tagFrequency[tagName] = (tagFrequency[tagName] || 0) + 1; totalTags++; }); } }); // 3. 转换格式并排序 let tagData = Object.entries(tagFrequency) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); // 4. 处理小百分比标签(合并为"其他") if (minPercentage > 0) { const otherTags = []; let otherCount = 0; tagData = tagData.filter(item => { const percentage = (item.value / totalTags) * 100; if (percentage >= minPercentage) { return true; } else { otherCount += item.value; otherTags.push(item.name); return false; } }); if (otherCount > 0) { tagData.push({ name: `其他 (${otherTags.length}个标签)`, value: otherCount }); } } // 5. 应用颜色 tagData = tagData.map((item, index) => ({ ...item, itemStyle: { color: colors[index % colors.length] } })); // 6. 更新图表数据 option.series[0].data = tagData; option.title.text = `${chartTitle} (共${totalTags}个标签)`; console.log(`标签统计完成,共${Object.keys(tagFrequency).length}种标签`); } catch (e) { console.error('数据处理失败:', e); option.title.text = '数据加载失败'; option.series[0].data = []; option.graphic = { type: 'text', left: 'center', top: 'middle', style: { text: `错误: ${e.message}`, fill: '#ff4d4f', fontSize: 16 } }; } }); // =============== 通用函数 (与之前相同) =============== // 获取数据库信息 async function getAVDataByBlockId(blockId, callback) { try { const block = await fetchSyncPost('/api/query/sql', { "stmt": `SELECT * FROM blocks WHERE id = '${blockId}'` }); const markdown = block.data[0]?.markdown; if (!markdown) throw new Error(`未找到ID为 ${blockId} 的数据库块`); const avId = getDataAvIdFromHtml(markdown); if (!avId) throw new Error(`在数据库块中未找到有效的av-id`); const av = await fetchSyncPost('/api/file/getFile', { "path": `/data/storage/av/${avId}.json` }); if (av && typeof callback === 'function') callback(av); else throw new Error(`未找到av-id=${avId}的数据库文件`); } catch (e) { console.error('获取数据库失败:', e); option.title.text = '数据库加载失败'; option.series[0].data = []; option.graphic = { type: 'text', left: 'center', top: 'middle', style: { text: `错误: ${e.message}`, fill: '#ff4d4f', fontSize: 16 } }; } } // 获取avid function getDataAvIdFromHtml(htmlString) { const match = htmlString.match(/data-av-id="([^"]+)"/); return match && match[1] ? match[1] : ""; } // API请求 async function fetchSyncPost(url, data) { const init = { method: "POST" }; init.body = data instanceof FormData ? data : JSON.stringify(data); const res = await fetch(url, init); return await res.json(); } // 刷新图表 async function freshChart(chartBlockId) { const ZWSP = "\u200b"; const looseJsonParse = (text) => Function(`"use strict";return (${text})`)(); const chartElement = document.querySelector(`.layout__center div[data-subtype="echarts"][data-node-id="${chartBlockId}"]`); if (!chartElement) return; let width; if (chartElement.firstElementChild.clientWidth === 0) { const tabElement = hasClosestByClassName(chartElement, "layout-tab-container", true); if (tabElement) { const visibleTab = Array.from(tabElement.children).find( item => item.classList.contains("protyle") && !item.classList.contains("fn__none") ); if (visibleTab) { width = visibleTab.querySelector(".protyle-wysiwyg")?.firstElementChild?.clientWidth; } } } const wysiswgElement = hasClosestByClassName(chartElement, "protyle-wysiwyg", true); if (!chartElement.firstElementChild.classList.contains("protyle-icons")) { chartElement.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement)); } const renderElement = chartElement.firstElementChild.nextElementSibling; try { renderElement.style.height = chartElement.style.height; const chartOption = await looseJsonParse(Lute.UnEscapeHTMLStr(chartElement.getAttribute("data-content"))); window.echarts.init( renderElement, window.siyuan.config.appearance.mode === 1 ? "dark" : undefined, { width } ).setOption(chartOption); chartElement.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: <br>${error}`; } } // DOM辅助函数 function hasClosestByClassName(element, className, top = false) { if (!element) return false; if (element.nodeType === 3) element = element.parentElement; let e = element; while (e && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) { if (e.classList?.contains(className)) return e; e = e.parentElement; } return null; } function genIconHTML(element) { const enable = element?.getAttribute("contenteditable") === "true"; return `<div class="protyle-icons"> <span class="protyle-icon protyle-icon--first protyle-action__edit${enable ? '' : ' fn__none'}"> <svg><use xlink:href="#iconEdit"></use></svg> </span> <span class="protyle-icon protyle-action__menu protyle-icon--last${enable ? '' : ' fn__none'}"> <svg><use xlink:href="#iconMore"></use></svg> </span> </div>`; } // 监听数据库变化 if (autoFreshDelay > 0 && !window[`__chart_observe__${avBlockId}`]) { const targetNode = document.querySelector(`.layout__center div[data-node-id="${avBlockId}"]`); if (targetNode) { window[`__chart_observe__${avBlockId}`] = observeDOMChanges( targetNode, () => freshChart(chartBlockId), autoFreshDelay, { attributes: false, childList: true, subtree: true } ); } } // DOM变化监听器 function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) { const config = { attributes: false, childList: true, subtree: true, ...options }; const observer = new MutationObserver(() => { clearTimeout(observer.timer); observer.timer = setTimeout(callback, debounceTime); }); observer.observe(targetNode, config); return () => observer.disconnect(); } // 返回配置对象 return option; })()
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    26178 引用 • 108738 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 281 关注
  • 正则表达式

    正则表达式(Regular Expression)使用单个字符串来描述、匹配一系列遵循某个句法规则的字符串。

    31 引用 • 94 回帖 • 2 关注
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    92 引用 • 752 回帖 • 3 关注
  • 996
    13 引用 • 200 回帖 • 3 关注
  • 书籍

    宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”

    82 引用 • 412 回帖
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用 • 4 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 1 关注
  • OneNote
    1 引用 • 3 回帖
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 179 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖 • 1 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 248 回帖 • 3 关注
  • Mobi.css

    Mobi.css is a lightweight, flexible CSS framework that focus on mobile.

    1 引用 • 6 回帖 • 765 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖 • 2 关注
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    35 引用 • 468 回帖 • 767 关注
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖 • 2 关注
  • 大疆创新

    深圳市大疆创新科技有限公司(DJI-Innovations,简称 DJI),成立于 2006 年,是全球领先的无人飞行器控制系统及无人机解决方案的研发和生产商,客户遍布全球 100 多个国家。通过持续的创新,大疆致力于为无人机工业、行业用户以及专业航拍应用提供性能最强、体验最佳的革命性智能飞控产品和解决方案。

    2 引用 • 14 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖
  • Excel
    31 引用 • 28 回帖
  • OpenCV
    15 引用 • 36 回帖 • 2 关注
  • Vditor

    Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript、Vue、React 和 Angular。

    372 引用 • 1857 回帖
  • AWS
    11 引用 • 28 回帖 • 8 关注
  • PWL

    组织简介

    用爱发电 (Programming With Love) 是一个以开源精神为核心的民间开源爱好者技术组织,“用爱发电”象征开源与贡献精神,加入组织,代表你将遵守组织的“个人开源爱好者”的各项条款。申请加入:用爱发电组织邀请帖
    用爱发电组织官网:https://programmingwithlove.stackoverflow.wiki/

    用爱发电组织的核心驱动力:

    • 遵守开源守则,体现开源&贡献精神:以分享为目的,拒绝非法牟利。
    • 自我保护:使用适当的 License 保护自己的原创作品。
    • 尊重他人:不以各种理由、各种漏洞进行未经允许的抄袭、散播、洩露;以礼相待,尊重所有对社区做出贡献的开发者;通过他人的分享习得知识,要留下足迹,表示感谢。
    • 热爱编程、热爱学习:加入组织,热爱编程是首当其要的。我们欢迎热爱讨论、分享、提问的朋友,也同样欢迎默默成就的朋友。
    • 倾听:正确并恳切对待、处理问题与建议,及时修复开源项目的 Bug ,及时与反馈者沟通。不抬杠、不无视、不辱骂。
    • 平视:不诋毁、轻视、嘲讽其他开发者,主动提出建议、施以帮助,以和谐为本。只要他人肯努力,你也可能会被昔日小看的人所超越,所以请保持谦虚。
    • 乐观且活跃:你的努力决定了你的高度。不要放弃,多年后回头俯瞰,才会发现自己已经成就往日所仰望的水平。积极地将项目开源,帮助他人学习、改进,自己也会获得相应的提升、成就与成就感。
    1 引用 • 487 回帖 • 3 关注
  • 导航

    各种网址链接、内容导航。

    45 引用 • 177 回帖
  • SQLite

    SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是全世界使用最为广泛的数据库引擎。

    4 引用 • 7 回帖
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    42 引用 • 130 回帖 • 253 关注
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    336 引用 • 324 回帖
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 647 关注