תארו לעצמכם: בחברה גדולה, מחלקת משאבי אנוש מעדכנת ידנית את רשימת העובדים בקובץ PDF. לאחר שבוע, הנתונים הופכים למיושנים, ההיררכיה מתבלבלת, ומציאת מומחה ספציפי הופכת למשימה מורכבת. בפרויקט אחד עבור חברת EdTech עם למעלה מ-500 עובדים, החלפנו את קובץ ה-PDF הידני בעץ אינטראקטיבי, וצמצמנו את זמן חיפוש איש הקשר מ-5 דקות ל-10 שניות, תוך חיסכון של 1.4–1.9 אלף דולר בשנה לחברה. אנו פותרים בעיה זו באמצעות ויזואליזציה אינטראקטיבית של מבנה ארגוני (Org Chart) — עץ עובדים דינמי עם כרטיסים, קשרים וחיפוש. כלי כזה לא רק מפשט את הניווט, אלא גם משמש כמקור מידע יחיד ואמין לכל הצוות.
הניסיון שלנו — 10 שנים בשוק, למעלה מ-50 פרויקטים עבור EdTech, קמעונאות והמגזר הציבורי. אנו משלבים Org Chart עם 1C, SAP וכל מערכות משאבי אנוש באמצעות REST API. להלן, נפרט את הגישות המרכזיות ליישום.
איך לבחור ספרייה ל-Org Chart אינטראקטיבי?
| ספרייה | גמישות | קלות התחלה | תמיכה ב-React | תכונות עיקריות |
|---|---|---|---|---|
| d3-hierarchy | גבוהה | נמוכה | כן (דרך SVG) | שליטה מלאה, זום/פאן, אנימציות מותאמות אישית |
| react-organizational-chart | בינונית | גבוהה | כן (JSX) | פשטות, עץ מוכן, עיצוב CSS |
| OrgChart.js | גבוהה | בינונית | כן (JS API) | גרירה ושחרור, ייצוא ל-PNG/PDF, מפת מיני |
| @ant-design/graphs | גבוהה | בינונית | כן (React) | סוגי גרפים רבים, ערכת נושא כהה, רספונסיביות |
הבחירה תלויה בדרישות. אם אתם זקוקים לשליטה מלאה בעיבוד ובפריסות כרטיסים לא סטנדרטיות — d3-hierarchy. אם הפרויקט טיפוסי ואתם צריכים השקה מהירה — react-organizational-chart (מהיר פי 3 להתחלה). עבור ארגונים עם גרירה ושחרור — OrgChart.js.
למה אנו משתמשים ב-d3-hierarchy?
בפרויקטים מורכבים (רשת של 2000+ עובדים, עומק היררכיה עד 10 רמות), d3-hierarchy נותן גמישות גדולה פי 2 מ-react-organizational-chart. אנו שולטים בכל פיקסל: כרטיסים מותאמים אישית עם תמונות פרופיל, פסי התקדמות, פעולות. אנו תומכים בטעינה עצלה של צמתי משנה — העץ נפתח בלחיצה, ומצמצם את העיבוד הראשוני ל-100 אלפיות השנייה.
הערה: כפי שצוין ב-d3-hierarchy, ספרייה זו מספקת אלגוריתמים למבני עץ עם שליטה מלאה בפריסה.
d3-hierarchy + React
import { hierarchy, tree } from 'd3-hierarchy';
import { useState, useMemo } from 'react';
interface Employee {
id: string;
name: string;
position: string;
department: string;
avatarUrl?: string;
children?: Employee[];
}
function OrgChart({ data }: { data: Employee }) {
const [expanded, setExpanded] = useState<Set<string>>(new Set([data.id]));
const [hoveredId, setHoveredId] = useState<string | null>(null);
const width = 900;
const nodeWidth = 180;
const nodeHeight = 80;
const { nodes, links } = useMemo(() => {
const root = hierarchy(data);
const treeLayout = tree<Employee>()
.nodeSize([nodeWidth + 20, nodeHeight + 40])
.separation((a, b) => (a.parent === b.parent ? 1.2 : 2));
const laid = treeLayout(root);
return { nodes: laid.descendants(), links: laid.links() };
}, [data]);
// Центрирование дерева
const xs = nodes.map(n => n.x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const treeWidth = maxX - minX + nodeWidth;
const treeHeight = Math.max(...nodes.map(n => n.depth)) * (nodeHeight + 40) + nodeHeight + 20;
const offsetX = (width - treeWidth) / 2 - minX;
return (
<div className="overflow-auto">
<svg width={width} height={treeHeight + 20}>
<g transform={`translate(0, 20)`}>
{/* Связи */}
{links.map((link, i) => (
<path
key={i}
d={`M${link.source.x + offsetX},${link.source.y + nodeHeight} C${link.source.x + offsetX},${(link.source.y + link.target.y + nodeHeight) / 2} ${link.target.x + offsetX},${(link.source.y + link.target.y + nodeHeight) / 2} ${link.target.x + offsetX},${link.target.y}`}
fill="none"
stroke="#d1d5db"
strokeWidth={1.5}
/>
))}
{/* Узлы */}
{nodes.map(node => (
<foreignObject
key={node.data.id}
x={node.x + offsetX - nodeWidth / 2}
y={node.y}
width={nodeWidth}
height={nodeHeight}
>
<EmployeeCard
employee={node.data}
isHovered={hoveredId === node.data.id}
hasChildren={!!node.data.children?.length}
isExpanded={expanded.has(node.data.id)}
onHover={setHoveredId}
onToggle={(id) => setExpanded(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
})}
/>
</foreignObject>
))}
</g>
</svg>
</div>
);
}
function EmployeeCard({ employee, isHovered, onHover, onToggle, hasChildren, isExpanded }) {
return (
<div
className={`border rounded-lg bg-white p-2 shadow-sm text-center cursor-pointer transition-shadow ${isHovered ? 'shadow-md ring-2 ring-blue-300' : ''}`}
onMouseEnter={() => onHover(employee.id)}
onMouseLeave={() => onHover(null)}
onClick={() => onToggle(employee.id)}
>
{employee.avatarUrl && (
<img src={employee.avatarUrl} className="w-8 h-8 rounded-full mx-auto mb-1" />
)}
<p className="text-xs font-semibold text-gray-800 leading-tight">{employee.name}</p>
<p className="text-xs text-gray-500">{employee.position}</p>
{hasChildren && (
<span className="text-xs text-blue-400">{isExpanded ? '▲' : '▼'}</span>
)}
</div>
);
} react-organizational-chart (גישה פשוטה)
import { Tree, TreeNode } from 'react-organizational-chart';
function SimpleOrgChart({ data }) {
return (
<Tree
label={<OrgNode employee={data} />}
lineWidth="2px"
lineColor="#d1d5db"
lineStyle="solid"
>
{data.children?.map(child => (
<OrgSubTree key={child.id} node={child} />
))}
</Tree>
);
}
function OrgSubTree({ node }) {
return (
<TreeNode label={<OrgNode employee={node} />}>
{node.children?.map(child => (
<OrgSubTree key={child.id} node={child} />
))}
</TreeNode>
);
}
function OrgNode({ employee }) {
return (
<div className="inline-flex flex-col items-center bg-white border border-gray-200 rounded-lg px-3 py-2 shadow-sm min-w-32">
<span className="text-sm font-semibold">{employee.name}</span>
<span className="text-xs text-gray-500">{employee.position}</span>
</div>
);
} איך אנו עובדים על Org Chart?
| שלב | משך | מה אנו עושים |
|---|---|---|
| אנליטיקה | 1–2 ימים | לימוד מבנה החברה, מקורות נתונים (1C, SAP, CSV), הסכמה על הרכב הכרטיסים |
| עיצוב | 1–2 ימים | ציור אב טיפוס של העץ, אישור לוגיקת ההרחבה והחיפוש |
| יישום | 3–5 ימים | כתיבת קוד ב-React + d3-hierarchy, הגדרת רספונסיביות ונגישות |
| בדיקות | 1–2 ימים | בדיקת ביצועים על 5000+ צמתים, תיקון קישורים, התנהגות במובייל |
| פריסה | יום אחד | העלאה לשרת שלכם או ל-Vercel, מתן גישה לקוד המקור ותיעוד |
לוח זמנים ועלות
גרסה בסיסית (כרטיסים ניתנים ללחיצה, עיבוד עץ) — משבוע אחד. גרסה מורחבת עם חיפוש, זום/פאן, טעינה עצלה — 1.5–2 שבועות. העלות מחושבת באופן אישי לפי נפח הנתונים והאינטגרציות הנדרשות. הזמינו ייעוץ — נעריך את הפרויקט שלכם תוך יום אחד.
מה אתם מקבלים
- קוד מקור ב-GitHub עם רישיון MIT.
- REST API לניהול עובדים (CRUD).
- סביבת הדגמה עם הנתונים שלכם.
- תיעוד לפריסה והתאמה אישית.
- שעת הדרכה לצוות (Zoom או פנים אל פנים).
- אחריות על הקוד — 6 חודשים של תיקוני באגים בחינם.
איך ליישם חיפוש בעץ?
function searchInHierarchy(root: Employee, query: string): Set<string> {
const matchedIds = new Set<string>();
function traverse(node: Employee) {
const matches = node.name.toLowerCase().includes(query.toLowerCase()) || node.position.toLowerCase().includes(query.toLowerCase());
if (matches) matchedIds.add(node.id);
node.children?.forEach(traverse);
}
traverse(root);
return matchedIds;
}החיפוש פועל במקרה הגרוע ב-O(n), אך עבור 2000 צמתים זה לא מורגש. הדגשת צמתים תואמים והרחבה אוטומטית של הנתיב אליהם הופכת את הניווט לנוח. בנוסף, אנו מיישמים ניווט מקלדת ותכונות ARIA לנגישות. העץ מתאים את עצמו למסכי מובייל עם גלילה אופקית. עם תכונה זו, החברה חוסכת עד 720–1,000 דולר בשנה על חיפוש עובדים.
איך לשלב עם 1C
- הגדרת REST API ב-1C: ניהול שכר ומשאבי אנוש.
- יצירת נקודת קצה לייצוא ספריית העובדים.
- הגדרת webhook לעדכונים מצטברים.
- יישום ייבוא נתונים בפורמט JSON בצד האתר.
- הפעלת סנכרון לפי לוח זמנים.
צרו קשר כדי לדון בפרויקט שלכם. כתבו לנו — ונכין הדגמה מותאמת למבנה שלכם. קבלו ייעוץ. אנו מציעים פיתוח מותאם אישית בכל רמת מורכבות.







