当前位置:网站首页>哈希无向图可视化
哈希无向图可视化
2022-07-25 19:18:00 【biyezuopinvip】
哈希无向图可视化
我不知道到底有没有“哈希无向图”这种奇奇怪怪的数据结构,我只是想通过引入这种结构:
- 展示 StructV 具有可视化任何结构的能力
- 利用该种结构,能覆盖到我想要介绍的新内容
首先,先看看我们想要的目标效果:
看着不难吧。左边哈希表的每一个值都指向右边无向图的每一个结点,然后无向图里的结点又各有连接。为什么我偏要拿这个结构作为第二篇教程的例子呢,因为该结构有两个特点:
- 哈希表的每个元素的图形(就是两个格子那个),StructV 中没有内置
- 该结构有两种不同类型的结点(哈希表元素和无向图结点)
So what should we do ?我们要做的:还行老三样:1.定义源数据,2.编写配置项,3.编写可视化实例类。
Step 1
首先,新建 sources.ts ,确定我们的 Sources 。注意,现在我们有两种类型的结点了,分别为哈希表元素和无向图结点,所以对应的 SourcesElement 也有两种。
对于哈希表元素的 SourcesElement ,我们观察最终效果图,不难看出,其只有两个关键的元素,分别是元素的 id(左边格子)和指向图结点的指针(右边格子)。因此我们可以很容易地写出其 SourcesElement 结构:
// ------------------------- sources.ts -------------------------
import { SourceElement } from './StructV/sources';
interface HashItemSourcesElement extends SourceElement {
id: number;
hashLink: { element: string, target: number }
}
在这里,我们用 hashLink 来命名指向图结点的指针的名称(命名真是一大难题)。观察到,这次我们指针域的值和上一篇的二叉树 BinaryTreeSourcesElement 有点不一样了,没有直接填结点的 id ,而是使用了一个 { element: string, target: number } 的对象来描述,为什么要这样呢?
StructV 是根据一定的规则来处理 SourceElement 的指针域的,如果一个指针域的值为一个 id(或者 id 组成的数组),例如上一篇的 BinaryTreeSourcesElement 的 children :
// 一个二叉树结点
{
id: 1,
children: [2, 3]
}
那么 StructV 会在同类型的 SourceElement 寻找目标结点。但是现在我们想在不同类型的 SourceElement 中建立指针连线,那么我们就要用 { element: string, target: number } 这样的形式进行声明。其中 element 为目标元素的类型名称,target 为目标元素的 id 。至于具体应该怎么填,我们之后再做讲解。
对于无向图的结点,我们观察得到其 SourceElement 也不复杂,同样只含 id ,data(图中的结点的字符不可能为 id )和其他结点的指针域,那么我们也可以很快写出其具体定义。对于指向图其他结点的指针,这次我们用 graphLink 来命名。
// ------------------------- sources.ts -------------------------
import { SourceElement } from './StructV/sources';
interface GraphNodeSourcesElement extends SourceElement {
id: number;
data: string;
graphLink: number | number[];
}
注意,因为所以图节点都只有指向其他图结点的指针,所以 graphLink 可以直接用 id(number)表示。我们可以总结一下关于指针连线的指定规则:
- 对于不同类型的 SourceElement 间的指针,需要用包含
element和target的对象来指定 - 对于同类型 SourceElement 间的指针,则可以直接使用 id 表示
既然现在我们确定了两个 SourceElement ,那么理应就可以定义 Sources 的结构了。记得第一篇教程我们曾提到过:
当有多种类型的 SourcesElement 时,Sources 必须为对象,当只有一种类型的 SourcesElement 时,Sources 便可简写为数组。
在二叉树的例子中,由于只有一种类型的 SourceElement ,因此 Sources 可以定义为一个数组,但是现在,我们必须把 Sources 定义为一个对象:
// ------------------------- sources.ts -------------------------
export interface HashGraphSources {
hashItem: HashItemSourcesElement[];
graphNode: GraphNodeSourcesElement[];
}
我们得到我们的 HashGraphSources ,其中 hashItem 为哈希表元素,graphNode 为无向图结点。命名可随意,只要保证到时候输入的数据命名对的上就行。
sources.ts 的完整代码
Step 2
第二步编写默认配置项 Options 。
Step 2.1
该步骤跟上一篇内容的方法大致相同,但是因为该例子有两种 SourceElement ,因此有一些地方更改和说明。
- 首先,因为多类型 SourceElement ,因此元素配置项 element 需要从接受 string 改为接受接受一个对象,该对象与
HashGraphSources格式相对应 - 其次,对应布局配置项 layout 也需要作一些变化,
element的字段需改为元素配置项 element 中对应的字段 - 指针连线配置项 link 需添加两种指针连线
具体应该怎么做?看下面代码:
新建 options.ts 文件,写下以下内容:
// ------------------------- options.ts -------------------------
import { EngineOption } from './StructV/option';
import { Style } from './StructV/Shapes/shape';
export interface HashGraphOptions extends EngineOption {
// 元素配置项
element: {
hashItem: string;
graphNode: string;
};
// 布局配置项
layout: {
// 结点布局外观
hashItem: {
// 结点尺寸
size: [number, number] | number;
// 结点文本
content: string;
// 结点样式
style: Partial<Style>;
};
// 结点布局外观
graphNode: {
// 结点尺寸
size: number;
// 结点文本
content: string;
// 结点样式
style: Partial<Style>;
};
// 指针连线声明
link: {
hashLink: {
// 连线两端图案
markers: [string, string] | string;
// 连接锚点
contact: [number, number];
// 连线样式
style: Partial<Style>;
};
graphLink: {
// 连接锚点
contact: [number, number];
// 连线样式
style: Partial<Style>;
};
};
// 图布局的半径
radius: number;
// 哈希表与图的距离
distance: number;
// 自动居中布局
autoAdjust: boolean;
};
// 动画配置项
animation: {
// 是否允许跳过动画
enableSkip: boolean;
// 是否开启动画
enableAnimation: boolean;
// 缓动函数
timingFunction: string;
// 动画时长
duration: number;
};
}
element 属性现在为一个对象,其中与 HashGraphSources 的属性(hashItem,graphNode)一致,分别表示每种 SourceElement 的可视化图形; layout 中分别定义 hashItem ,graphNode 的外观和样式;link 中分别配置 HashItemSourcesElement 中的 hashLink 和 GraphNodeSourcesElement 中的 graphLink 。
之后,就是往里填充内容了。Emmmm。。。慢着,按照最终效果图,显然,无向图中的结点 graphNode 是圆形(circle),那么哈希元素项 hashItem 是什么图形呢?很遗憾,StructV 中并没有内置这个图形,因此我们要使用它,必须利用 StructV 的自定义图形功能。如何做,我们先放一会再说,现在我们先给这个图形取个好听的名字,那就叫 hashBlock 吧。
下面是配置项具体内容:
// ------------------------- options.ts -------------------------
export const HGOptions: HashGraphOptions = {
element: {
hashItem: 'hashBlock',
graphNode: 'circle'
},
layout: {
hashItem: {
size: [80, 40],
content: '[id]',
style: {
stroke: '#000',
fill: '#a29bfe'
}
},
graphNode: {
size: 50,
content: '[data]',
style: {
stroke: '#000',
fill: '#a29bfe'
}
},
link: {
graphLink: {
contact: [4, 4],
style: {
fill: '#000',
lineWidth: 2
}
},
hashLink: {
contact: [1, 3],
markers: ['circle', 'arrow'],
style: {
fill: '#000',
lineWidth: 2,
lineDash: [4, 4]
}
}
},
radius: 150,
distance: 350,
autoAdjust: true
},
animation: {
enableSkip: true,
duration: 1000,
timingFunction: 'quinticOut',
enableAnimation: true
}
}
options.ts 的完整代码
Step 2.2

看起来一点都不复杂是吧,就是简单的两个正方形拼起来的图形。我们同样希望这样的简单图形在使用 StructV 创建时也同样很容易,很好。创建自定义图形和创建可视化实例类一样,都是通过继承某个基类来完成。
还记得我们给这个图形起了个什么名字吗?新建一个 hashBlock.ts 文件,写下以下模板代码:
// ------------------------- hashBlock.ts -------------------------
import { Composite } from "./StructV/Shapes/composite";
import { BaseShapeOption } from "./StructV/option";
export class HashBlock extends Composite {
constructor(id: string, name: string, opt: BaseShapeOption) {
super(id, name, opt);
}
}
StructV 将每个图形都抽象为一个类,所有图形的类统称为 Shape 。可以看见父类往子类传递了 3 个参数,分别为图形的 id ,图形的名字和图形的配置项。我们可暂时不必深入了解 Shape 和这 3 个参数的详细作用,只要知道我们的 hashBlock 也是一个类,并继承于 Composite 。Composite 看字面意思是“组合,复合”的意思,这说明了我们的自定义图形 hashBlock 是复合而来的。由什么东西复合?答案是基础图形。在 StructV 中,内置的基础图形如下:
- Rect 矩形
- Circle 圆形
- Isogon 正多边形
- PolyLine 折线
- Curve 曲线
- Arrow 箭头
- Text 文本
也许你已经猜到了,我们的自定义图形只能由上述这些基础图形进行组合而成。也就是说,我们不能创建一种新的基础图形,但是我们可以用这些基础图形组合出一种新图形。我们称这些组成复合图形的基础图形为该图形的子图形。
那么,现在问题就清晰了,创建一个自定义图形,我们只需要知道:
- 由哪些子图形组合
- 子图形的外观和样式怎么设置
- 子图形怎么组合(或者说怎么摆放)
在 Composite 类中,我们提供了 addSubShape 方法用作添加子图形。通过在构造函数中调用 addSubShape 方法进行子图形的配置:
// ------------------------- hashBlock.ts -------------------------
import { Composite } from "./StructV/Shapes/composite";
import { BaseShapeOption } from "./StructV/option";
export class HashBlock extends Composite {
constructor(id: string, name: string, opt: BaseShapeOption) {
super(id, name, opt);
// 添加子图形
this.addSubShape({
cell1: {
shapeName: 'rect',
init: option => ({
content: option.content[0],
}),
draw: (parent, block) => {
let widthPart = parent.width / 2;
block.y = parent.y;
block.x = parent.x - widthPart / 2;
block.height = parent.height;
block.width = widthPart;
}
},
cell2: {
shapeName: 'rect',
init: option => ({
content: option.content[1],
zIndex: -1,
style: {
fill: '#eee'
}
}),
draw: (parent, block) => {
let widthPart = parent.width / 2;
block.y = parent.y;
block.x = parent.x + widthPart / 2;
block.height = parent.height - block.style.lineWidth;
block.width = widthPart;
}
}
});
}
}
突然来了这么一大串是不是有点懵。我们来从外到内一步一步剖析这段新加的代码。首先,能看到 addSubShape 函数接受了一个对象作为参数,通过观察我们可以抽象出这个参数的结构:
interface SubShapes {
// 子图形的别名
[key: string]: {
// 基础图形的名称
shapeName: string;
// 初始化子图形的外观和样式
init: (parentOption: BaseShapeOption, parentStyle: Style) => BaseShapeOption;
// 布局子图形
draw: (parent: Shape, subShape: Shape) => void;
}
}
首先这个对象的属性名,如 cell1, cell2 都是这个子图形的别名,别名可以任取,但是不能重复。其中 cell1 就是 hashBlock 左边的正方形,同理 cell2 就是右边的那个。
然后别名的值也是一个对象,这个对象里面配置了子图形的详细信息,分别是 shapeName ,init 和 draw。其中 shapeName 很明显啦就是基础图形的名字,决定了我们要选哪个基础图形作为子图形,例如上面 cell1 我们选了 rect,即矩形,那当然啦,因为 hashBlock 就是两个正方形组成的,因此同理 cell2 。
重点要讲的是 init 和 draw ,这两个属性均为函数。 init 用作初始化子图形的外观和样式,返回一个 BaseShapeOption 类型的值。 BaseShapeOption 类型是什么类型?还记得我们的 Options 里面的布局配置项 layout 吗:
graphNode: {
size: number;
content: string;
style: Partial<Style>;
};
这样的一组配置在 StructV 中称为一个 BaseShapeOption 。
此外,init 还接受两个参数,分别为父图形的 BaseShapeOption 父图形的 Style ,子图形可根据这两个参数去配置自身的外观和样式。
这样设计的意义何在?StructV 将一个自定义图形(或者说复合图形)视为一个整体对待,因此在配置我们的自定义图形时,图形的配置和样式项即
BaseShapeOption和Style需要由某一途径传递至子图形,因为子图形(基础图形)才是真正被渲染出来的元素, Composite 只是抽象意义的结构。拿上面的例子来说,我们设置hashBlock的颜色fill: 'red',那么可视化引擎怎么知道究竟是把全部矩形设置为红色还是把左边或者右边的矩形设置为红色呢?这时候只要接受父图形的颜色传递下来的颜色根据需要定制即可。这跟 React 单向数据流动的道理是一样的。
draw 函数的作用清晰很多,就是设置子图形的布局。因为子图形的布局需要依赖父图形,因此与 init 一样,draw 接受两个参数,分别为 parent :父图形实例,subShape :子图形实例。具体布局的计算就不讲解了,相信大家都能看懂,就是简单地把长方形分割为两个正方形而已。
目前为止我们的 hashBlock 算是基本完成了,只要我们理解了 addSubShape 方法,就可以创建无数的自定义图形。但是慢着,观察我们的效果图,可以发现 hashBlock 有一个锚点是位于图形内部的(右边正方形的中心),因此最后我们还需要使用自定义锚点功能。
在自定义图形中通过重写 defaultAnchors 方法添加或修改锚点:
// ------------------------- hashBlock.ts -------------------------
import { Composite } from "./StructV/Shapes/composite";
import { BaseShapeOption } from "./StructV/option";
import { anchorSet } from "./StructV/Model/linkModel";
export class HashBlock extends Composite {
// ...省略代码
/**
* 修改默认锚点
* @param baseAnchors 默认的5个锚点
* @param width 图形的宽
* @param height 图形的高
*/
defaultAnchors(baseAnchors: anchorSet, width: number, height: number): anchorSet {
return {
...baseAnchors,
1: [width / 4, 0]
};
}
}
defaultAnchors 方法接受 3 个参数:baseAnchors 默认的 5 个锚点,width 图形的宽, height 图形的高。并返回一个新的锚点集(anchorSet)。还记得默认的 5 个锚点是哪五个吗?回忆一下这张图:
5 个锚点各自有对应的编号,而编号 1 的锚点为图形最右边的锚点。现在,我们在 defaultAnchors 中将编号为 1 的锚点重新设置为一个新的值,达到了修改默认锚点的目的。同理可以推断出,如果我们要添加锚点,只要在下面写除(0,1,2,3,4)外的值即可,如:
return {
...baseAnchors,
5: [width / 4, height / 4]
};
表示我们添加了一个编号为 5 的新锚点。
锚点的值 [width / 4, 0] 指定了锚点的相对位置,相对谁?相对于图形的几何中心,即(x,y)。因此,[width / 4, 0] 表示该锚点的横坐标位于图形水平中心往右偏移 width / 4,纵坐标位于图形垂直中心的位置,也就是 hashBlock 右边正方形的中心。
大功告成。
边栏推荐
- Youfu force supercomputing provides customized high-performance computing services for customers
- [encryption weekly] has the encryption market recovered? The cold winter has not thawed yet! Check the major events in the encryption market last week!
- Swift 基础 Codable(JSONEncoder JSONDecoder)的使用
- Huawei recruited "talented teenagers" twice this year; 5.4 million twitter account information was leaked, with a selling price of $30000; Google fired engineers who believed in AI consciousness | gee
- Wechat campus maintenance and repair applet graduation design finished product of applet completion work (4) opening report
- Pymoo学习 (5):收敛性分析
- [Detr for 3D object detection] 3detr: an end to end transformer model for 3D object detection
- 高并发下如何保证数据库和缓存双写一致性?
- 乐理基础 调式
- Have you ever seen this kind of dynamic programming -- the stock problem of state machine dynamic programming (Part 1)
猜你喜欢

【919. 完全二叉树插入器】

Improvement of wechat applet 26 playing music page ②

Youth, oh, youth

微信小程序 28 热搜榜的完善①

高并发下如何保证数据库和缓存双写一致性?

小程序毕设作品之微信校园维修报修小程序毕业设计成品(1)开发概要

小程序毕设作品之微信校园维修报修小程序毕业设计成品(7)中期检查报告

小程序毕设作品之微信校园维修报修小程序毕业设计成品(4)开题报告

Basic music theory -- configuring chords

How to ensure the consistency of double write between database and cache?
随机推荐
How to prohibit the use of 360 browser (how to disable the built-in browser)
帝国CMS7.5仿《问答库》题库问答学习平台网站源码 带手机版
【小程序开发】常用组件及基本使用详解
Common development software download addresses
With 8 years of product experience, I have summarized these practical experience of continuous and efficient research and development
解决Win10账户没有了管理员权限
[applet development] do you know about applet development?
modelsim和quartus联合仿真PLL FIFO等IP核
QIIME2得到PICRUSt2结果后如何分析
Full scale and Xuan of C key
歌曲转调之后和弦如何转换
小程序毕设作品之微信校园维修报修小程序毕业设计成品(2)小程序功能
基础乐理之音程的度数
房企打响“保交战”
Improvement of wechat applet 26 playing music page ②
Talk about 11 tips for interface performance optimization
【919. 完全二叉树插入器】
【阅读笔记】《深度学习》第一章:引言
小程序毕设作品之微信校园维修报修小程序毕业设计成品(5)任务书
平衡二叉树