在圖形用戶(hù)界面(GUI)設(shè)計(jì)中,自定義連線(xiàn)技術(shù)不僅提升了用戶(hù)體驗(yàn),還為復(fù)雜數(shù)據(jù)可視化開(kāi)辟了新的可能性。該功能點(diǎn)允許用戶(hù)靈活地在界面元素之間創(chuàng)建視覺(jué)連接,使流程圖、思維導(dǎo)圖和網(wǎng)絡(luò)拓?fù)鋱D等信息呈現(xiàn)更加直觀(guān)和動(dòng)態(tài)。
圖撲軟件自研 HT for Web 產(chǎn)品框架中,ht.Edge 節(jié)點(diǎn)用于表示節(jié)點(diǎn)間的連線(xiàn)關(guān)系。熟悉 HT 的用戶(hù)應(yīng)該了解 ht.Edge 內(nèi)置了多種連線(xiàn)類(lèi)型,能滿(mǎn)足一般拓?fù)鋱D需求,但在特殊情況下,這些默認(rèn)類(lèi)型可能無(wú)法滿(mǎn)足需求。為此,HT 提供了自定義連線(xiàn)功能,允許開(kāi)發(fā)者根據(jù)具體需求創(chuàng)建特殊的連線(xiàn)類(lèi)型,實(shí)現(xiàn)更靈活的圖形表示。


自定義連線(xiàn)
圖撲 HT 框架提供靈活的自定義連線(xiàn)功能,開(kāi)發(fā)者可以通過(guò)調(diào)用 ht.Default.setEdgeType(type, func, mutual) 方法來(lái)創(chuàng)建獨(dú)特的連線(xiàn)類(lèi)型。以下是該方法的參數(shù)詳解:
■type:自定義連線(xiàn)類(lèi)型的名稱(chēng),與 style 中的 edge.type 屬性相對(duì)應(yīng)。
■func:計(jì)算連線(xiàn)路徑信息的函數(shù),接收四個(gè)參數(shù):
gap:多條連線(xiàn)成捆時(shí),本連線(xiàn)對(duì)象對(duì)應(yīng)中心連線(xiàn)的間距。
edge:當(dāng)前連線(xiàn)對(duì)象。
graphView:當(dāng)前對(duì)應(yīng)拓?fù)浣M件對(duì)象。
sameSourceWithFirstEdge:boolean 類(lèi)型,該連線(xiàn)是否與同組的第一條連線(xiàn)同源。
■mutual:決定該連線(xiàn)類(lèi)型是否會(huì)影響同一起始或結(jié)束節(jié)點(diǎn)上的其他連線(xiàn)。
接下來(lái),我們深入分析一種常見(jiàn)的拓?fù)潢P(guān)系實(shí)現(xiàn)步驟,即"橫-豎-橫"的連線(xiàn)方式。
下面是一段定義上圖連線(xiàn)類(lèi)型的示例代碼。代碼很簡(jiǎn)單,首先獲取起始節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)的信息,然后根據(jù)這兩個(gè)節(jié)點(diǎn)的坐標(biāo),按照預(yù)定的規(guī)則計(jì)算出連線(xiàn)的路徑點(diǎn)。
ht.Default.setEdgeType('horizontal-vertical', function (edge, gap, graphView) {
const points = new ht.List();
const segments = new ht.List();
const source = edge.getSource();
const target = edge.getTarget();
const sourceP = source.p();
const targetP = target.p();
points.add(sourceP);
if (targetP.x !== sourceP.x) {
points.add({ x: sourceP.x + (targetP.x - sourceP.x) / 2, y: sourceP.y });
points.add({ x: sourceP.x + (targetP.x - sourceP.x) / 2, y: targetP.y });
}
points.add(targetP);
return { points, segments };
})
定義好連線(xiàn)類(lèi)型后,只需通過(guò) edge.s('edge.type', 'horizontal-vertical') 這段簡(jiǎn)單的代碼行,就能將 edge 對(duì)象的連線(xiàn)設(shè)置為我們剛剛定義的類(lèi)型。由此一來(lái),即可看到令人滿(mǎn)意的效果,大幅提升圖形的可讀性和美觀(guān)度。
總線(xiàn)拓?fù)?/strong>
總線(xiàn)拓?fù)涫且环N網(wǎng)絡(luò)結(jié)構(gòu),所有設(shè)備(如計(jì)算機(jī)、打印機(jī)等)都連接到一個(gè)共同的通信介質(zhì)上,通常是一根電纜,這個(gè)介質(zhì)被稱(chēng)為"總線(xiàn)"(bus)。總線(xiàn)拓?fù)湓?a target="_blank">工業(yè)控制和嵌入式系統(tǒng)等特定領(lǐng)域中被廣泛應(yīng)用。在圖撲 HT 框架中,我們可以利用 ht.Shape 組件繪制總線(xiàn),并通過(guò) ht.Edge 組件將各個(gè)設(shè)備節(jié)點(diǎn)連接到總線(xiàn)上。這些連接的視覺(jué)表現(xiàn)可通過(guò)自定義連線(xiàn)類(lèi)型靈活定義,從而實(shí)現(xiàn)精確的總線(xiàn)拓?fù)鋱D表示。
上面展示的是一個(gè)總線(xiàn)的示例效果,可以直觀(guān)看到所有設(shè)備都連接到了總線(xiàn)上。在具體實(shí)現(xiàn)過(guò)程中,最具挑戰(zhàn)性的問(wèn)題是:如何計(jì)算出總線(xiàn)上距離目標(biāo)節(jié)點(diǎn)坐標(biāo)最近的點(diǎn)?
計(jì)算節(jié)點(diǎn)到總線(xiàn)距離
總線(xiàn)通常由多條直線(xiàn)段組成,因此計(jì)算某一節(jié)點(diǎn)到總線(xiàn)的最短距離可按以下思路進(jìn)行:
將總線(xiàn)分割為多段直線(xiàn)
總線(xiàn)由多個(gè)直線(xiàn)段構(gòu)成,可以取總線(xiàn)上相鄰兩點(diǎn)構(gòu)成一條直線(xiàn)。具體實(shí)現(xiàn)時(shí),遍歷 points 數(shù)據(jù),獲取 points[index] 和 points[index+1] 作為線(xiàn)段的兩個(gè)端點(diǎn)。注意,如果設(shè)置了 segments,其中 1 代表新路徑的起點(diǎn),所以當(dāng) segments[index+1] 為 1 時(shí)應(yīng)跳過(guò)。
計(jì)算點(diǎn)到每條直線(xiàn)的距離
獲取每條直線(xiàn)段后,計(jì)算節(jié)點(diǎn)坐標(biāo)到各線(xiàn)段的距離,并將距離值存入一個(gè)集合中。
獲取最短距離
從距離集合中找出最小值,即為節(jié)點(diǎn)到總線(xiàn)的最短距離。
基于上述思路,我們可以實(shí)現(xiàn)一個(gè)總線(xiàn)連線(xiàn)類(lèi)型。以下是具體的實(shí)現(xiàn)代碼:
// 計(jì)算點(diǎn)到直線(xiàn)的距離,返回結(jié)果是個(gè)對(duì)象結(jié)構(gòu)
var pointToInsideLine = function (p1, p2, p) {
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x = p.x,
y = p.y,
result = {},
dx = x2 - x1,
dy = y2 - y1,
d = Math.sqrt(dx * dx + dy * dy),
ca = dx / d, // cosine
sa = dy / d, // sine
mX = (-x1 + x) * ca + (-y1 + y) * sa;
result.x = x1 + mX * ca;
result.y = y1 + mX * sa;
if (!isPointInLine(result, p1, p2)) {
result.x = Math.abs(result.x - p1.x) < Math.abs(result.x - p2.x) ? p1.x : p2.x;
result.y = Math.abs(result.y - p1.y) < Math.abs(result.y - p2.y) ? p1.y : p2.y;
}
dx = x - result.x;
dy = y - result.y;
result.z = Math.sqrt(dx * dx + dy * dy);
return result;
};
// 判斷點(diǎn)是否在線(xiàn)上
var isPointInLine = function (p, p1, p2) {
return p.x >= Math.min(p1.x, p2.x) &&
p.x <= Math.max(p1.x, p2.x) &&
p.y >= Math.min(p1.y, p2.y) &&
p.y <= Math.max(p1.y, p2.y);
};
// 注冊(cè)連線(xiàn)類(lèi)型
ht.Default.setEdgeType('bus', function (edge) {
var source = edge.getSourceAgent(),
target = edge.getTargetAgent();
var targetP = target.p();
var points = source.getPoints().toArray();
var segments = source.getSegments();
var beginPoint;
for (let i = 0; i < points.length - 1; i++) {
if (segments) {
if (segments[i + 1] === 1) continue;
}
const point1 = points[i];
const point2 = points[i + 1];
const minPosition = pointToInsideLine(point1, point2, targetP);
if (!beginPoint || minPosition.z < beginPoint.z) {
beginPoint = minPosition;
}
}
return {
points: new ht.List([ beginPoint, targetP ]),
segments: new ht.List([1, 2])
};
});
執(zhí)行上述代碼后,我們將得到如下效果:
從上圖可以清楚看出,示例成功獲取了節(jié)點(diǎn)到總線(xiàn)的最近點(diǎn),并繪制了相應(yīng)的連線(xiàn)節(jié)點(diǎn)。值得注意的是,對(duì)于直線(xiàn)段而言,節(jié)點(diǎn)在直線(xiàn)上的投影點(diǎn)即為其距總線(xiàn)最近的點(diǎn)。
視覺(jué)美感優(yōu)化
雖然示例已實(shí)現(xiàn)了基礎(chǔ)總線(xiàn)效果,但由于拓?fù)鋱D采用 2.5D 效果,僅計(jì)算投影點(diǎn)可能無(wú)法呈現(xiàn)理想的視覺(jué)效果。為了增強(qiáng)視覺(jué)表現(xiàn),我們可以考慮讓連線(xiàn)旋轉(zhuǎn)一定角度。為此,我們可以在現(xiàn)有功能的基礎(chǔ)上添加旋轉(zhuǎn)代碼,使連線(xiàn)與整體圖形更加協(xié)調(diào),提升視覺(jué)美感。
ht.Default.setEdgeType('bus', function (edge) {
var source = edge.getSourceAgent(),
target = edge.getTargetAgent();
var targetP = target.p();
var points = source.getPoints().toArray();
var segments = source.getSegments();
var beginPoint, linePoints;
for (let i = 0; i < points.length - 1; i++) {
if (segments) {
if (segments[i + 1] === 1) continue;
}
const point1 = points[i];
const point2 = points[i + 1];
const minPosition = pointToInsideLine(point1, point2, targetP);
if (!beginPoint || minPosition.z < beginPoint.z) {
beginPoint = minPosition;
linePoints = [point1, point2]
}
}
var rotation = angleBetweenLineAndHorizontal(linePoints[0], linePoints[1]);
var rotatePoint = findIntersection([rotatePointAroundAnotherPoint(beginPoint, targetP, rotation), targetP], linePoints);
if(isPointInLine(rotatePoint, linePoints[0], linePoints[1])){
beginPoint = rotatePoint;
}
return {
points: new ht.List([
beginPoint, targetP
]),
segments: new ht.List([1, 2])
};
});
/**
* 計(jì)算兩點(diǎn)之間直線(xiàn)與水平線(xiàn)的夾角
*/
function angleBetweenLineAndHorizontal(p1, p2) {
if (new ht.Math.Vector2(p1.x, p1.y).length() > new ht.Math.Vector2(p2.x, p2.y).length()) {
var p = p2;
p2 = p1;
p1 = p;
}
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y;
var dx = x2 - x1;
var dy = y2 - y1;
var angleRadians = Math.atan2(dy, dx); // 計(jì)算夾角(弧度)
var angleDegrees = angleRadians * (180 / Math.PI); // 弧度轉(zhuǎn)角
// 確保角度在 0 到 360 之間
if (angleDegrees < 0) {
angleDegrees += 360;
}
return angleDegrees;
}
function rotatePointAroundAnotherPoint(point, center, angleDegrees) {
var angleRadians = angleDegrees * (Math.PI / 180);
var cosTheta = Math.cos(angleRadians);
var sinTheta = Math.sin(angleRadians);
var translatedX = point.x - center.x;
var translatedY = point.y - center.y;
var rotatedX = translatedX * cosTheta - translatedY * sinTheta;
var rotatedY = translatedX * sinTheta + translatedY * cosTheta;
var finalX = rotatedX + center.x;
var finalY = rotatedY + center.y;
return { x: finalX, y: finalY };
}
/**
* 給定兩個(gè)點(diǎn),計(jì)算直線(xiàn)的系數(shù) A, B, C
* 直線(xiàn)方程:Ax + By = C
*/
function getLineEquation(x1, y1, x2, y2) {
var A = y2 - y1;
var B = x1 - x2;
var C = A * x1 + B * y1;
return { A, B, C };
}
/**
* 計(jì)算兩條直線(xiàn)的交點(diǎn)
*/
function calculateIntersection(line1, line2) {
var { A: A1, B: B1, C: C1 } = line1;
var { A: A2, B: B2, C: C2 } = line2;
var determinant = A1 * B2 - A2 * B1;
if (determinant === 0) {
// 平行或重合
return null;
} else {
var x = (C1 * B2 - C2 * B1) / determinant;
var y = (A1 * C2 - A2 * C1) / determinant;
return { x, y };
}
}
/**
* 找到兩條線(xiàn)的交點(diǎn),或者延長(zhǎng)線(xiàn)的交點(diǎn)
*/
function findIntersection(line1Points, line2Points) {
var [p1, p2] = line1Points;
var [p3, p4] = line2Points;
var line1 = getLineEquation(p1.x, p1.y, p2.x, p2.y);
var line2 = getLineEquation(p3.x, p3.y, p4.x, p4.y);
var intersection = calculateIntersection(line1, line2);
return intersection;
}
實(shí)現(xiàn)的最終效果如下:
圖撲軟件 HT 自定義連線(xiàn)功能為圖形交互設(shè)計(jì)開(kāi)辟了廣闊的新天地。從基本的"橫-豎-橫"連線(xiàn)到復(fù)雜的總線(xiàn)拓?fù)鋱D,不僅提升了數(shù)據(jù)可視化的靈活性,還大幅增強(qiáng)了用戶(hù)體驗(yàn)。通過(guò)精細(xì)調(diào)整連線(xiàn)的旋轉(zhuǎn)角度和投影點(diǎn),在 2.5D 效果中呈現(xiàn)更加美觀(guān)和直觀(guān)的拓?fù)潢P(guān)系。
不僅適用于網(wǎng)絡(luò)結(jié)構(gòu)的展示,還可擴(kuò)展到各種復(fù)雜系統(tǒng)的可視化中。為設(shè)計(jì)師和開(kāi)發(fā)者提供了強(qiáng)大的工具,幫助他們創(chuàng)造出更加豐富、富有表現(xiàn)力的圖形界面。
審核編輯 黃宇
-
拓?fù)鋱D
+關(guān)注
關(guān)注
1文章
20瀏覽量
14748 -
數(shù)據(jù)可視化
+關(guān)注
關(guān)注
0文章
500瀏覽量
11475
發(fā)布評(píng)論請(qǐng)先 登錄
WebGIS 智慧交通——路網(wǎng)運(yùn)行態(tài)勢(shì) BI 可視化大屏
基于圖撲 HT 引擎:數(shù)字孿生民航飛聯(lián)網(wǎng)方案
基于圖撲 HT 數(shù)字孿生 3D 風(fēng)電場(chǎng)可視化系統(tǒng)實(shí)現(xiàn)解析
工業(yè)數(shù)字孿生:圖撲可視化技術(shù)架構(gòu)與行業(yè)應(yīng)用解析
基于HT實(shí)現(xiàn)海上LNG終端數(shù)字孿生可視化監(jiān)控
基于 HT 技術(shù)的園區(qū)元宇宙可視化管理平臺(tái)
圖撲 HT 驅(qū)動(dòng)智慧社區(qū)數(shù)字化轉(zhuǎn)型:多維可視化與系統(tǒng)集成實(shí)踐
圖撲 HT 數(shù)字孿生在智慧加油站中的技術(shù)實(shí)現(xiàn)與應(yīng)用解析
圖撲 HT 自研技術(shù)架構(gòu)下 AR 應(yīng)用開(kāi)發(fā)與行業(yè)解決方案實(shí)現(xiàn)
圖撲 HT 技術(shù)賦能智慧畜牧三維可視化:架構(gòu)設(shè)計(jì)與實(shí)踐應(yīng)用
基于 HT 搭建的農(nóng)林牧數(shù)據(jù)可視化監(jiān)控平臺(tái)
基于 HT 可視化實(shí)現(xiàn)三維物流園區(qū)一體化管控系統(tǒng)
HT 可視化在工業(yè)產(chǎn)線(xiàn)看板智能化應(yīng)用中的技術(shù)實(shí)現(xiàn)
基于 HT 的 3D 可視化智慧礦山開(kāi)發(fā)實(shí)現(xiàn)
基于圖撲 HT 技術(shù)的電纜廠(chǎng) 3D 可視化管控系統(tǒng)深度解析
圖撲 HT 總線(xiàn)式拓?fù)鋱D的可視化實(shí)現(xiàn)
評(píng)論