唐朝诗人群像可视化:从数据建模到ECharts图谱的工程实践
2026/9/17 17:09:23 网站建设 项目流程

简介:这是一份面向计算机设计大赛参赛者与数字人文可视化初学者的完整项目源码包,来自“诗意千年——唐朝诗人群像的数字展现”作品。项目整合诗人传记、诗歌文本与历史文献,通过数字技术将唐朝诗人的生平与创作历程转化为可交互的视觉叙事,适合作为文化传承类项目的参考样板,也可用于学习数据可视化与前端交互实现。压缩包共109个文件,总大小44.68MB,文件类型覆盖完整前端工程:81个PNG图片提供诗人画像、背景及词云图基础素材,9个JS脚本处理动态渲染与交互逻辑,6个HTML页面搭建多场景展示结构,4个CSS文件完成视觉排版,另有4个JSON存放结构化数据、4个JPG辅助图片及1个说明文档,打开即可对照理解项目架构。当前已有160人学习浏览。获取源码后,可复用其页面设计、词云图展示、样式主题与数据组织方式,帮助快速搭建同类数字人文可视化项目。

1. 数字人文项目的技术拆解:唐朝诗人群像到底在做什么

拿到“唐朝诗人群像的数字展现”这类计算机设计大赛作品,多数人第一反应是去看界面炫不炫、动画多不多,但真正决定作品上限的,往往是看不见的数据层和交互链路。这个项目本质上是一个数字人文(Digital Humanities)可视化应用:把诗人的生平、作品、社交关系、时代背景整理成结构化数据,再用前端图表引擎渲染成可交互的关系图谱、时间轴和作品索引。它面向的受众是历史爱好者和评委,但背后使用的技术栈和普通中后台系统没有本质区别。本文不讨论这个 zip 包本身是否完美,而是顺着“数字展现”这条主线索,把这类项目从选型、建模、渲染到交付的完整工程路径讲清楚。无论是准备参赛、做课程设计,还是想在企业内部做文化类数据看板,这套思路都能直接复用。

2. 技术选型与数据建模:把诗人、作品和关系变成可查询的 JSON

2.1 可视化项目的技术栈选择与取舍

做数字人文类项目,常见的技术路线有三条:第一条是纯前端方案,用 Vue 或 React 配合 ECharts、D3.js 做全部渲染,数据放在静态 JSON 或本地 SQLite 里,优点是部署简单、适合比赛演示;第二条是前后端分离,用 Flask 或 Node.js 提供 API,前端负责展示,适合数据量大、需要动态筛选的场景;第三条是基于 Notebook 的分析型方案,用 Python 处理完数据后导出 HTML 快照,交互性较弱但开发速度最快。

对于“唐朝诗人群像”这种中等规模数据集,我一般推荐第一种为主、第二种为辅。原因很直接:诗人数量通常在一两百人以内,诗歌几千首,完全可以用静态 JSON 承载;但为了支持“按时期筛选”“按关系跳转”这类交互,纯静态方案需要在前端写大量过滤逻辑,反而比加一个薄 API 层更麻烦。折中的做法是:数据准备阶段用 Python 脚本清洗生成 JSON,前端通过 fetch 加载,本地起一个静态服务器调试,最终打包时把数据内联进构建产物。

技术选型的具体建议如下表:

模块推荐方案替代方案选型理由
前端框架Vue 3 + ViteReact + Webpack模板语法直观,选手上手快,Vite 启动快
图谱渲染ECharts 5D3.js / AntV G6内置力导向布局,配置简单,文档齐全
数据格式JSON + 索引文件SQLite + APIJSON 可读性好,便于提交源码时直接查看
样式方案Tailwind CSS原生 CSS快速搭建图文混排页面

2.2 诗人、作品与社交关系的数据模型设计

数据建模是“数字展现”项目最容易翻车的环节。很多人一上来就写前端,结果做到关系图谱时发现缺少“师徒关系”“同年出生”这类边信息,被迫返工。为了避免这个问题,我习惯在写任何 UI 代码之前,先用 TypeScript 接口或 JSON Schema 把数据结构固定下来。诗人节点至少需要以下字段:id(唯一标识)、name(姓名)、birth_yeardeath_year(负数为公元前)、period(初唐/盛唐/中唐/晚唐)、tags(标签数组,如“山水田园”“边塞”)、poem_count(存诗数量)、bio(精简人物小传)、代表作列表。关系边则至少包含sourcetargetrelation(如“师徒”“好友”“同僚”)和description

需要注意的是,年份字段不要用字符串存,比如“约712年”这种值无法参与时间轴计算。我建议拆成birth_year_approxbirth_year_uncertain两个字段,前者存可计算的数值,后者标记是否精确。这样做时间轴缩放和区间筛选时就不会被脏数据卡住。诗歌数据可以单独建一个集合,每首诗关联一个author_id,如果作品有多个版本,用variant_of字段指向主版本。

2.3 用 Python 脚本清洗和生成图谱 JSON

数据清洗通常是这类项目里最耗时的一步。来源可能是古诗文网爬虫结果、公开的数据集或者手动录入的 Excel,字段命名不统一、年份有缺失、人名有别名。我一般会写一个一次性脚本,把原始数据读入 pandas DataFrame,做标准化处理后输出三个文件:poets.jsonpoems.jsonrelations.json。下面是一个清洗脚本的核心片段:

import pandas as pd import json # 原始数据: 每行一位诗人, 包含生卒年、字、号、派别 df = pd.read_csv("raw_poets.csv") # 补全省略的公元纪年 df["birth_year"] = df["birth_year"].fillna(0) # 时期划分: 按生年或主要活动年份归类 def assign_period(row): y = row["birth_year"] if y < 650: return "初唐" elif y < 713: return "盛唐" elif y < 790: return "中唐" else: return "晚唐" df["period"] = df.apply(assign_period, axis=1) # 输出精简字段 poets = [] for _, r in df.iterrows(): poets.append({ "id": r["id"], "name": r["name"], "birth_year": int(r["birth_year"]), "period": r["period"], "tags": [t.strip() for t in r["tags"].split("|")], }) with open("public/data/poets.json", "w", encoding="utf-8") as f: json.dump(poets, f, ensure_ascii=False, indent=2)

这段代码做的事情是把原始 CSV 里的诗人数据标准化成前端可直接消费的 JSON。fillna(0)的作用是处理“生年不详”的情况,0 在时间轴上不会参与连线;assign_period按生年粗分四唐,实际项目中如果知道诗人的主要活动时期,应该优先覆盖该字段。ensure_ascii=False是关键参数,不设置的话中文会被转成\uXXXX,虽然浏览器能正常解析,但提交源码后评委打开 JSON 文件会看到满屏转义字符,体验很差。

3. 关系图谱与时间轴:唐朝诗人群像的核心界面实现

3.1 用 ECharts 力导向图渲染诗人社交网络

图谱是这类数字人文项目的主视觉。ECharts 的graph系列内置了力导向布局,只需要提供nodeslinks两个数组就能渲染出可拖拽的关系网。但在实际项目中,直接扔全量数据会有两个问题:第一,节点数量超过 80 个时,力导向布局会非常慢,拖拽卡顿;第二,所有节点挤在一起,标签互相遮挡,视觉上完全不可读。所以要做一个分层处理:默认只展示核心节点(存诗数量前 40 位)+ 它们之间的关系,其余节点在用户点击“展开”时增量加载。

下面给出一个最小可用的配置示例:

const chart = echarts.init(document.getElementById("graph")); const option = { tooltip: { formatter: (params) => { if (params.dataType === "node") { const p = params.data; return `<b>${p.name}</b><br/>${p.period}${p.birth_year || "生年不详"}`; } return params.data.relation; } }, series: [{ type: "graph", layout: "force", data: nodes, links: links, roam: true, draggable: true, label: { show: true, position: "right", formatter: (p) => p.data.name }, force: { repulsion: 150, edgeLength: 80, gravity: 0.1 }, lineStyle: { color: "#aaa", width: 1, curveness: 0.2 } }] }; chart.setOption(option);

这里layout: "force"开启力导向布局,repulsion控制节点之间的斥力大小,值越大节点越分散。edgeLength是理想边长,太小会导致关系密集的节点重叠。curveness给边加一点弧度,当两个诗人之间存在多条关系(比如既是好友又是同僚)时,弧线能避免线条完全重合。roam: true允许用户缩放和平移画布,这个是评委最常试的交互,不要漏掉。

3.2 时间轴联动:按年代切片观察诗人群体的流动

单纯的关系图谱只能回答“谁和谁认识”,回答不了“盛唐时期诗人群体是如何聚拢的”这个问题。所以数字人文项目几乎都要配一条可拖拽的时间轴。实现思路是:准备一批预计算好的“年代快照”,每个快照包含截止到该年份存活的诗人列表以及他们之间的关系。拖动时间轴时,动态更新chart.setOptiondatalinks

const slider = document.getElementById("timeline"); const yearLabel = document.getElementById("year"); slider.addEventListener("input", (e) => { const year = parseInt(e.target.value, 10); const snapshot = snapshots.find((s) => s.year === year); if (snapshot) { chart.setOption({ series: [{ data: snapshot.nodes, links: snapshot.links }] }); yearLabel.textContent = `${year} 年`; } });

快照数据可以在 Python 脚本里提前算好:遍历诗人的出生年和去世年,按年份过滤出“此时在世”的节点,再筛选两端都在该集合内的边。这种方式比前端即时计算快得多,因为前端只需要做一次数组替换,CPU 开销极小。需要注意的是,快照数组每次更新时应该调用chart.clear()setOption,否则旧的图例和状态可能残留。

3.3 诗人卡片与作品索引的联动逻辑

关系图谱解决了“宏观”视角,还需要一个“微观”入口让用户了解个体诗人。常见做法是点击图谱中的节点,右侧滑出一个详情面板,展示诗人小传、代表作节选、存诗数量统计和关联人物列表。这里的核心技巧是事件委托——给整个画布绑定一个click事件,通过params.dataType判断是否点中了节点,而不是给每个节点单独绑定监听器,否则节点增删时容易造成内存泄漏。

作品索引可以用一个简单的搜索框实现,支持按诗名、诗句和作者三个维度的模糊匹配。因为数据量不大(几千首),前端全量过滤完全可行,不需要引入 Elasticsearch。用一个computed属性监听输入值,过滤出匹配结果,再点击结果时自动定位到对应作者节点并高亮其所有关系边。高亮方式的实现是重新遍历links,把非目标边设置为浅色透明,目标边加粗。

4. 数据渲染与视觉叙事:让“数字展现”不只是图表堆砌

4.1 从工具思维到叙事思维:一个页面里该有什么

很多计算机设计大赛的作品在技术实现上没毛病,但视觉效果看起来像“数据管理后台”:左侧一个表格,右侧一个图表,底部再来一个折线图。问题出在缺少主次关系。围绕唐朝诗人群像这个主题,首屏应该遵循“背景层—主体层—细节层”的三层叙事结构。背景层是一幅淡化的唐代疆域或山水画风格的页面底色,主体层是居中偏左的关系图谱,细节层是右侧的诗人详情卡片和底部的时间轴。用户第一眼看到的是图谱,其次是时间轴暗示“可以拖动”,最后才是详情卡片等待被点击。

具体实现上,头部用一张约 600ms 缓入的暗色渐变图作为视觉锚点,标题字体选用偏书法的开源字体,比如“站酷庆科黄油体”或“霞鹜文楷”,但正文继续用系统字体,保证可读性。字体文件要注意按unicode-range切片加载,否则一个几 MB 的字体文件会拖慢首屏速度。这里可以使用font-display: swap确保文字先展示,字体加载完成后替换。

4.2 颜色系统与关系边样式如何影响信息密度

唐朝诗人群体有明确的时代分区,颜色系统应该辅助用户“一眼看懂分区”,而不是仅仅为了好看。我建议四唐各用一种主色,比如初唐用青灰(#7f8c8d)、盛唐用金棕(#b8860b)、中唐用靛蓝(#3b5998)、晚唐用赭红(#a0522d)。节点内部用对应颜色填充,同时给节点加一圈同色系的半透明外发光,增强视觉分量。关系边的颜色默认是灰色,但不同类型的边可以用线型区分:师徒关系用实线,好友用虚线,同僚用点线。ECharts 的lineStyle.type支持这三种线型,配合tooltip中显示关系描述,信息密度就出来了。

这里需要注意一个细节:当某个节点被选中时,它的关联边应该保留颜色,非关联边统一淡化为 20% 透明度的灰色。这个逻辑要用图层管理来做,ECharts 同一 series 中无法针对单条边单独设置透明度并实时响应,所以需要准备两份 links 数据:一份是全量原始数据,一份是“被选中时的过滤数据”,切换时整体替换,而不是逐条修改。

4.3 动效时长与滚动叙事的工程实现

数字人文项目普遍存在过度动效的问题。力导向图本身就是动态的,如果加载时再叠加旋转、放大或者逐个弹出动画,页面会显得很“晃”。我常用的策略是:首次加载时只做一个 300ms 的节点透明度渐变,力导向的“自然凝聚”过程本身就是最好的动画,不需要额外编排。切换到时间轴快照时,通过setOption替换数据时给animationDurationUpdate设置为 400ms,形成节点平滑移位的效果。诗人详情卡片滑入时用 200ms 的transform: translateX,配合ease-out曲线,做到“快而不生硬”。

滚动叙事一般用在首页下半部分:用户滚动时依次看到四个时期的代表人物卡片,每个卡片横跨一张时间轴小图。这里用 CSSposition: sticky做简单的滚动驱动式交互即可,不必引入 GSAP 或 ScrollMagic 这类重量级库。如果确实想让视觉更有“诗卷感”,可以考虑用 Canvas 绘制一条贯穿页面的丝线,丝线路径由滚动位置映射到一条三次贝塞尔曲线,这是性价比最高的定制动效。

5. 源码落地与打包优化:拿别人的 zip 也能跑起来的工程细节

5.1 目录结构与源码包的可交付性

比赛或课程设计提交的 zip 包,评委第一步打开看到的应该是清晰的目录结构,而不是一堆无规则文件。我一般会按下面这种方式组织源码包:

poetry-tang/ ├── index.html ├── package.json ├── vite.config.js ├── README.md ├── public/ │ ├── data/ │ │ ├── poets.json │ │ ├── poems.json │ │ ├── relations.json │ │ └── snapshots.json │ └── assets/ │ └── bg-tang.jpg └── src/ ├── main.js ├── App.vue ├── components/ │ ├── PoetGraph.vue │ ├── PoetDetail.vue │ ├── TimelineSlider.vue │ └── PoemSearch.vue ├── utils/ │ └── graphUtils.js └── styles/ └── main.css

README.md至少要有三块内容:环境要求(Node 版本、包管理器)、启动命令、数据文件格式说明。公开数据集的来源和许可信息也要写明,这是很多参赛作品忽略的合规点。

5.2 一段可复用的数据完整性校验脚本

zip 压缩包在拷贝、解压过程中最容易出现数据文件缺失或 JSON 截断的问题。以下是基于 Node.js 的完整校验脚本,核心是用JSON.parse验证数据文件可解析性,同时实现节点 ID 与关系引用的交叉验证。

import fs from "fs"; import path from "path"; const dataDir = path.resolve("public/data"); function readJson(file) { const raw = fs.readFileSync(path.join(dataDir, file), "utf-8"); return JSON.parse(raw); // 解析失败会抛错, 便于定位问题 } function validate() { const poets = readJson("poets.json"); const relations = readJson("relations.json"); const poetIds = new Set(poets.map((p) => p.id)); // 检查节点唯一性 if (poetIds.size !== poets.length) { console.error("[FAIL] 存在重复的诗人 id"); process.exit(1); } // 检查关系引用的指向 const invalidEdges = relations.filter( (r) => !poetIds.has(r.source) || !poetIds.has(r.target) ); if (invalidEdges.length > 0) { console.error(`[FAIL] ${invalidEdges.length} 条关系引用了不存在的节点`); process.exit(1); } console.log("[PASS] 数据完整性校验通过"); } validate();

将脚本保存为validate-data.mjs,运行node validate-data.mjs即可。这套检查逻辑可以及时拦下“无向关系重复记录”“悬空引用”两类高频数据问题。若数据文件较大,可将readFileSync换成createReadStream流式解析,避免内存峰值过高。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询