├── README.md ├── formatData.js ├── index.html └── upset.js /README.md: -------------------------------------------------------------------------------- 1 | # d3 UpSet Plot 2 | 3 | A function that creates an UpSet plot in d3. An UpSet plot is a substitute for a Venn diagrams - you can easily view relationships between multiple sets. 4 | 5 | Takes in: 6 | ```javascript 7 | const data = [{ 8 | "name": "name of set", 9 | "values": ["A", "B"] 10 | }] 11 | ``` 12 | 13 | It computes each relationship recursively, and sorts decreasing. Tooltips appear when hovering over the bars. 14 | 15 | You can either choose to include solo sets with all its data, with the function insertSoloDataAll, or include solo sets with only the values that ARE NOT in other sets with the function insertSoloDataOutersect. You should probably comment out the function you don't want to use. Alternatively, you can comment out both functions, to not include any of the solo sets. 16 | 17 | Demo at http://bl.ocks.org/chuntul/f211d4c0ffa12cbadfb601e230341721, where the data given is: 18 | ```javascript 19 | const data = [ 20 | { 21 | "name": "set1", 22 | "values": ["a","b","c","d"] 23 | }, 24 | { 25 | "name": "set2", 26 | "values": ["a","b","c","d", "e", "f"] 27 | }, 28 | { 29 | "name": "set3", 30 | "values": ["a","b","g", "h", "i"] 31 | }, 32 | { 33 | "name": "set4", 34 | "values": ["a","i", "j","c","d"] 35 | } 36 | ]; 37 | ``` 38 | -------------------------------------------------------------------------------- /formatData.js: -------------------------------------------------------------------------------- 1 | // format intersection data 2 | const formatIntersectionData = (data) => { 3 | // compiling solo set data - how many values per set 4 | const soloSets = []; 5 | 6 | // nameStr is for the setName, which makes it easy to compile 7 | // each name would be A, then B, so on.. 8 | const nameStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.substr(0, data.length); 9 | data.forEach((x, i) => { 10 | soloSets.push({ 11 | name: x.name, 12 | setName: nameStr.substr(i, 1), 13 | num: x.values.length, 14 | values: x.values, 15 | }); 16 | }); 17 | 18 | // compiling list of intersection names recursively 19 | // ["A", "AB", "ABC", ...] 20 | const getIntNames = (start, end, nameStr) => { 21 | // eg. BCD 22 | const name = nameStr.substring(start, end); 23 | 24 | // when reaching the last letter 25 | if (name.length === 1) { 26 | return [name]; 27 | } 28 | const retArr = getIntNames(start + 1, end, nameStr); 29 | 30 | // eg. for name = BCD, would return [B] + [BC,BCD,BD] + [C,CD,D] 31 | return [name[0]].concat(retArr.map((x) => name[0] + x), retArr); 32 | }; 33 | 34 | let intNames = getIntNames(0, nameStr.length, nameStr); 35 | 36 | // removing solo names 37 | intNames = intNames.filter((x) => x.length !== 1); 38 | 39 | let intersections = []; 40 | 41 | // compile intersections of values for each intersection name 42 | intNames.forEach((intName) => { 43 | // collecting all values: [pub1arr, pub2arr, ...] 44 | const values = intName.split('').map((set) => soloSets.find((x) => x.setName === set).values); 45 | 46 | // getting intersection 47 | // https://stackoverflow.com/questions/37320296/how-to-calculate-intersection-of-multiple-arrays-in-javascript-and-what-does-e 48 | const result = values.reduce((a, b) => a.filter((c) => b.includes(c))); 49 | intersections.push({ 50 | name: intName.split('').map((set) => soloSets.find((x) => x.setName === set).name).join(' + '), 51 | setName: intName, 52 | num: result.length, 53 | values: result, 54 | }); 55 | }); 56 | 57 | // taking out all 0s 58 | intersections = intersections.filter((x) => x.value !== 0); 59 | return { intersections, soloSets }; 60 | }; 61 | 62 | // include solo sets with all its data 63 | const insertSoloDataAll = (intersections, soloSets) => { 64 | soloSets.forEach(x => { 65 | intersections.push(x); 66 | }); 67 | return intersections; 68 | }; 69 | 70 | // include solo sets with only the values that ARE NOT in other sets 71 | const insertSoloDataOutersect = (intersections, soloSets) => { 72 | soloSets.forEach(x => { 73 | // compile all unique values from other sets except current set 74 | const otherSets = [...new Set(soloSets.map(y => y.setName === x.setName ? [] : y.values).flat())]; 75 | 76 | // subtract otherSets values from current set values 77 | const values = x.values.filter(y => !otherSets.includes(y)); 78 | intersections.push({ 79 | name: x.name, 80 | setName: x.setName, 81 | num: values.length, 82 | values: values, 83 | }) 84 | 85 | }) 86 | return intersections; 87 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /upset.js: -------------------------------------------------------------------------------- 1 | const plotUpset = (data, soloSets, plotId) => { 2 | // all sets 3 | const allSetNames = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.substr(0, soloSets.length).split(''); 4 | 5 | // position and dimensions 6 | const margin = { 7 | top: 20, 8 | right: 0, 9 | bottom: 300, 10 | left: 150, 11 | }; 12 | const width = 40 * data.length; 13 | const height = 400; 14 | 15 | // make the canvas 16 | const svg = d3.select(`#${plotId}`) 17 | .append('svg') 18 | .attr('width', width + margin.left + margin.right) 19 | .attr('height', height + margin.top + margin.bottom) 20 | .attr('xmlns', 'http://www.w3.org/2000/svg') 21 | .attr('xmlns:xlink', 'http://www.w3.org/1999/xlink') 22 | .attr('class','plot') 23 | .append('g') 24 | .attr('transform', 25 | `translate(${margin.left},${margin.top})`) 26 | .attr('fill', 'white'); 27 | 28 | // make a group for the upset circle intersection things 29 | const upsetCircles = svg.append('g') 30 | .attr('id', 'upsetCircles') 31 | .attr('transform', `translate(20,${height + 40})`); 32 | 33 | const rad = 13; 34 | 35 | // making dataset labels 36 | soloSets.forEach((x, i) => { 37 | upsetCircles.append('text') 38 | .attr('dx', -30) 39 | .attr('dy', 5 + i * (rad * 2.7)) 40 | .attr('text-anchor', 'end') 41 | .attr('fill', 'black') 42 | .style('font-size', 15) 43 | .text(x.name); 44 | }); 45 | 46 | // sort data decreasing 47 | data.sort((a, b) => parseFloat(b.num) - parseFloat(a.num)); 48 | 49 | // make the bars 50 | const upsetBars = svg.append('g') 51 | .attr('id', 'upsetBars'); 52 | 53 | const nums = data.map((x) => x.num); 54 | 55 | // set range for data by domain, and scale by range 56 | const xrange = d3.scaleLinear() 57 | .domain([0, nums.length]) 58 | .range([0, width]); 59 | 60 | const yrange = d3.scaleLinear() 61 | .domain([0, d3.max(nums)]) 62 | .range([height, 0]); 63 | 64 | // set axes for graph 65 | const xAxis = d3.axisBottom() 66 | .scale(xrange) 67 | .tickPadding(2) 68 | .tickFormat((d, i) => data[i].setName) 69 | .tickValues(d3.range(data.length)); 70 | 71 | const yAxis = d3.axisLeft() 72 | .scale(yrange) 73 | .tickSize(5); 74 | 75 | // add X axis 76 | upsetBars.append('g') 77 | .attr('class', 'x axis') 78 | .attr('transform', `translate(0,${height})`) 79 | .attr('fill', 'none') 80 | .attr('stroke', 'black') 81 | .attr('stroke-width', 1) 82 | .call(xAxis) 83 | .selectAll('.tick') 84 | .remove(); 85 | 86 | 87 | // Add the Y Axis 88 | upsetBars.append('g') 89 | .attr('class', 'y axis') 90 | .attr('fill', 'none') 91 | .attr('stroke', 'black') 92 | .attr('stroke-width', 1) 93 | .call(yAxis) 94 | .selectAll('text') 95 | .attr('fill', 'black') 96 | .attr('stroke', 'none'); 97 | 98 | 99 | const chart = upsetBars.append('g') 100 | .attr('transform', 'translate(1,0)') 101 | .attr('id', 'chart'); 102 | 103 | // adding each bar 104 | const bars = chart.selectAll('.bar') 105 | .data(data) 106 | .enter() 107 | .append('rect') 108 | .attr('class', 'bar') 109 | .attr('width', 20) 110 | .attr('x', (d, i) => 9 + i * (rad * 2.7)) 111 | .attr('y', (d) => yrange(d.num)) 112 | .style('fill', '#02577b') 113 | .attr('height', (d) => height - yrange(d.num)); 114 | 115 | // circles 116 | data.forEach((x, i) => { 117 | allSetNames.forEach((y, j) => { 118 | upsetCircles.append('circle') 119 | .attr('cx', i * (rad * 2.7)) 120 | .attr('cy', j * (rad * 2.7)) 121 | .attr('r', rad) 122 | .attr('class', `set-${x.setName}`) 123 | .style('opacity', 1) 124 | .attr('fill', () => { 125 | if (x.setName.indexOf(y) !== -1) { 126 | return '#02577b'; 127 | } 128 | return 'silver'; 129 | }); 130 | }); 131 | 132 | upsetCircles.append('line') 133 | .attr('id', `setline${i}`) 134 | .attr('x1', i * (rad * 2.7)) 135 | .attr('y1', allSetNames.indexOf(x.setName[0]) * (rad * 2.7)) 136 | .attr('x2', i * (rad * 2.7)) 137 | .attr('y2', allSetNames.indexOf(x.setName[x.setName.length - 1]) * (rad * 2.7)) 138 | .style('stroke', '#02577b') 139 | .attr('stroke-width', 4); 140 | }); 141 | 142 | // tooltip 143 | const tooltip = d3.select(`#${plotId}`) 144 | .append('div') 145 | .style('position', 'absolute') 146 | .style('z-index', '10') 147 | .style('visibility', 'hidden') 148 | .style('color', 'white') 149 | .style('padding', '0px 10px') 150 | .style('background', '#02577b') 151 | .style('border-radius', '12px') 152 | .text('hehe'); // it changes, don't worry 153 | 154 | bars.on('mouseover', (d) => { 155 | tooltip.text(`${d.name}: ${d.num} value${d.num === 1 ? '' : 's'}`).style('visibility', 'visible'); 156 | }) 157 | .on('mousemove', () => { 158 | tooltip.style('top', `${d3.event.pageY - 20}px`).style('left', `${d3.event.pageX + 20}px`); 159 | }) 160 | .on('mouseout', () => { 161 | tooltip.style('visibility', 'hidden'); 162 | }); 163 | }; --------------------------------------------------------------------------------