欢迎访问我的博客:TZLoop's Blog (zonelyn.com)javascript
转载本文请注明原始出处。css
有人说蚂蚁的世界是二维的(很是不许确),那是由于它们永远不知道何为高矮深浅。 由于它们感官的“无能”,导致它们丧失感觉世间万物的机会。html
对知识系统(eg.博客)而言,良好的组织结构是极为重要的,尤为是当内容增多,关联复杂后显得尤其重要。传统的“分类(Categories)+标签(Tags)”的二级模式虽足以应付大部分用户的需求,但本质上其仍是须要用户对已有分类和标签有良好的组织,这对不少用户来讲是根本作不到,由于咱们每每缺的就是这种“纵览全局”的能力。java
分类每每越分越多,标签也是随意放置,长此以往,不只已有的分类和标签杂乱无章,更为甚者是新增内容时根本不知从何下手,每每须要遍历过往的标签和分类,才能作出最终定夺。如今,经过图布局的方式,能够以一种近乎完美的方式对复杂的内容进行组织,详细效果请查看 该页面。node
对于知识系统(以后均以博客代指)而言,传统的模式只是简单的分支,或者称其为树形结构,在探索过程当中,用户就如同“蚂蚁”同样,只得选择先从哪进入,而后再进入到哪里。对于单篇内容而言并没有影响,但当须要感知全局时,每每这种模式就会出现问题。算法
分级/树形标记模式自己就是一个分类过程,本身的知识内容(博客文章)是对象,维护者将其放置在不一样的类别下。**标签(Tags)**则更像是分类过程当中的副产物,更贴近文章内容,但又言简意赅,经过分级的思考方式,分类和标签和文章的关系是:数据库
分类-标签-文章(1:M:N)编程
对于上述关系,分别用A、B、C表示的话,则整个系统其实就是一个“Ai-Bi-Ci”的三元组集合。该集合的好坏(即质量)就是其在语义上的契合程度,例如:json
分类:军事 -> 标签:爆炸 -> 文章:伊拉克遭遇恐怖袭击 分类:娱乐 -> 标签:爆炸 -> 文章:阿富汗遭遇恐怖袭击
当抽象为网络/图以后,军事类别和娱乐类别会经过“爆炸”这一标签相连,如是,明显的会发现“爆炸”位置不对。(虽然例子很蠢,但当语义区分模糊、标签数量繁多时,极易出现该状况)。下面直接拿已完成的布局来解释:api
粉红色为分类、蓝色为标签、节点半径为被使用的次数
语义不符的链接点(异常的跨类标签),若是链接点对某一方语义不匹配,那么极可能该文章是特殊的,或者该标签不该该出如今该文章。(下图里可视化的文章在这儿,属于特殊文章,正常“生活分类”和“可视化”的语义并不匹配) [图片上传失败...(image-3cbab4-1578849188423)]
合格的链接点(跨分类的标签):虽然标签出如今不一样分类中是很是正常的,例如“总结”,能够出如今任何分类中。但相似“总结”这类标签每每数量不少,即屡次的出如今不一样的类别中,那咱们就说这是一个合格的跨分类标签。
对于分类点,以本博客为例,因为是对已存在数据进行分析,因此若是某分类下属节点不足,那么高度怀疑该分类不合理,除非是须要往后扩充的分类。这一需求在图布局的视图下很是容易分辨出来,合格的类别应该有众多叶节点,当叶节点不足,则应考虑将其降级至标签。(例以下图中的“朴素贝叶斯”,可将其降级为标签,并归类到“研究方向”中)
值得注意的一点是: 这里使用的图布局使用力导向(Force-directed)布局算法,相关则相近,无关则疏远,又完美的给布局结果以语义上的解释,即:
上节中的分析看似颇有道理,布局结果的使用也很是方便,那么如何从无到有将其构建出来?主要有如下几个方面:
上述过程当中,肯定**“图布局”模式是基础,剩下的无非是将信息绑定到可视化元素上**,例如,已实现的布局将“类别/标签”用颜色区分,其实用形状等其余可视化元素区分也彻底能够。
到此为止,只是上层结构,相似数据库存储,搞了半天只是在搞索引,并无触碰到数据,因此目前为止该网络并无直通最底层(文章内容)的能力,这个问题刚好被Hexo的文件结构所解决,Hexo给每一个标签和每一个分类都渲染了单独的页面,关联的文章被放置在页面中,在此,直接经过节点的文本信息构造访问地址,将其绑定到文本上,便可点击后跳转到相关页面,虽然不是直接跳转文章,但也能够说具有至关的垂直打击能力了。
进阶版本:变的更强
简单粗暴的加入以前三元组被抛弃掉的文章信息,但因为加入后过于散乱,因此有必要将文章信息固定,以便于视觉呈现。以下图(d3.js实现的、用于可视化编程概念的可视化模型):
上图就是简单的带固定节点的力导向布局,但其实现代码比较复杂,目前处在构造数据阶段。通常的可视化模型套用的步骤:
阅读原站代码 -> 从原站抽离可视化部分 -> 搞清调用数据的方法及格式 -> 构造一样的数据 -> 独立运行 -> 放回本身的站点内
到此,对于分级/树形分类的三点不足,能够发现很轻松就能够解决。既有全局视角,又能够同时具有直达的能力,对于组织内容数量较高(超过50)的站点很是适合该模式的导航、或辅助探索。
下文开始,详细记录了如何在Hexo博客中实现用图组织内容的方法,可是,请注意:如下内容并不是操做教程,仅代表相信思路以供参考,或许您能够实现出更好的版本,但仅依照下文内容并不保证必定能重现,一些尝试和debug的细节过于繁琐并未列出,若有疑问欢迎留言。
文档说明,借助该函数,能够在Hexo渲染生成页面文件以前,完成用户的自定义JavaScript代码。
其实,在Hexo的框架内,ejs(或其余类型的)模板中的代码就是渲染生成html的代码,在这些页面中,借助Hexo内建的对象,好比.post对象和.achieves对象,能够访问到其中保存的所有文章信息及关联信息。例如:
let posts = hexo.locals.get('posts'); let Xtags = posts.data[x].tags let tagsY = Xtags.data[y].name
上述内容,能够最终获得第X篇文章(POST)中的第Y个标签的文本。相似的方法一样能够获得某篇文章的Categories的信息。这就是构造可视化数据的基本方法。(在渲染前构造、借助.post对象) 关于位置,在ejs模板中放置构造代码固然能够,可是不优雅,Hexo中建议的插入方式是:
因为处在尝试阶段,因此这里使用步骤3 的方法,这样各模块相对独立,对主题源代码入侵小。
这里采用的是 D3.js 进行的可视化呈现,基本上是复用的 d3 的官方模板,但将文本信息一并和节点进行可视化展现。这段代码首先须要被抽取出来,这对于 d3 来讲很是简单,只需注意引入的JavaScript库以及使用的json文本数据。
<svg width="1000" height="1000"></svg> //d3绘制的内容所有放置在该画布上 <script src="https://d3js.org/d3.v4.min.js"></script> <script> var sss = 'JSON字符串'; //这就是整个代码所可视化的数据 var abc = parseInt($(".card").css("width").replace("px","")); if(abc>1080) abc=1050; else if(abc>1040) abc=1020; else abc=abc-40; $("svg").css("width",abc); $("svg").css("height",abc); //此部分将画布大小跟随文章页宽度变化 var svg = d3.select("svg"), width = abc, height = abc; var color = d3.scaleOrdinal(d3.schemeCategory20); var simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) { return d.id; })) .force("charge", d3.forceManyBody().strength(-180).distanceMin(10).distanceMax(300).theta(1)) .force("center", d3.forceCenter(width / 2 - 40, height / 2 - 30)); var graph = JSON.parse(sss); var link = svg.append("g") .attr("class", "links") .selectAll("line") .data(graph.links) .enter().append("line") .attr("stroke-width", function(d) { return Math.sqrt(d.value); }); var node = svg.append("g") .attr("class", "nodes") .selectAll("g") .data(graph.nodes) .enter().append("g") var circles = node.append("circle") .attr("r", function(d) { if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整 else return d.group+1; }) .attr("fill", function(d) { if(d.group>=100) return "#ff4081"; else return "#3f51b5"; }) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); var lables = node.append("text") .html(function(d) { if(d.group>=100) { var p = d.group/100*(10.00/48.00)+10; return "<a style='font-size:"+p+"px;font-weight:600;color:red' href='/categories/"+d.id.replace("_","-")+"'>"+d.id+"</a>"; }else{ var q = d.group+10; return "<a style='font-size:"+q+"px;' href='/tags/"+d.id+"'>"+d.id+"</a>"; } }) .attr('x', function(d) { if(d.group>=100) return d.group/100*(10.00/48.00)+5; //取整 else return d.group+3; }) .attr('y',function(d) { if(d.group>=100) return d.group/100*(3.00/48.00)+5; //取整 else return 5; }); node.append("title") .text(function(d) { return d.id; }); simulation .nodes(graph.nodes) .on("tick", ticked); simulation.force("link") .links(graph.links); function ticked() { link .attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }); node .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) } function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } </script>
须要匹配示例的输入格式,这样才能最大化的复用代码。上述内容的官方示例中使用的格式是:
{ "nodes": [ {"id": "Myriel", "group": 1}, ... ... {"id": "Mme.Hucheloup", "group": 8} ], "links": [ {"source": "Napoleon", "target": "Myriel", "value": 1}, ... ... {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} ] }
即,须要在可视化页面被渲染出来以前就获得上述格式的数据,这即是要借助Hexo的辅助函数来完成,将构造数据的代码封装成一个函数,而后在适当的ejs模板中调用一下,便可在 hexo generate 以后,从Console中拿到构造好的数据。
在此,构造规则是:类别永远单向的指向标签,类别不互连,标签不互连,同时,还须要计算的是类别和标签出现的次数。
hexo.extend.helper.register('getPostData', () => { var posts = hexo.locals.get('posts'); var tagsMap = new Map(); //counter // 利用posts对象获取类名和标签名 for(var i = 0; i< posts.length; i++){ var nameCS; posts.data[i].categories.forEach(function(k, v) { nameCS = k.name; return; }) for(var j = 0; j< posts.data[i].tags.length; j++){ var pname = posts.data[i].tags.data[j].name; var pval = tagsMap.get(pname); if(pval != null){ // 将类名和标签名压制在一块儿 tagsMap.set(nameCS+">"+pname, parseInt(tagsMap.get(pname))+1); }else{ // tagsMap.set(nameCS+">"+pname, 1); } } } //由此开始,构造符合特定格式的JSON字符串 let obj= []; let setss = new Map(); for (let[k,v] of tagsMap) { var st = k.split(">"); var str = {}; str.source = st[0]; str.target = st[1]; str.value = v; obj.push(str); if(setss.get(st[0]) != null){ // 类节点 每次加100 setss.set(st[0], parseInt(setss.get(st[0]))+100); }else{ // setss.set(st[0], 100); } if(setss.get(st[1].trim()) != null){ // 标签节点 每次加1 setss.set(st[1], parseInt(setss.get(st[1]))+1); setss.set(st[0], parseInt(setss.get(st[0]))+100); }else{ // setss.set(st[1], 1); setss.set(st[0], parseInt(setss.get(st[0]))+100); } } let obk= []; for (let [k,v] of setss) { var str = {}; str.id = k.trim(); str.group = v; //经过数量分类 obk.push(str); } let d3str = {}; d3str.nodes = obk; d3str.links = obj; console.log(JSON.stringify(d3str).trim()); //按第三步说的,能够手动放置数据到可视化页面 return JSON.stringify(d3str).trim(); //或按第四步,将数据返回至ejs模板中,直接渲染出可视化页面 });
注意上述代码中的注释,这里利用了类节点和标签节点出现的次数,来分辨两种节点的种类,由于绘制时类节点和标签节点都是一视同仁的被绘制。如何分辨呢?在可视化页面中有如下代码:
var circles = node.append("circle") .attr("r", function(d) { if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整 else return d.group+1; })
按照不一样的次数计算步长,获得的类节点的次数必定是100的倍数,而标签节点的次数必定小于100,这个值能够设的很大,从而让二者不可能出现交集。在判断时“若是次数大于100”,那么就是类节点,取整百的好处是,归一化方便。例如上述代码须要给定节点的大小,类节点的次数统计多是100-4800(1-48次),而标签节点的次数倒是1-10(1-10次),如是,二者应绘制的同样大。这就须要归一化,只须要缩放100倍再乘比例系数便可。
上文中**hexo.extend.helper.register('getPostData', () => {})**的“getPostData”即注册的函数名,在ejs(或其余)模板中直接调用便可。但因为我但愿把这个可视化模块放在个人评论页或者关于页面,而这两个页面都不是渲染出来的,因此就只能采用先前第三步的作法,只构造出数据,再手动放入可视化页面。
// 在 index.ejs 内添加: <% var arr = getPostData(); %>
因此,须要作的就是找一个渲染页面的ejs,调用下该函数便可,这里放在index.ejs里,注意因为分页可能该模板会构造不少次,因此就会重复输出不少遍JSON数据。
基本上仍是抓住代码执行的输入输出作文章。从待改造代码的输入找格式,而后从原代码的框架中构造出该格式的数据(输出),就像适配同样,如此即可以利用Hexo能够得到的数据,借助D3.js等可视化库,把本身的博客(知识系统)作一个梳理和呈现,从而更好的帮助本身管理和维护,也给了本身二次挖掘本身知识的机会。
本文做者:TZLoop 我的博客:TZLoop's Blog (zonelyn.com) 转载本文请注明原始出处。