tva
← บทความเชิงลึก

ReactFlow สำหรับแสดงผังเครือญาติ

ผังเครือญาติคือกราฟ ข้อมูลประกอบด้วยโหนดแทนบุคคลและเส้นเชื่อมที่ระบุประเภทความสัมพันธ์ เช่น พ่อแม่กับลูก คู่สมรส และพี่น้อง ปัญหาคือจะจัดกราฟอย่างไรให้อ่านเข้าใจได้ในหลายขนาด ตั้งแต่มุมมองสามรุ่นไปจนถึงสายตระกูลหลายรุ่นที่มีคนหลายร้อยคน ReactFlow จัดการโครงสร้างพื้นฐานด้านการแสดงผลและการโต้ตอบ ส่วนการตัดสินใจสำคัญอยู่ที่ประเภทโหนดที่กำหนดเอง อัลกอริทึมจัดวาง Dagre รูปแบบเส้นเชื่อมแต่ละความสัมพันธ์ และกลยุทธ์ประสิทธิภาพที่จำเป็นเมื่อผังใหญ่ขึ้น

บทความนี้อธิบายการตัดสินใจในการพัฒนา ข้อแลกเปลี่ยน และส่วนที่รูปแบบของ ReactFlow ทั้งช่วยและจำกัดสิ่งที่เราสร้าง


เหตุใดจึงเลือก ReactFlow

ทางเลือกที่เราพิจารณาจริงจังคือ D3 ซึ่งมีอัลกอริทึมจัดวางต้นไม้ที่พัฒนามานาน มีเอกสารดี และใช้แสดงผังตระกูลมาหลายปี แต่ D3 ต้องจัดการ DOM โดยตรง จึงขัดกับกระบวนการปรับ DOM ให้ตรงกับสถานะของ React หากต้องการใช้คอมโพเนนต์ React สำหรับส่วนโต้ตอบ แนวทางผสมทำได้แต่ดูแลยุ่งยาก

ReactFlow ใช้รูปแบบคอมโพเนนต์ React ทั้งหมด โหนดและเส้นเชื่อมเป็นออบเจ็กต์ JavaScript ธรรมดาในสถานะ ไลบรารีจัดการการแสดงผล (โหนด HTML และเส้นเชื่อม SVG) การเลื่อนมุมมอง การซูม และเหตุการณ์โต้ตอบ โหนดที่กำหนดเองเป็นเพียงคอมโพเนนต์ React จึงใส่เนื้อหาโต้ตอบที่หลากหลาย เช่น พื้นที่รูปภาพ รายละเอียดที่ขยายได้ และปุ่มทำงานไว้ในแต่ละโหนดได้โดยไม่ออกจากรูปแบบ React

ข้อแลกเปลี่ยนคือ ReactFlow กำหนดแนวทางไว้มากกว่า D3 แบบจำลองโหนดและเส้นเชื่อมมีโครงสร้างเฉพาะ ต้องจัดวางจากภายนอกเพราะไม่มีอัลกอริทึมจัดวางในตัว และต้องจัดการประสิทธิภาพโดยตรงมากกว่าแนวทาง D3 ล้วนเมื่อมีโหนดจำนวนมาก แต่สำหรับโครงการระดับไม่กี่ร้อยโหนด ข้อแลกเปลี่ยนนี้คุ้มค่า

โหนดที่กำหนดเอง: การ์ดบุคคล

ระบบโหนดที่กำหนดเองเป็นจุดเด่นของ ReactFlow โหนดที่กำหนดเองคือคอมโพเนนต์ React ที่รับข้อมูลโหนดผ่าน props และแสดงผลได้ตามต้องการ ข้อกำหนดเดียวคือต้องแสดงคอมโพเนนต์ Handle ณ จุดเชื่อมต่อที่เส้นเชื่อมเข้ามายึด

คอมโพเนนต์ PersonNode แสดงการ์ดที่มีชื่อ ปีเกิด ปีเสียชีวิตหากมี และพื้นที่รูปภาพที่แสดงอักษรย่อชื่อเมื่อไม่มีรูป ขอบการ์ดเปลี่ยนสีอย่างละเอียดตามระดับรุ่น เป็นสัญญาณทางภาพที่ช่วยให้ผู้ชมรู้ตำแหน่งในผังที่ลึก คนที่ยังมีชีวิตและคนที่เสียชีวิตใช้รูปแบบการแสดงผลต่างกัน

การตัดสินใจที่ทำให้ชัดเจนขึ้นมากคือใช้จุดเชื่อมแยกกันสำหรับเส้นเชื่อมแต่ละประเภท โดยจุดเชื่อมพ่อแม่กับลูกอยู่ด้านบนและด้านล่าง ส่วนจุดเชื่อมคู่สมรสอยู่ด้านซ้ายและด้านขวา เส้นพ่อแม่กับลูกเชื่อมจากด้านล่างไปด้านบน ให้ผังไหลแนวตั้งในทิศทางสม่ำเสมอ ส่วนเส้นคู่สมรสเชื่อมด้านข้างในระดับรุ่นเดียวกัน การแยกประเภทเส้นตามตำแหน่งจุดเชื่อมช่วยให้เห็นทิศทางความสัมพันธ์ทันที

คอมโพเนนต์โหนดยังจัดการสถานะ “ยุบ” สำหรับต้นไม้ย่อยที่ลึก เมื่อยุบต้นไม้ย่อย โหนดจะแสดงป้ายจำนวนลูกหลานที่ซ่อนอยู่ คลิกป้ายเพื่อขยาย รูปแบบการโต้ตอบนี้จำเป็นต่อการใช้งานผังขนาดใหญ่ การแสดงสี่ร้อยโหนดพร้อมกันอ่านไม่รู้เรื่อง แต่การยุบผังส่วนใหญ่แล้วขยายกิ่งตามต้องการทำให้สำรวจผังได้

การจัดวางลำดับชั้นด้วย Dagre

ReactFlow ไม่คำนวณตำแหน่งโหนด คุณต้องระบุในข้อมูลโหนดแล้วไลบรารีแสดงตามตำแหน่งนั้น หากคำนวณเองสำหรับผังเครือญาติ ก็ต้องสร้างอัลกอริทึมจัดวางลำดับชั้นขึ้นใหม่ ทางเลือกมาตรฐานคือ Dagre ไลบรารีจัดวางกราฟมีทิศทางที่ไม่มีวงจร ซึ่งสร้างลำดับชั้นที่ชัดเจนและมีเส้นตัดกันน้อยที่สุด

การเชื่อมต่อ Dagre ทำงานนอกวงจรแสดงผลของ ReactFlow เมื่อข้อมูลโหนดหรือเส้นเชื่อมเปลี่ยน ฟังก์ชันจัดวางจะทำงานแบบซิงโครนัส กำหนดตำแหน่งให้แต่ละโหนด และคืนอาร์เรย์โหนดพร้อมตำแหน่งให้ ReactFlow แสดงผล ฟังก์ชันรับโหนดและเส้นเชื่อมที่ยังไม่จัดวาง สร้างกราฟ Dagre จัดวาง แล้วอ่านค่า x และ y ที่คำนวณได้กลับมา

import dagre from '@dagrejs/dagre';

function layoutTree(nodes, edges, nodeWidth, nodeHeight) {
  const g = new dagre.graphlib.Graph();
  g.setDefaultEdgeLabel(() => ({}));
  g.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 40 });

  nodes.forEach(node => {
    g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  });
  edges.forEach(edge => {
    g.setEdge(edge.source, edge.target);
  });

  dagre.layout(g);

  return nodes.map(node => {
    const { x, y } = g.node(node.id);
    return { ...node, position: { x: x - nodeWidth / 2, y: y - nodeHeight / 2 } };
  });
}

พารามิเตอร์ rankdir ของ Dagre ควบคุมทิศทางการจัดวาง “TB” หรือจากบนลงล่างเหมาะกับผังเครือญาติที่เวลาไหลลงตามรุ่น ranksep ควบคุมระยะแนวตั้งระหว่างรุ่น ส่วน nodesep ควบคุมระยะแนวนอนระหว่างพี่น้อง ต้องปรับค่าเหล่านี้ให้เหมาะกับขนาดโหนดที่ใช้

ข้อจำกัดของ Dagre สำหรับผังเครือญาติคือมองกราฟเป็น DAG เส้นคู่สมรสซึ่งเชื่อมสองโหนดในระดับเดียวกันและไม่มีทิศทางไม่ใช่เส้นของ DAG ตามธรรมชาติ วิธีเลี่ยงข้อจำกัดคือจัดวาง Dagre โดยใช้เฉพาะเส้นพ่อแม่กับลูกเพื่อกำหนดระดับรุ่นและตำแหน่งแนวนอน แล้ววาดเส้นคู่สมรสแยกเป็นเส้นตรงที่พิกัด y เดียวกัน ฟังก์ชันแบบย่อข้างต้นจัดวางเส้นเชื่อมทุกเส้นที่ได้รับ จึงต้องกรองรายการให้เหลือเฉพาะเส้นพ่อแม่กับลูกก่อนเรียกใช้ นอกจากนี้ Dagre เพียงอย่างเดียวไม่รับประกันว่าคู่สมรสจะอยู่ข้างกันหรือในระดับเดียวกัน เพราะคู่สมรสที่ไม่มีข้อมูลพ่อแม่และไม่มีลูกในผังที่มองเห็นจะไม่มีเส้นพ่อแม่กับลูกเลย จึงต้องมีขั้นตอนปรับตำแหน่งคู่สมรสหลังจัดวางเพื่อให้ได้ผลตามต้องการ: คู่สมรสอยู่ข้างกันในระดับเดียวกัน มีเส้นแนวนอนเชื่อมระหว่างทั้งคู่

รูปแบบเส้นเชื่อมสำหรับแต่ละความสัมพันธ์

ReactFlow รองรับคอมโพเนนต์เส้นเชื่อมที่กำหนดเองเช่นเดียวกับคอมโพเนนต์โหนด ความสัมพันธ์แต่ละประเภท ได้แก่ พ่อแม่กับลูก คู่สมรส การรับบุตรบุญธรรม และความสัมพันธ์กับพ่อแม่เลี้ยง ใช้รูปแบบเส้นต่างกัน เส้นพ่อแม่กับลูกเป็นเส้นทึบ เส้นคู่สมรสเป็นเส้นเชื่อมแนวนอนบาง ความสัมพันธ์จากการรับบุตรบุญธรรมใช้เส้นประ ส่วนความสัมพันธ์กับพ่อแม่เลี้ยงใช้เส้นจุด

คอมโพเนนต์เส้นเชื่อมรับตำแหน่งโหนดต้นทางและปลายทางผ่าน props แล้วแสดงเส้นทาง SVG เส้นตรงทำได้ง่าย สำหรับเส้นโค้งระหว่างโหนดคนละตำแหน่ง ReactFlow มีฟังก์ชันช่วยสร้างเส้นทาง ได้แก่ getStraightPath, getBezierPath และ getSmoothStepPath ซึ่งสร้างข้อความเส้นทาง SVG ที่ถูกต้องตามพิกัดปลายเส้น

คอมโพเนนต์เส้นเชื่อมที่กำหนดเองช่วยให้แสดงป้ายคำอธิบายความสัมพันธ์เต็มเมื่อวางตัวชี้เหนือเส้น ซึ่งมีประโยชน์กับโครงสร้างครอบครัวซับซ้อนที่รูปแบบเส้นอย่างเดียวอาจกำกวม เช่น เมื่อวางตัวชี้เหนือเส้นระหว่างสองโหนด จะแสดง “พ่อแม่บุญธรรม — 1987” หรือ “สมรส 1975 หย่า 1989” ป้ายแสดงผ่าน foreignObject ใน SVG จึงใช้ HTML และ CSS มาตรฐานกำหนดรูปแบบคำแนะนำได้

ประสิทธิภาพเมื่อผังมีขนาดใหญ่

ReactFlow จัดการผังขนาดปานกลางถึงไม่กี่ร้อยโหนดได้โดยไม่ต้องปรับประสิทธิภาพเฉพาะ เมื่อใหญ่กว่านั้นต้องใช้กลยุทธ์บางอย่าง

ReactFlow สามารถแสดงเฉพาะโหนดและเส้นเชื่อมที่อยู่ในพื้นที่มองเห็นปัจจุบันได้ ฟังก์ชันนี้ปิดอยู่โดยค่าเริ่มต้น และเปิดใช้ได้ด้วย prop onlyRenderVisibleElements เมื่อเปิดใช้แล้วจะจัดการปัญหาประสิทธิภาพส่วนใหญ่ของผังขนาดใหญ่ได้ หากคอมโพเนนต์โหนดเองไม่ได้ใช้ทรัพยากรมากในการแสดงผล PersonNode ที่ดึงข้อมูลภายนอกหรือคำนวณหนักทุกครั้งที่แสดงผลจะทำให้กระตุก ไม่ว่าจะตัดโหนดนอกพื้นที่มองเห็นออกหรือไม่ จึงควรให้คอมโพเนนต์โหนดไม่มีผลข้างเคียงและทำงานเร็ว

กลยุทธ์ที่สองคือจดจำอาร์เรย์โหนดและเส้นเชื่อมด้วย memoization เนื่องจาก ReactFlow แสดงผลใหม่เมื่อการอ้างอิงอาร์เรย์โหนดหรือเส้นเชื่อมเปลี่ยน การส่งอาร์เรย์ที่สร้างใหม่ทุกครั้งที่คอมโพเนนต์แม่แสดงผลทำให้เกิดงานที่ไม่จำเป็น การครอบการคำนวณจัดวางด้วย useMemo โดยกำหนดให้ขึ้นกับข้อมูลต้นทาง ไม่ใช่สถานะที่คำนวณต่อจากข้อมูล จะทำให้การคำนวณจัดวางที่มีต้นทุนสูงและการแสดงผลใหม่ที่เกี่ยวข้องเกิดเฉพาะเมื่อข้อมูลจริงเปลี่ยน

กลยุทธ์ที่สามคือการยุบและขยายที่อธิบายในส่วนประเภทโหนด ผัง 400 โหนดที่แสดงเพียง 40 โหนดในแต่ละขณะมีประสิทธิภาพใกล้เคียงกับผัง 40 โหนด สถานะการยุบเก็บในสถานะ React ระดับการแสดงภาพ ไม่ใช่ในแบบจำลองข้อมูลพื้นฐาน เมื่อยุบต้นไม้ย่อย จะกรองส่วนนั้นออกจากอาร์เรย์โหนดและเส้นเชื่อมที่ส่งให้ ReactFlow เมื่อขยายก็เพิ่มกลับ แล้วจัดวาง Dagre ใหม่ด้วยชุดข้อมูลที่กรองแล้ว

ข้อควรพิจารณาในการออกแบบการโต้ตอบ

นอกจากกลไกจัดวางและแสดงผล รูปแบบการโต้ตอบของผังเครือญาติต้องคิดอย่างรอบคอบ ReactFlow มีการเลื่อนมุมมองและซูมมาให้ แต่ภาพเริ่มต้นและระดับซูมมีผลมากต่อความสะดวกในการใช้งาน การเรียก fitView() หลังแสดงผลการจัดวางทำให้ผังอยู่ในพื้นที่มองเห็นและมีขนาดเหมาะสมตอนโหลด

การสำรวจระหว่างบุคคล เช่น คลิกคนหนึ่งเพื่อจัดให้อยู่กลางพื้นที่มองเห็น หรือไล่ตามเส้นเชื่อมไปยังรุ่นที่อยู่ติดกัน ต้องใช้เมธอด setCenter() และ fitBounds() ของอินสแตนซ์ ReactFlow เข้าถึงได้ผ่านฮุก useReactFlow() และควรเปิดแอนิเมชันที่ลื่นไหลเมื่อเรียก เพื่อให้ผู้ใช้รับรู้ความต่อเนื่องของตำแหน่งในผัง

การค้นหาและกรอง เช่น หาบุคคลตามชื่อแล้วไปยังตำแหน่งของคนนั้นในผัง เป็นสิ่งที่ผังเครือญาติซึ่งมีความซับซ้อนมักต้องมี ในการพัฒนานี้ โหนดที่พบจะกะพริบเน้นหลังจัดพื้นที่มองเห็นให้อยู่ตรงกลาง ทำโดยเพิ่มคลาสแอนิเมชัน CSS ให้โหนดที่ตรงกันช่วงสั้น ๆ แล้วนำออก เมธอด updateNode ของ ReactFlow จัดการได้โดยไม่ต้องจัดวางทั้งผังใหม่

บทความที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง