Output list of genes from chorddiag interactive diagram

33 Views Asked by At

we're currently exploring chorddiag in our lab to better display large proteomic and CRISPR screen datasets. Initial display of the diagram seems to work great. However, we have something of a basic question: is there any function built in to chorddiag which will allow a user who is interacting with the diagram to click a given chord and output a list or CSV of the resulting comparison? i.e. if one were to click the chord connecting Dataset A and Dataset B, it would give an output of overlapping values? We have looked through the chorddiag documentation but haven't seen anything obvious. If it's something we'll have to build on our own, that's fine - we just want to be sure before we accidentally reinvent the wheel.

Cheers!

Here is the diagram as it stands. We are interested in identifying some method whereby we click the highlighted chord and see the precise list of overlapping items that is represented by that chord (i.e. overlapping items between Dataset A and Dataset B).

enter image description here

1

There are 1 best solutions below

1
Mr. Polywhirl On

You could add a click handler to each of your chords (aka "ribbons"):

const matrixToCsv = (matrix, delimiter = ',') =>
  matrix.map(row => row.join(delimiter)).join('\n');

.on("click", function(d) {
  const { source, target } = d;
  const csv = matrixToCsv([
   'source,target,value'.split(','),
    [office[source.index], office[target.index], target.value],  
    [office[target.index], office[source.index], source.value]])
  console.log(csv);
});

Upon clicking on a chord, the CSV data for source/target/value will be printed to the console.

Full example

Below is a modified version of a complete Chord diagram. I added the click handler (as seen above) to the chords (aka "ribbons") that span between groups.

// Source: https://stackoverflow.com/a/43778409/1762224
function fade(opacity) {
  return function(d, i) {
    ribbons
      .filter(function(d) {
        return d.source.index != i && d.target.index != i;
      })
      .transition()
      .style("opacity", opacity);
  };
}
/********************************/
var matrix = [
  [11975,  5871, 8916, 2868],
  [ 1951, 10048, 2060, 6171],
  [ 8010, 16145, 8090, 8045],
  [ 1013,   990,  940, 6907]
];

var office = ["A", "B", "C", "D"]

//Initialize canvas and inner/outer radii of the chords
var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height"),
  outerRadius = Math.min(width, height) * 0.5 - 65,
  innerRadius = outerRadius - 20;

//Set number format to show $M for millions with thousands separator (i.e. $1,000M). 
var formatValue = d3.formatPrefix("$,.0", 1e3);

// Initialize chord diagram
var chord = d3.chord()
  .padAngle(0.05)
  .sortSubgroups(d3.descending);

// Set Arc Raddii
var arc = d3.arc()
  .innerRadius(innerRadius)
  .outerRadius(outerRadius);

// Set Ribbbons
var ribbon = d3.ribbon()
  .radius(innerRadius);

// Initialize colors to an ordinal scheme with 10 categories
var color = d3.scaleOrdinal(d3.schemeCategory10);

// Center origin
var g = svg.append("g")
  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
  .datum(chord(matrix));

// Defines each "group" in the chord diagram
var group = g.append("g")
  .attr("class", "groups")
  .selectAll("g")
  .data(function(chords) {
    return chords.groups;
  })
  .enter().append("g")


// Draw the radial arcs for each group
group.append("path")
  .style("fill", function(d) {
    return color(d.index);
  })
  .style("stroke", function(d) {
    return d3.rgb(color(d.index)).darker();
  })
  .attr("d", arc)
  .on("mouseover", fade(.1))
  .on("mouseout", fade(1))

group.append("title").text(function(d) {
  return groupTip(d);
});

// Add labels to each group
group.append("text")
  .attr("dy", ".35em") // Width
  .attr("class", "office-label")
  .attr("transform", function(d, i) { // Angle
    d.angle = (d.startAngle + d.endAngle) / 2; // Calculate the average of the start angle and the end angle
    d.name = office[i]; // Assignment for the city
    return "rotate(" + (d.angle * 180 / Math.PI) + ")" +
      "translate(0," + -1.1 * (outerRadius + 30) + ")" +
      ((d.angle > Math.PI * 3 / 4 && d.angle < Math.PI * 5 / 4) ? "rotate(180)" : "");
  }) // To spin when the angle between 135 to 225 degrees
  .text(function(d) {
    return d.name;
  });

const matrixToCsv = (matrix, delimiter = ',') =>
  matrix.map(row => row.join(delimiter)).join('\n');

// Draw the ribbons that go from group to group
var ribbons = g.append("g")
  .attr("class", "ribbons")
  .selectAll("path")
  .data(function(chords) {
    return chords;
  })
  .enter().append("path")
  .attr("d", ribbon)
  .style("fill", function(d) {
    return color(d.target.index);
  })
  .style("stroke", function(d) {
    return d3.rgb(color(d.target.index)).darker();
  })
  .on("click", function(d) {
    const { source, target } = d;
    const csv = matrixToCsv([
      'source,target,value'.split(','),
      [office[source.index], office[target.index], target.value],
      [office[target.index], office[source.index], source.value]
    ])
    console.log(csv);
  });

ribbons.append("title").
text(function(d) {
  return chordTip(d);
});


// Define tick marks to run along each arc
var groupTick = group.selectAll(".group-tick")
  .data(function(d) {
    return groupTicks(d, 1e3);
  })
  .enter().append("g")
  .attr("class", "group-tick")
  .attr("transform", function(d) {
    return "rotate(" + (d.angle * 180 / Math.PI - 90) + ") translate(" + outerRadius + ",0)";
  });

groupTick.append("line")
  .attr("x2", 6);

groupTick
  .filter(function(d) {
    return d.value % 2e3 === 0;
  })
  .append("text")
  .attr("x", 8)
  .attr("dy", ".35em")
  .attr("transform", function(d) {
    return d.angle > Math.PI ? "rotate(180) translate(-16)" : null;
  })
  .style("text-anchor", function(d) {
    return d.angle > Math.PI ? "end" : null;
  })
  .text(function(d) {
    return formatValue(d.value);
  });

// Returns an array of tick angles and values for a given group and step.
function groupTicks(d, step) {
  var k = (d.endAngle - d.startAngle) / d.value;
  return d3.range(0, d.value, step).map(function(value) {
    return {
      value: value,
      angle: value * k + d.startAngle
    };
  });
}

function chordTip(d) {
  var p = d3.format(".2%"),
    q = d3.formatPrefix("$,.2", 1e3)
  return "Flow Info:\n" +
    office[d.source.index] + " → " + office[d.target.index] + ": " + q(d.target.value) + "\n" +
    office[d.target.index] + " → " + office[d.source.index] + ": " + q(d.source.value);
}

function groupTip(d) {
  var q = d3.formatPrefix("$,.2", 1e3)
  return "Total Managed by " + office[d.index] + ":\n" + q(d.value)
}
body {
  font: 10px sans-serif;
}

.group-tick line {
  stroke: #000;
}

.office-label {
  font-size: 25px;
  font: sans-serif;
  text-anchor: middle;
}

.ribbons {
  fill-opacity: 0.6;
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="450" height="450"></svg>