feat: integrate d3-org-chart for network visualization and add SignalR token notification service
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* d3-org-chart integration for Blazor
|
||||
* Network Binary Tree visualization for FourSat
|
||||
*/
|
||||
|
||||
window.OrgChart = {
|
||||
chart: null,
|
||||
dotNetHelper: null,
|
||||
|
||||
/**
|
||||
* Initialize the organization chart
|
||||
* @param {string} containerId - The ID of the container element
|
||||
* @param {object} data - The tree data in flat array format
|
||||
* @param {object} dotNetHelper - Blazor .NET helper for callbacks
|
||||
*/
|
||||
init: function (containerId, data, dotNetHelper) {
|
||||
this.dotNetHelper = dotNetHelper;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error('OrgChart: Container not found:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous chart
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
container.innerHTML = '<div class="no-data-message">دادهای برای نمایش وجود ندارد</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.chart = new d3.OrgChart()
|
||||
.container('#' + containerId)
|
||||
.data(data)
|
||||
.nodeWidth((d) => 130)
|
||||
.nodeHeight((d) => 56)
|
||||
.childrenMargin((d) => 50)
|
||||
.compactMarginBetween((d) => 15)
|
||||
.compactMarginPair((d) => 15)
|
||||
.neighbourMargin((a, b) => 15)
|
||||
.siblingsMargin((d) => 15)
|
||||
.buttonContent(({ node, state }) => {
|
||||
const hasChildren = node.data._directSubordinates > 0;
|
||||
const isExpanded = node.children;
|
||||
return hasChildren ? `<div class="org-expand-btn">
|
||||
<span>${isExpanded ? '−' : '+'}</span>
|
||||
</div>` : '';
|
||||
})
|
||||
.linkUpdate(function (d, i, arr) {
|
||||
d3.select(this)
|
||||
.attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#0380C0' : '#ccc')
|
||||
.attr('stroke-width', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? 3 : 2);
|
||||
})
|
||||
.nodeContent(function (d, i, arr, state) {
|
||||
const data = d.data;
|
||||
const isRoot = !data.parentId || data.parentId === '';
|
||||
const positionClass = data.position === 'Left' ? 'position-left' :
|
||||
data.position === 'Right' ? 'position-right' : 'position-root';
|
||||
const activeClass = data.isActive ? 'active' : 'inactive';
|
||||
|
||||
// Avatar - first letter of name
|
||||
const firstChar = data.fullName ? data.fullName.charAt(0) : '?';
|
||||
|
||||
return `
|
||||
<div class="org-node-card ${positionClass} ${activeClass}" data-user-id="${data.id}">
|
||||
<div class="node-header-compact ${isRoot ? 'root' : ''}">
|
||||
<div class="node-avatar-sm">${firstChar}</div>
|
||||
<div class="node-info">
|
||||
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
||||
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.onNodeClick((d) => {
|
||||
if (this.dotNetHelper) {
|
||||
// Convert string id to number for C# long
|
||||
const userId = parseInt(d.data.id, 10);
|
||||
this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId);
|
||||
}
|
||||
})
|
||||
.render();
|
||||
|
||||
// Initial centering and zoom
|
||||
this.chart.fit();
|
||||
|
||||
} catch (error) {
|
||||
console.error('OrgChart: Error initializing chart:', error);
|
||||
container.innerHTML = '<div class="error-message">خطا در بارگذاری نمودار</div>';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the chart with new data
|
||||
* @param {object} data - The new tree data
|
||||
*/
|
||||
update: function (data) {
|
||||
if (this.chart) {
|
||||
this.chart.data(data).render();
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand all nodes
|
||||
*/
|
||||
expandAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.expandAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapse all nodes
|
||||
*/
|
||||
collapseAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.collapseAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Center the chart
|
||||
*/
|
||||
center: function () {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fit entire tree to screen (zoom out completely)
|
||||
*/
|
||||
fitToScreen: function () {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Zoom to specific node
|
||||
* @param {string} nodeId - The ID of the node to zoom to
|
||||
*/
|
||||
zoomToNode: function (nodeId) {
|
||||
if (this.chart) {
|
||||
this.chart.setCentered(nodeId).render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight path to specific node
|
||||
* @param {string} nodeId - The ID of the node
|
||||
*/
|
||||
highlightNode: function (nodeId) {
|
||||
if (this.chart) {
|
||||
this.chart.setHighlighted(nodeId).render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear highlighting
|
||||
*/
|
||||
clearHighlight: function () {
|
||||
if (this.chart) {
|
||||
this.chart.clearHighlighting().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as PNG
|
||||
*/
|
||||
exportPng: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportImg({ full: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as SVG
|
||||
*/
|
||||
exportSvg: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportSvg();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispose the chart
|
||||
*/
|
||||
dispose: function () {
|
||||
if (this.chart) {
|
||||
// d3-org-chart doesn't have built-in dispose, so we just clear the reference
|
||||
this.chart = null;
|
||||
this.dotNetHelper = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert hierarchical tree data to flat array format for d3-org-chart
|
||||
* @param {object} rootNode - The root node with nested children
|
||||
* @returns {array} - Flat array of nodes
|
||||
*/
|
||||
convertToFlatArray: function (rootNode) {
|
||||
if (!rootNode) return [];
|
||||
|
||||
const result = [];
|
||||
|
||||
function traverse(node, parentId) {
|
||||
const flatNode = {
|
||||
id: node.userId?.toString() || node.id?.toString(),
|
||||
parentId: parentId,
|
||||
fullName: node.fullName || '',
|
||||
mobile: node.mobile || '',
|
||||
avatar: node.avatar,
|
||||
position: node.position || 'Root',
|
||||
level: node.level || 0,
|
||||
isActive: node.isActive !== false,
|
||||
isClubActive: node.isClubActive || false,
|
||||
activationWeekNumber: node.activationWeekNumber,
|
||||
joinedAt: node.joinedAt
|
||||
};
|
||||
|
||||
result.push(flatNode);
|
||||
|
||||
// Process left child
|
||||
if (node.leftChild) {
|
||||
traverse(node.leftChild, flatNode.id);
|
||||
}
|
||||
|
||||
// Process right child
|
||||
if (node.rightChild) {
|
||||
traverse(node.rightChild, flatNode.id);
|
||||
}
|
||||
}
|
||||
|
||||
traverse(rootNode, '');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user