Organizational Chart without using Library

Discussão em 'Angular' iniciado por Lloyd, Outubro 30, 2024.

  1. Lloyd

    Lloyd Guest

    I have working half organization chart, I am struggling with making sure connectors are nicely connecting and moving with nodes when they are expanded or the node tree is growing/grown size. I need help with the calculations for proper lines connectors.

    My idea is to utilize the calc method and but the formula i am not sure about. Image attached with broken lines is the one without the calc method changes.

    // Update CSS custom property for line calculations
    container.style.setProperty('--first-child-offset', `${firstChildCenter}px`);
    container.style.setProperty('--last-child-offset', `${lastChildCenter}px`);


    I did this changes on the DOM inspect mode first container(app-chart-node)

    --first-child-offset: calc(18% - 7px); --last-child-offset: calc(83% - 10px); second(inner children from a child). --first-child-offset: calc(30% - 7px); --last-child-offset: calc(73% - 7px);


    HTML code

    <div class="org-node-container">
    <div
    class="org-node"
    [class.expanded]="orgChartService.isNodeExpanded(node.id)"
    >
    <div class="node-content">
    <div class="node-info">
    <div class="node-name">{{ node.name }}</div>
    <div class="node-title" *ngIf="node.title">{{ node.title }}</div>
    </div>
    </div>

    <button
    *ngIf="node.children?.length"
    class="toggle-btn"
    (click)="toggleNode($event)"
    [attr.aria-expanded]="orgChartService.isNodeExpanded(node.id)"
    [attr.aria-label]="
    orgChartService.isNodeExpanded(node.id) ? 'Collapse' : 'Expand'
    "
    >
    <span class="toggle-icon"></span>
    </button>
    </div>

    <div
    *ngIf="node.children?.length && orgChartService.isNodeExpanded(node.id)"
    class="children-container"
    [class.single-child]="node.children && node.children.length === 1"
    [attr.data-children]="node.children && node.children.length"
    #childrenContainer
    >
    <claude-node
    *ngFor="
    let child of node.children;
    let first = first;
    let last = last
    "
    [node]="child"
    [level]="level + 1"
    [class.first-child]="first"
    [class.last-child]="last"
    >
    </claude-node>
    </div>
    </div>


    TS

    private updateChildrenLines() {
    const container = this.elementRef.nativeElement.querySelector('.children-container');
    if (!container || !this.node.children) return;

    // Handle single child case
    if (this.node.children.length === 1) {
    container.classList.add('single-child');
    return;
    }

    container.classList.remove('single-child');

    // Update the container's width to ensure proper line rendering
    requestAnimationFrame(() => {
    const children = container.querySelectorAll('claude-node');
    if (children.length >= 2) {
    const firstChild = children[0] as HTMLElement;
    const lastChild = children[children.length - 1] as HTMLElement;

    // Calculate the actual width needed based on the children's positions
    const containerWidth = container.offsetWidth;
    const firstChildCenter = firstChild.offsetLeft + (firstChild.offsetWidth / 2);
    const lastChildCenter = lastChild.offsetLeft + (lastChild.offsetWidth / 2);

    // Update CSS custom property for line calculations
    container.style.setProperty('--first-child-offset', `${firstChildCenter}px`);
    container.style.setProperty('--last-child-offset', `${lastChildCenter}px`);

    this.cdr.detectChanges();
    }
    });
    }


    CSS

    .org-node-container {
    --node-min-width: 200px;
    --node-padding: 16px;
    --node-margin: 24px;
    --line-color: #ccc;
    --line-thickness: 2px;
    --transition-duration: 0.3s;
    --horizontal-line-width: 0px;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    margin: 0 var(--node-margin);
    }

    /* Remove vertical line above parent */
    .org-node-container:first-child::before {
    display: none;
    }

    .org-node {
    position: relative;
    min-width: var(--node-min-width);
    margin: var(--node-margin) 10px 0 0;
    transition: all var(--transition-duration) ease;
    }

    .node-content {
    background: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: var(--node-padding);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    transition: all var(--transition-duration) ease;
    cursor: pointer;
    position: relative;
    z-index: 1;
    }

    .node-content:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    }

    .children-container {
    --first-child-offset: 0px;
    --last-child-offset: 0px;
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    position: relative;
    padding-top: var(--node-margin);
    transition: all var(--transition-duration) ease;
    /* gap: 20px; */
    column-gap: 20px;
    }

    /* Vertical line from parent to horizontal line */
    .children-container::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: var(--line-thickness);
    height: var(--node-margin);
    background: var(--line-color);
    }

    /* Horizontal line between children */
    /* .children-container:not(.single-child)::after {
    content: '';
    position: absolute;
    top: var(--node-margin);
    left: 0;
    right: 0;
    height: var(--line-thickness);
    background: var(--line-color);
    Calculate width based on first and last child position
    left: calc(var(--first-child-offset) - var(--node-margin));
    width: calc(var(--last-child-offset) - var(--first-child-offset) + (2 * var(--node-margin)));
    margin: 0 auto;
    } */
    .children-container:not(.single-child)::after {
    content: '';
    position: absolute;
    top: var(--node-margin);
    left: calc(var(--first-child-offset) - var(--node-margin));
    width: calc(var(--last-child-offset) - var(--first-child-offset) + (2 * var(--node-margin)));
    height: var(--line-thickness);
    background: var(--line-color);
    }

    .children-container claude-node {
    position: relative;
    flex: 1;
    min-width: calc(var(--node-min-width) + 2 * var(--node-margin));
    display: flex;
    flex-direction: column;
    align-items: center;
    }

    /* Vertical lines from horizontal line to children */
    .children-container claude-node::before {
    content: '';
    position: absolute;
    top: 0px;
    left: 50%;
    transform: translateX(-50%);
    width: var(--line-thickness);
    height: var(--node-margin);
    background: var(--line-color);
    }

    /* Node info styles */
    .node-info {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    }

    .node-name {
    font-weight: 600;
    color: #333;
    font-size: 1rem;
    }

    .node-title {
    font-size: 0.875rem;
    color: #666;
    }

    /* Toggle button styles */
    .toggle-btn {
    position: absolute;
    top: 55px;
    left: 53%;
    transform: translateX(-50%);
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: white;
    border: 1px solid #ddd;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    cursor: pointer;
    z-index: 2;
    transition: all var(--transition-duration) ease;
    }

    .toggle-btn:hover {
    background: #f5f5f5;
    }

    .toggle-btn .toggle-icon {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 8px;
    height: 8px;
    border: solid #666;
    border-width: 0 2px 2px 0;
    transform: translate(-50%, -50%) rotate(45deg);
    transition: transform var(--transition-duration) ease;
    }

    .org-node.expanded .toggle-icon {
    transform: translate(-50%, -25%) rotate(-135deg);
    }


    I do not want to use chart libraries, so I tried couple of CSS properties but I can't seem to find the right connections and missing the formulas to calculate and manage the lines dynamically.

    I will appreciate any help and thank you.

    For Demonstraion

    Continue reading...

Compartilhe esta Página