最近项目需要写一个d3的力导向图,之前没接触过d3.js 所以吧这次开发的经历写一下

文章目录

  • 分配点与线
  • 创建dom
    • 线
  • 绘制线

友情提示:不要让设计设计的华丽呼哨,点多了很卡,而且svg 有些标签是不支持css 控制 某些样式的,也不是很好实现

如果之前没写过d3 的旁友 还不熟悉d3 的话。可以吧d3. js 理解为1个帮助你处理数据的库。

把点与线 给到d3, d3 会根据你传入的长宽 自动给你分配x,y 的位置,自己再通过 js 去点的位置想干嘛干嘛(画点)

###基本的展示

####基本配置


// 生成力const force = d3.forceSimulation().force('link',d3.forceLink().id((d) => d.id),).force('collide', d3.forceCollide(72).strength(0.1)).force('charge',d3.forceManyBody().strength(-400),).force('center', d3.forceCenter());

分配点与线

处理一下线的数据, 两个点可能出现多条线的情况


export const setLinkNumber = (group, type) => {if (group.length === 0) return;const linksA = [];const linksB = [];for (let i = 0; i < group.length; i++) {const link = group[i];// 对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分if (link.source.id < link.target.id) {linksA.push(link);} else {linksB.push(link);}}// 确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。// 特殊情况:当关系都是连接到同一个实体时,不平分let maxLinkNumber = 0;if (type === 'self') {maxLinkNumber = group.length;} else {maxLinkNumber = group.length % 2 === 0 ? group.length / 2 : (group.length + 1) / 2;}// 如果两个方向的关系数量一样多,直接分别设置编号即可if (linksA.length === linksB.length) {let startLinkNumber = 1;for (let i = 0; i < linksA.length; i++) {linksA[i].linknum = startLinkNumber++;}startLinkNumber = 1;for (let i = 0; i < linksB.length; i++) {linksB[i].linknum = startLinkNumber++;}} else {// 当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号// 如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)let biggerLinks;let smallerLinks;if (linksA.length > linksB.length) {biggerLinks = linksA;smallerLinks = linksB;} else {biggerLinks = linksB;smallerLinks = linksA;}let startLinkNumber = maxLinkNumber;for (let i = 0; i < smallerLinks.length; i++) {smallerLinks[i].linknum = startLinkNumber--;}const tmpNumber = startLinkNumber;startLinkNumber = 1;let p = 0;while (startLinkNumber <= maxLinkNumber) {biggerLinks[p++].linknum = startLinkNumber++;}// 开始负编号startLinkNumber = 0 - tmpNumber;for (let i = p; i < biggerLinks.length; i++) {biggerLinks[i].linknum = startLinkNumber++;}}};function getKey(target, source) {const result = target > source ? `${target}:${source}` : `${source}:${target}`;return result;}export const operationData = (chartData, clickType) => {const linkmap = {};const linkGroup = {};const { links, dots } = chartData;for (let i = 0; i < links.length; i++) {const link = links[i];const { target, source } = link;const key = getKey(target, source);if (linkGroup[key]) {linkGroup[key].push(link);linkmap[key] += 1;} else {linkGroup[key] = [links[i]];}}Object.keys(linkGroup).forEach((groupKey) => {linkmap[groupKey] = linkGroup[groupKey].length;});// 关联线与点JSON.parse(JSON.stringify(links)).forEach((e) => {const sourceNode = dots.filter((n) => n.id === e.source)[0];const targetNode = dots.filter((n) => n.id === e.target)[0];const nowIndex = links.findIndex((n) => n.source === e.source && n.target === e.target,);if (!sourceNode || !targetNode) {links.splice(nowIndex, 1);} else {links[nowIndex].source = sourceNode;links[nowIndex].target = targetNode;}});for (let i = 0; i < links.length; i++) {let { target, source } = links[i];target = target.id;source = source.id;const link = links[i];const key = getKey(target, source);link.size = linkmap[key];const group = linkGroup[key];const type = 'noself'; // 标示该组关系是指向两个不同实体还是同一个实体setLinkNumber(group, type);}return { links, dots };};

处理好的数据丢到d3里面去


// tick 渲染时执行的方法force.nodes(dots).alpha(0.01).on('tick', this.tick).restart();force.force('link').links(links).distance(150);

丢进去 会吐出来有x,y 的数据 如:

数据处理完了 接下来创建dom

创建dom

线

线: 线是一个 g 标签包含着N 条线(a)

线内包含着2条线以及线相关的箭头

两条线的目的是因为1条线很细的情况下会很不好hover 到。所以1条粗线 一条细线 ,直接把透明度(opacity) 属性 设置为0 即可

所有箭头状态的属性,因为箭头和线不是"一体"的,所以当hover 的时候,圆点(dot) 的大小会发生变化,箭头的refX,refY,也会发生变化,甚至 箭头的大小变化 refX 和refY也得改变。


export const styleSize = {normal: {refX: 30,markerHeight: 8,markerWidth: 8,},hover: {refX: 28,markerHeight: 10,markerWidth: 10,},click: {refX: 19,markerHeight: 17.5,markerWidth: 17.5,},dotnormal: {refX: 35,markerHeight: 8,markerWidth: 8,},dothover: {refX: 43,markerHeight: 10,markerWidth: 10,},dotclick: {refX: 48,markerHeight: 10,markerWidth: 10,},dotlineclick: {refX: 28,markerHeight: 25,markerWidth: 20,},};// isThumb 是否是缩略图export const drawLine = (svg, type, links) => {const isThumb = type === 'thumb';const warp = isThumb ? svg.insert('g', '.dragThumb') : svg.append('g');const lineWarp = warp.attr('class', `${isThumb ? 'thumbG' : 'forceLines forceMainG'}`).selectAll('g').data(links).enter().append('a')const {refX, markerWidth, markerHeight,} = styleSize.normal;const markerId = (d) => `marker-${(d.id)}`;lineWarp.append('marker').attr('id', markerId).attr('markerUnits', 'userSpaceOnUse').attr('viewBox', '0 -5 10 10') // 坐标系的区域.attr('refX', refX) // 箭头坐标.attr('markerWidth', markerWidth) // 标识的大小.attr('markerHeight', markerHeight).attr('orient', 'auto') // 绘制方向,可设定为:auto(自动确认方向)和 角度值.attr('stroke-width', 2) // 箭头宽度.append('path').attr('d', 'M0,-5L10,0L0,5') // 箭头的路径.attr('fill', 'inherit'); //箭头的颜色, 设置箭头的颜色 不可以直接找到箭头然后更改fill 因为真正有颜色的是 箭头里面的dom// 展示的线const line = lineWarp.append('path')//实线虚线自己控制.attr('stroke-dasharray', (d) => (虚线 ? '8,5' : '')).attr('marker-end', (d) => {if (isThumb) return '';return `url(#${(markerId(d))})`;});// 实际hover 以及 点击的线const bkLine = lineWarp.append('path').attr('stroke-width', 10).attr('stroke', 'red').attr('fill', 'none').attr('opacity', '0')return {lineWarp,line,bkLine,};};

此处只创建1个圆点


export const drawCircle = (svg, nodes, type) => {const dotWarp = svg.append('g').attr('class', 'forceNodes forceMainG').selectAll('g').data(nodes).enter().append('a').attr('xlink:href', 'javascript:void(0)');const circle = dotWarp.append('circle').attr('class', 'forceNode regionNode');return {circle, dotWarp};};

绘制线

在之前讲的tick 中去改变 path 的d属性 以及 点的 位置


this.paths.attr('d', function (data) {return pathD(data, this);});this.bkLine.attr('d', function (data) {return pathD(data, this);});this.dotWarp.attr('transform', setTransform); // 圆圈
export const pathD = (d, dom) => {const { x: sx, y: sy } = d.source;const { x: tx, y: ty } = d.target;let dr;// 如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制if (d.target === d.source) {dr = 30 / d.linknum;return (`M${sx},${sy}A${dr},${dr} 0 1,1 ${tx},${ty + 1}`);} if (d.size % 2 !== 0 && d.linknum === 1) {// 如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边return `M ${sx},${sy},L ${tx},${ty}`;}// 根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧// 注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,// 第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;// 第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果const curve = 1.5;const homogeneous = 2;const dx = d.target.x - d.source.x;const dy = d.target.y - d.source.y;dr = (Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous))/ (curve * homogeneous);// 当节点编号为负数时,对弧形进行反向凹凸,达到对称效果if (d.linknum < 0) {if (dom) {d3.select(dom.previousElementSibling).attr('refY', 4).attr('oldRefY', 4);}dr = (Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous))/ (curve * homogeneous);return (`M${sx},${sy}A${dr},${dr} 0 0,0 ${tx},${ty}`);}if (dom) {d3.select(dom.previousElementSibling).attr('refY', -4).attr('oldRefY', -4);}return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;};
export const setTransform = (node) => {const { x, y, k } = node;let result = '';if (x && y)result += `translate(${x},${y})`;if (k)result += ` scale(${k})`;return result;};

####点的拖拽

固定节点的方法就是 设置fx fy=null


// 创建完dotWarp的时候 可以直接绑定dotWarp.call(d3.drag().on('start', this.started).on('drag', this.dragged).on('end', this.ended),);started(d) {const { force } = this;if (!d3.event.active) {force.alphaTarget(0.2).restart(); // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]}d.fx = d.x;d.fy = d.y;},dragged(d) {const { x, y } = d3.event;if (this.inBoundaries(x, y).isIn) {d.fx = x;d.fy = y;}d.fx = d3.event.x;d.fy = d3.event.y;},ended(d) {const { force } = this;if (!d3.event.active) {force.alphaTarget(0);}d.fx = null;d.fy = null;},

####zoom 画布的拖拽 以及放大缩小

/*** @params* zoomMin 最小缩小倍数* zoomMax 最大放大倍数*/const zoom = d3.zoom().scaleExtent([zoomMin, zoomMax]).on('zoom', () => {const transInfo = d3.event.transform; //绘制框选的时候需要用到mainSvg.selectAll('g').attr('transform', transInfo);this.transInfo = transInfo;this.$emit('zoom', transInfo); // 告诉外层 发生了拖拽});mainSvg.call(zoom).on('dblclick.zoom', null); // 注销双击缩放

放大 缩小


//svg.transition().duration(750).call(zoom.scaleBy,放大的倍数);// 缩小 0.9倍直到 缩小到最小倍数svg.transition().duration(750).call(zoom.scaleBy, 0.9);// 放大 1.1倍直到 放大到最大倍数svg.transition().duration(750).call(zoom.scaleBy, 1.1);

####点的框选

拖拽中创建一个矩形框,拖拽后判断中心点是否在矩形框中则为被框选中 (位置需要与缩放的scale 配合计算)

####删除

点的删除实际上 就是把 相关点与线全部删除, 并且清空画布后, 重新用删除后的数据重新绘制。

####缩略图

缩略图目前的逻辑是主图的最大倍数作为背景,主图的宽高作为缩略图视野(蓝框)的宽高。

因为缩略图的dom 的宽高是css 定死的,所以给定主图(正常)的宽高 会自动缩放。

主图的拖拽与缩略图背景图的关系会在下面一节说

/*** @params* width 缩略图宽度* height 缩略图高度* mainWidth 主图的宽度* mainHeight 主图的高度* zoomMax 最大缩放比例* */thumbSvg = d3.select('#thumbWarp').append('svg');dragThumb = thumbSvg.append('rect').attr('class', 'dragThumb').attr('fill', 'none');let w; let h; let x = 0; let y = 0;thumbSvg.attr('width', width).attr('height', height).attr('id', 'thumbSvg').attr('viewBox', () => {// 缩略图的宽高为 主图的 最大缩略比例w = mainWidth * zoomMax;h = mainHeight * zoomMax;// 设置偏移 让背景图移至中心,缩略图与主图的差/ 2 就是需要移动的距离x = -(w - mainWidth) / 2;y = -(h - mainHeight) / 2;return `${x} ${y} ${w} ${h}`;});dragThumb.attr('width', mainWidth).attr('height', mainHeight);

####主图的拖拽、缩放与缩略图

主图的拖拽与缩放 在调用上面的缩放的时候会调用zoom 的on zoom 方法

并将缩放以及拖拽的距离传给 缩略图

因为缩放会造成 主图的 translate 发生变化 与手动拖拽造成的translate 会有差 所以 要扣除缩放造成的偏移

if (!mainTransform.x && !mainTransform.y && mainTransform.k === 1) {this.initSvg();return;}const {innerZoomInfo, mainWidth, mainHeight,} = this;// 如果传入的 缩放值与之前记录的缩放值不一致 则认为发生了缩放 记录发生缩放后偏移值if (!innerZoomInfo || innerZoomInfo.k !== mainTransform.k) {this.moveDiff = {x: (mainWidth - innerZoomInfo.k * mainWidth) / 2,y: (mainHeight - innerZoomInfo.k * mainHeight) / 2,};}const { x: diffX, y: diffY } = this.moveDiff;const { x, y, k } = mainTransform; // 主图偏移以及缩放数据this.dragThumb.attr('width', mainWidth / k).attr('height', mainHeight / k).attr('transform', () => setTransform({x: -((x - diffX) / k), // 这个地方应该不能直接 除 k 这里的x,y 应该是放大后的x,y应该减去缩放的差值 再 除Ky: -((y - diffY) / k),}));

###自己实现一个简单的拓扑图

####碰撞检测

一开始的逻辑,两个正方形任意正方形包裹住另外一个任意一点则为碰撞 如下图。如果画的真正是个圆形的话则存在精度不足的问题

但是这种情况不适于 两个长方形只相交,如:

最后还是需要改为两个圆进行检测,逻辑为任意两个圆形的圆心距离是否小于两圆半径之和,若小于则为碰撞。


Math.sqrt(Math.pow(circleA.x - circleB.x, 2) +Math.pow(circleA.y - circleB.y, 2)) < circleA.radius + circleB.radius

详情见 aotu实验室 碰撞专栏

####点的分配

点的位置的分配 就是确定中心点后,将关系最多的点作为中心点,其关系点向四周分散,没有关系的同级点,则向中心点四周进行分散,其关系点以确定后位置的点的坐标向周围分散。

根据三角形的正玄、余弦来得值;

假设一个圆的圆心坐标是(a,b),半径为r,角度为d

则圆上每个点的坐标可以通过下面的公式得到


/** @params* d 角度* r 半径长度*/X = a + Math.cos(((Math.PI * 2) / 360) * d) * r;Y = b + Math.sin(((Math.PI * 2) / 360) * d) * r;

角度可以通过 关系边进行得到. d = 360/关系边的数量,确定第一圈点的角度。

拿到角度后 ,维持一个所有点坐标的对象,再结合碰撞上门的碰撞检测,我们就可以遍历 获取所有点的坐标了


/** @params* dotsLocations 所有点的坐标信息*/initNodes() {const { x: centerX, y: centerY } = this.center;const { distance } = this;const getDeg = (all, now) => 360 / (all - (now || 0));// 把中心点分配给线最多的点const centerdot = this.dots[0];centerdot.x = centerX;centerdot.y = centerY;this.dotsLocations[centerdot.id] = { x: centerX, y: centerY };this.dots.forEach((dot) => {const { x: outx, y: outy } = dot;if (!outx && !outy) {// 兄弟点 (无关系的点) 默认以中心店的10度进行遍历dot = this.getLocation(dot, centerX, centerY,10, distance).dot;}const { x: cx, y: cy } = dot;const dotsLength = dot.relationDots.length;let { distance: innerDistance } = this;// 获取剩余点的角度let addDeg = getDeg(dotsLength);dot.relationDots.forEach((relationId, index) => {let relationDot = this.findDot(relationId);if (!relationDot.x && !relationDot.y) {const {dot: resultDot,isPlus,outerR,} = this.getLocation(relationDot, cx, cy, addDeg, innerDistance);if (isPlus) {// 如果第一圈遍历完毕,则开始以 半径 * 2 为第二圈开始遍历innerDistance = outerR;addDeg = getDeg(dotsLength, index);addDeg += randomNumber(5, 9);  //防止第一圈与第二圈的点所生成的角度一致 造成链接的线重叠在一起}relationDot = resultDot;}});});}

// 分配位置getLocation(dot, cx, cy, addDeg, distance) {// 由第一张图 得知 -90度为最上面  从最上面开始循环let outerDeg = -90;let outerR = distance;const { distance: addDistance } = this;let firsted; // 用于分布完后一周while (Object.keys(this.checkDotLocation(dot)).length !== 0) {outerDeg += addDeg;if (outerDeg > 360) {// 转完一圈 随机生成第二圈的角度再开始对当前点进行定位addDeg = randomNumber(10, 35);outerDeg = addDeg;if (firsted) {outerR += addDistance;}firsted = true;}const innerLocation = getDegXy(cx, cy, outerDeg, outerR);dot = Object.assign(dot, innerLocation);}this.dotsLocations[dot.id] = { x: dot.x, y: dot.y };return {dot,isPlus: firsted,outerR,};}
// 碰撞检测checkDotLocation(circleA) {let repeat = false;if (!circleA.x || !circleA.y) return true;const { forceCollide } = this;console.log(this.dotsLocations)Object.keys(this.dotsLocations).forEach((key) => {if (key === circleA.id) {return;}const circleB = this.dotsLocations[key];let isRepeat = Math.sqrt(Math.pow(circleA.x - circleB.x, 2) + Math.pow(circleA.y - circleB.y, 2)) < forceCollide * 2;if(isRepeat)repeat = true;});return repeat;}}

生成时间与D3 的差不多

####碰撞后点的移动 (力?)

碰撞后的逻辑呢 简单的就是已拖动点为圆点,计算碰撞点与圆点的夹角,再通过角度与距离得出碰撞后被碰撞点的x,y的坐标


changeLocation(data, x, y, eliminate) {// 先对原来的点进行赋值data.x = x;data.y = y;// 对点的坐标进行赋值,使之后的碰撞使用新值进行计算this.dotsLocations[data.id] = { x, y };let crashDots = this.checkDotLocation(data);// 获得所有被碰撞的点Object.keys(crashDots).forEach((crashId) => {if (eliminate === crashId) return; // 碰撞后的碰撞防止 更改当前拖拽元素const crashDot = this.findDot(crashId);// 获取被碰撞的x,y 值const { x: crashX, y: crashY } = crashDot;// 此处的角度是要移动的方向的角度let deg = getDeg(crashDot.x,crashDot.y,data.x,data.y);// - 180 的目的是为了 与上面的黑图角度一致// 2是碰撞后  移动2个像素的半径const {x:endX,y:endY} = getDegXy(crashDot.x, crashDot.y, deg - 180, 2);// 讲被碰撞的点作为圆点 改变值 并进行碰撞点的碰撞的碰撞检测(禁止套娃 )this.changeLocation(crashDot, endX, endY, data.id);});}

获取夹角角度


function getDeg(x1,y1,x2,y2){//中心点let cx = x1;let cy = y1;//2个点之间的角度获取let c1 = Math.atan2(y1 - cy, x1 - cx) * 180 / (Math.PI);let c2 = Math.atan2(y2 - cy, x2 - cx) * 180 / (Math.PI);let angle;c1 = c1 <= -90 ? (360 + c1) : c1;c2 = c2 <= -90 ? (360 + c2) : c2;//夹角获取angle = Math.floor(c2 - c1);angle = angle < 0 ? angle + 360 : angle;return angle;}

到此实现一个简单的拓扑图就搞定了。

使用我们自己的force 代替 d3.js 的效果,后期想要什么效果就可以自己再加了 如 拖动主点相关点动,其他关联点不动的需求。

tick方法需要自己手动去调用了


let force = new Force({x: svgW / 2,y: svgH / 2,distance: 200,forceCollide:30,});force.nodes(dot);force.initLines(line);

####拖动

这边的tick 是当 点的xy 发生变化的时候 自己去重新构建点和线。再实际项目中每一次拖动就会构建,会比较卡,可以丢到requestAnimationFrame 去调用

dotDoms.on("mousedown", function (d) {dragDom = {data: d,dom: this,};});d3.select("svg").on("mousemove", function (d) {if (!dragDom) return;const { offsetX: x, offsetY: y } = d3.event;if (x < -1 || y < -1 || x >= svgH - 10 || y >= svgH - 10) {//边界dragDom = null;return;}force.changeLocation(dragDom.data, x, y);tick();});d3.select("svg").on("mouseup", function (d) {dragDom = null;});

使用d3.js开发力导向图相关推荐

  1. d3.js实现力导向图圈选框选

    #d3.js实现力导向图圈选框选 今天给大家带来的是如何在2D可视化图形中加入通过鼠标拖动圈选功能,以力导向图为例. ##最终效果 demo跳转 ##代码解析 我们是要在节点的上方绘制一个矩形覆盖节点 ...

  2. D3.js实现力导向图(Dray和Zoom)

    今天遇到个问题就是把json(里面nodes和edges属性)文件通过D3.js展示出来,下午终于弄出来了,写篇博客记录一下. 先展示一下效果图: 放大后是这个效果: 什么是力导向图 D3.js官网: ...

  3. 关于用d3.js画力导向图(Force-directed Graph)—如何让图收放自如

    使用d3画出很好看的力图,可以展示那种Neo4j知识图谱的效果.但是当一满屏都是小圆圈时又有点丧失了即看即懂的效果.因此需要实现对节点的收放. 主要是对生成力图的数据点集进行操作. 其中在收回某个节点 ...

  4. vuejs+d3.js开发的轨道图+饼图

    应公司要求开发了一个流程关联关系轨道图,在开发的过程中发现可以把统计功能也一块做了,所以就有了现在的轨道图+饼图分析的功能.废话不多少,先上效果图: 采用的vuejs+elementui+d3.js开 ...

  5. d3.js 力导向图 关系连接线 使用 path 标签时不显示的问题

    由于d3.js版本差异,再用d3开发力导向图时,使用path标签时可能会出现不显示的问题,特记录该问题. // links 连接线设置,使用path标签 _this.links = g.append( ...

  6. D3.js的v5版本入门教程(第十四章)—— 力导向图

    D3.js的v5版本入门教程(第十四章) 这一章我们来绘制一个力导向图,什么叫力导向图,通俗一点将就是有节点和线组成,当鼠标拖拽一个节点时,其他节点都会受到影响(力导向图有多种类型,本章绘制的效果就是 ...

  7. D3.js 力导向图来处理拓扑图

    记录一点碰到的问题和解决方案.感觉国内关于D3.js 4.0版本的相关资料还是少. 力导向图布局 D3一种布局的方式,可以将你nodes links的节点数据转换成可以绘制的坐标点数据,然后通过svg ...

  8. d3力导向图增加节点_d3.js力导向图节点如何都显示在边框内

    最近用到d3.js中的force力导向图,想实现效果如下,所有城市节点都在可视范围内,如果超出有滚动条也可以. 遇到的问题是,当节点一多,有的节点就会跑到外面去,这边是通过加大charge相互作用力, ...

  9. D3.js 力导向图的显示优化

    D3.js 作为一个前端,说到可视化除了听过 D3.js 的大名,常见的可视化库还有 ECharts.Chart.js,这两个库功能也很强大,但是有一个共同特点是封装层次高,留给开发者可设计和控制的部 ...

最新文章

  1. android AIDL服务
  2. video camera in shanghai
  3. 如何比较 Java 的字符串
  4. C-Lodop回调函数的触发
  5. Codeforces Round #712 (Div. 2) E. Travelling Salesman Problem 思维转换
  6. Code Chef December Challenge 2018题解
  7. 这是300年后的人类生活!你相信吗?
  8. 使用jfreechart来创建一个简单的饼图
  9. python 实现对地图的点击_python使用folium库绘制地图点击框
  10. 如何避免OOM 异常?
  11. mysql - binlog主从复制
  12. Kafka Producer生产者原理
  13. 计算机网路网络层之IP协议(3)——IP编址
  14. Lync Server 2010安装笔记之五-Lync Server前端服务器高可用
  15. NCURSES程序设计之拼图游戏
  16. RabbitMQ(五) | MQ集群搭建、部署、仲裁队列、集群扩容
  17. 边城性格悲剧_悲剧! 我的手表已延迟!
  18. 好好学习,天天向上------融会贯通
  19. 普源DSA1030-TG,9kHz-3GHz频谱分析仪
  20. 词法分析器(实现报告)

热门文章

  1. WIN10下DOCKER的安装与使用
  2. 2021.5.22 2022蓝桥杯练习赛3
  3. 量能决定趋势 通达信趋势起妖副图 趋势量能选股指标源码
  4. Yahoo,Msn,Skype,QQ,阿里旺旺在线聊天链接接口调用
  5. gdal--矢量求交
  6. echarts xy轴虚线展示 字体颜色修改
  7. 文件夹打包成pkg_linux如何解压tar.gz到指定文件夹或目录
  8. 乘坐北京地铁费用计算
  9. 七种水果不能空腹食用
  10. 【经典】FPGA三国志