0

I've been trying to learn how to create a Norse Mythology family tree using D3 and was able to find some code that someone wrote on here that had the perfect layout of the Norse gods -> (found here) I really like the way that this looks, but the links between parents and children get a little muddled. Is there a way to have the color of the links have an ordinal color scale and have the links be at a right angle?

var margins = {
    top: 20,
    bottom: 300,
    left: 30,
    right: 100
};

var height = 600;
var width = 900;

var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;

var svg = d3.select('body')
    .append('svg')
    .attr('width', totalWidth)
    .attr('height', totalHeight);

var graphGroup = svg.append('g')
    .attr('transform', "translate(" + margins.left + "," + margins.top + ")");

var levels = [
    [{ id: 'Ymir' },
        {id: 'Auðumbla'}
    ],
        [
            { id: 'Fárbauti ',parents:['Ymir'], info:"pronounce like this" },
            { id: 'Laufey',parents:['Ymir']},
            { id: 'Ægir', parents:['Ymir']},
            { id: 'Rán', parents:['Ymir']},
            {id:'Búri', parents:['Auðumbla']}
        ],
        [
            { id: 'Loki', parents:['Fárbauti ', 'Laufey']},
            {id: 'A Horse'},
            { id: 'Bára', parents:['Ægir','Rán']},
            { id: 'Blóðughadda', parents:['Ægir','Rán']},
            { id: 'Bylgja', parents:['Ægir','Rán']},
            { id: 'Dúfa', parents:['Ægir','Rán']},
            { id: 'Hefring', parents:['Ægir','Rán']},
            { id: 'Himinglæva', parents:['Ægir','Rán']},
            { id: 'Angrboða', parents: ['Ymir'] },

            { id: 'Hrǫnn', parents:['Ægir','Rán']},
            { id: 'Kólga', parents:['Ægir','Rán']},
            { id: 'Unnr', parents:['Ægir','Rán']},
            {id: 'Bestla'},
            {id:'Burr', parents:['Búri']},
            {id:'Fjörgyn'},
        ],

        [
            { id: 'Fenrir', parents:['Angrboða','Loki']},
            { id:'Jörmungandr', parents:['Angrboða','Loki']},
            { id:'Hel', parents:['Angrboða','Loki']},
            { id:'Sleipnir', parents:['Loki','A Horse']},
            {id:'Jörd'},
            {id: 'Heimdall', parents:['Bára','Blóðughadda','Bylgja','Dúfa','Hefring','Himinglæva','Hrǫnn','Kólga','Unnr']},
            {id:'Hœnir', parents:['Bestla','Burr']},
            {id:'Odin', parents:['Bestla','Burr']},
            {id:'Vili', parents:['Bestla','Burr']},
            {id:'Vé', parents:['Bestla','Burr']},
            {id:'Frigg', parents:['Fjörgyn']}

        ],

        [
            {id: 'Njörds sister'},
            {id:'Njörd'},
            {id:'Járnsaxa'},
            {id:'Sif'},
            {id:'Thor', parents:['Odin','Jörd']},
            {id:'Höðr', parents:['Odin', 'Frigg']},
            {id:'Bragi', parents:['Odin']},
            {id:'Baldur',parents:['Odin','Frigg']},
            {id:'Nanna'}
        ],
        [
            {id:'Freyr', parents:['Njörd', 'Njörds sister']},
            {id:'Freya', parents:['Njörd', 'Njörds sister']},
            {id:'Magni', parents:['Thor','Járnsaxa']},
            {id:'Thrúd', parents:['Thor','Sif']},
            {id:'Módi', parents:['Thor','Sif']},
            {id:'Ullr', parents:['Sif','Odin']},
            {id:'Forseti', parents:['Baldur', 'Nanna']}


        ]

]

// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));

var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);

// objectification
nodes.forEach(d => {
    d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})

// precompute bundles
levels.forEach((l, i) => {
    var index = {}
    l.forEach(n => {
        if (n.parents.length == 0) {
            return
        }

        var id = n.parents.map(d => d.id).sort().join('--')
        if (id in index) {
            index[id].parents = index[id].parents.concat(n.parents)
        } else {
            index[id] = {
                id: id,
                parents: n.parents.slice(),
                level: i
            }
        }
        n.bundle = index[id]
    })
    l.bundles = Object.keys(index).map(k => index[k])
    l.bundles.forEach((b, i) => b.i = i)
})

var links = []
nodes.forEach(d => {
    d.parents.forEach(p => links.push({
        source: d,
        bundle: d.bundle,
        target: p
    }))
})

var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])

// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
    if (p.bundles_index === undefined) {
        p.bundles_index = {}
    }
    if (!(b.id in p.bundles_index)) {
        p.bundles_index[b.id] = []
    }
    p.bundles_index[b.id].push(b)
}))

nodes.forEach(n => {
    if (n.bundles_index !== undefined) {
        n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
    } else {
        n.bundles_index = {}
        n.bundles = []
    }
    n.bundles.forEach((b, i) => b.i = i)
})

links.forEach(l => {
    if (l.bundle.links === undefined) {
        l.bundle.links = []
    }
    l.bundle.links.push(l)
})

// layout
const padding = 8
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16

nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)

var x_offset = padding
var y_offset = padding
levels.forEach(l => {
    x_offset += l.bundles.length * bundle_width
    y_offset += level_y_padding
    l.forEach((n, i) => {
        n.x = n.level * node_width + x_offset
        n.y = node_height + y_offset + n.height / 2

        y_offset += node_height + n.height
    })
})

var i = 0
levels.forEach(l => {
    l.bundles.forEach(b => {
        b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
        b.y = i * node_height
    })
    i += l.length
})

links.forEach(l => {
    l.xt = l.target.x
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
    l.xb = l.bundle.x
    l.xs = l.source.x
    l.ys = l.source.y
})

// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
    y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
    l.forEach(n => n.y -= y_negative_offset)
})

// very ugly, I know
links.forEach(l => {
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
    l.ys = l.source.y
    l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
    l.c2 = c
})

const cluster = d3.cluster()
    .size([width, height]);

const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);

linkks.forEach((linkk) => {
    let nodeG1 = svg.append("g")
        .selectAll("circle")
        .data(linkk)
        .join("circle")
        .attr("cx", d => d.target.x)
        .attr("cy", d => d.target.y)
        .attr("fill", "none")
        .attr("stroke", (d) => {
            return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
        })
        .attr("r", 6);
    let nodeG11 = svg.append("g")
        .selectAll("circle")
        .data(linkk)
        .join("circle")
        .attr("cx", d => d.source.x)
        .attr("cy", d => d.source.y)
        .attr("fill", "none")
        .attr("stroke", (d) => {
            return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
        })
        .attr("r", 6);


    let nodeG2 = svg.append("g")
        .attr("font-family", "sans-serif")
        .attr("font-size", 14)
        .selectAll("text")
        .data(linkk)
        .join("text")
        .attr("class", "text")
        .attr("x", d => d.target.x + padding)
        .attr("y", d => d.target.y)
        .text(d => d.target.id )
        .attr("fill", (d) => {
            return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 2)))).toString(16);
        });

    let nodeG22 = svg.append("g")
        .attr("font-family", "sans-serif")
        .attr("font-size", 14)
        .selectAll("text")
        .data(linkk)
        .join("text")
        .attr("class", "text")
        .attr("x", d => d.source.x + padding)
        .attr("y", d => d.source.y)
        .text(d => d.source.id )
        .attr("fill", (d) => {
            return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
        });

    let nodeG = svg.append('g')
        .attr('class', 'node')
        .selectAll("path")
        .data(linkk)
        .join('path')
        .attr("class", "link")
        .attr("d", d3.linkHorizontal()
            .source(d => [d.xs, d.ys])
            .target(d => [d.xt, d.yt]))
        .attr("fill", "none")
        .attr("stroke-opacity", 0.325)
        .attr("stroke-width", 0.75)
        .attr("stroke", (d) => {
            return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
        });
});
path {
    display: block;
    z-index: 0;
}

text,
circle {
    display: block;
    z-index: 1000;
}
<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="main.css"/>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>

<body>
<div id ="Norse"></div>
<!-- Version 7 is the latest version of D3. -->
<script src="https://d3js.org/d3.v5.min.js"></script>
        <!-- Our visualization data will be in this '.js' file -->
<script src="main_viz.js"></script>
</div>

</body>
</html>

This is the example that I'm basing the preferred design off of: enter image description here

noobie
  • 25
  • 4
  • Hi, can you help with your code? Im getting lines at top http://jsfiddle.net/Risay/cd61vnfg – Risa Nov 20 '21 at 11:57
  • @Risa The order of the parents and the children in the code is directly related to how it is displayed. I would try moving the children of "2" around and seeing how changing the location of the children affects the tree. Also, is there any reason why you have the name of the nodes as numbers different from the ids? For me personally, it was a little confusing to see which children had parent name-"2" and not id-2. – noobie Nov 22 '21 at 17:33

0 Answers0