|
| 1 | + <div class="view-controls"> |
| 2 | + <button type="button" id="aggregation-settings-toggle" class="btn btn-light btn-sm" title="Toggle Aggregation Settings"> |
| 3 | + <span class="oi oi-cog"></span> |
| 4 | + </button> |
| 5 | + <button type="button" class="btn btn-light btn-sm" data-toggle="modal" data-target="#aggregation-export-modal" title="Export Aggregation data"> |
| 6 | + <span class="oi oi-data-transfer-download"></span> |
| 7 | + </button> |
| 8 | + </div> |
| 9 | + |
| 10 | + <div id="aggregation" class="table-sm"></div> |
| 11 | + |
| 12 | + <div id="aggregation-settings-pane" class="left-pane"> |
| 13 | + <ul class="nav nav-tabs" role="tablist"> |
| 14 | + <li class="nav-item active"> |
| 15 | + <a href="#aggregation-settings" id="aggregation-tab" class="nav-link active" aria-controls="aggregation" role="tab" data-toggle="tab">Aggregation</a> |
| 16 | + </li> |
| 17 | + </ul> |
| 18 | + <div class="tab-content"> |
| 19 | + <div class="tab-pane fade show active" role="tabpanel" aria-labelledby="aggregation-tab"> |
| 20 | + <div class="form-group row" title="What data would you like to aggregate?"> |
| 21 | + <div class="col-4">Data</div> |
| 22 | + <div class="col-8"> |
| 23 | + <div class="btn-group btn-group-toggle btn-group-sm w-100" data-toggle="buttons"> |
| 24 | + <label class="btn btn-light col active"> |
| 25 | + <input type="radio" name="aggregate-dataset" data-value="node" autocomplete="off" checked> Nodes |
| 26 | + </label> |
| 27 | + <label class="btn btn-light col"> |
| 28 | + <input type="radio" name="aggregate-dataset" data-value="link" autocomplete="off"> Links |
| 29 | + </label> |
| 30 | + <label class="btn btn-light"> |
| 31 | + <input type="radio" name="aggregate-dataset" data-value="cluster" autocomplete="off"> Clusters |
| 32 | + </label> |
| 33 | + </div> |
| 34 | + </div> |
| 35 | + </div> |
| 36 | + <div class="form-group row" title="What data field would you like to aggregate on?"> |
| 37 | + <div class="col-4">Variable</div> |
| 38 | + <div class="col-8"> |
| 39 | + <select id="aggregate-variable" class="custom-select custom-select-sm"></select> |
| 40 | + </div> |
| 41 | + </div> |
| 42 | + <div class="form-group row" title="Would you like to group variables that are close together?"> |
| 43 | + <div class="col-4">Binning</div> |
| 44 | + <div class="col-8"> |
| 45 | + <div class="btn-group btn-group-toggle btn-group-sm w-100" data-toggle="buttons"> |
| 46 | + <label class="btn btn-light col active"> |
| 47 | + <input type="radio" name="aggregate-binning" data-value="off" autocomplete="off" checked> Unbinned |
| 48 | + </label> |
| 49 | + <label class="btn btn-light col"> |
| 50 | + <input type="radio" name="aggregate-binning" data-value="on" autocomplete="off"> Binned |
| 51 | + </label> |
| 52 | + </div> |
| 53 | + </div> |
| 54 | + </div> |
| 55 | + <div class="form-group row" id="aggregate-bins-row" title="How many bins would you like to sort that data into?"> |
| 56 | + <div class="col-4">Number of Bins</div> |
| 57 | + <div class="col-8"> |
| 58 | + <input id="aggregate-bins" class="custom-select custom-select-sm" type="number" min="1" value="10" step="1"></input> |
| 59 | + </div> |
| 60 | + </div> |
| 61 | + </div> |
| 62 | + </div> |
| 63 | + </div> |
| 64 | + |
| 65 | + <div id="aggregation-export-modal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="false"> |
| 66 | + <div class="modal-dialog" role="document"> |
| 67 | + <div class="modal-content"> |
| 68 | + <div class="modal-header"> |
| 69 | + <h5 class="modal-title">Export Aggregation</h5> |
| 70 | + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |
| 71 | + <span aria-hidden="true">×</span> |
| 72 | + </button> |
| 73 | + </div> |
| 74 | + <div class="modal-body"> |
| 75 | + <div class="form-group row"> |
| 76 | + <div class="col-9"> |
| 77 | + <input type="text" id="export-aggregation-file-name" class="form-control form-control-sm" placeholder="Filename" /> |
| 78 | + </div> |
| 79 | + <div class="col-3"> |
| 80 | + <select id="export-aggregation-file-type" class="form-control form-control-sm"> |
| 81 | + <option selected>csv</option> |
| 82 | + <option>xlsx</option> |
| 83 | + <option>json</option> |
| 84 | + </select> |
| 85 | + </div> |
| 86 | + </div> |
| 87 | + </div> |
| 88 | + <div class="modal-footer"> |
| 89 | + <button type="button" class="btn btn-error" data-dismiss="modal">Cancel</button> |
| 90 | + <button type="button" id="aggregation-export" class="btn btn-primary" data-dismiss="modal">Export</button> |
| 91 | + </div> |
| 92 | + </div><!-- /.modal-content --> |
| 93 | + </div><!-- /.modal-dialog --> |
| 94 | + </div><!-- /.modal --> |
| 95 | + |
| 96 | + <script> |
| 97 | + (function(){ |
| 98 | + |
| 99 | + let data = []; |
| 100 | + |
| 101 | + let aggregation = new Tabulator("#aggregation", { |
| 102 | + height: "calc(100% - 60px) !important", |
| 103 | + layout: "fitColumns" |
| 104 | + }); |
| 105 | + |
| 106 | + function updateColumns(){ |
| 107 | + let dataset = $('input[name=aggregate-dataset]:checked').data('value'); |
| 108 | + $('#aggregate-variable').html( |
| 109 | + session.data[dataset + 'Fields'] |
| 110 | + .map(l => `<option value="${l}">${app.titleize(l)}</option>`) |
| 111 | + .join('\n') |
| 112 | + ).val(dataset === 'cluster' ? 'ID' : 'cluster'); |
| 113 | + } |
| 114 | + |
| 115 | + function updateTable(){ |
| 116 | + let dataset = $('input[name=aggregate-dataset]:checked').data('value'); |
| 117 | + let column = $('#aggregate-variable').val(); |
| 118 | + let binned = $('[name="aggregate-binning"]:checked').data('value') === 'on'; |
| 119 | + let values = []; |
| 120 | + data = []; |
| 121 | + let rawdata = app['getVisible' + dataset[0].toUpperCase() + dataset.slice(1) + 's'](); |
| 122 | + let n = rawdata.length; |
| 123 | + if(binned){ |
| 124 | + let values = rawdata.map(r => r[column]).sort((a, b) => a - b); |
| 125 | + let perBin = Math.ceil(n/$('#aggregate-bins').val()); |
| 126 | + values.forEach((row, i) => { |
| 127 | + let bin = data[Math.floor(i/perBin)]; |
| 128 | + if(bin){ |
| 129 | + bin.n++; |
| 130 | + bin.max = row; |
| 131 | + bin[column] = bin.min + '-' + bin.max; |
| 132 | + } else { |
| 133 | + data.push({ |
| 134 | + n: 1, |
| 135 | + min: row, |
| 136 | + max: row |
| 137 | + }); |
| 138 | + } |
| 139 | + }); |
| 140 | + console.log(data); |
| 141 | + } else { |
| 142 | + rawdata.forEach(row => { |
| 143 | + let val = row[column]; |
| 144 | + if(values.includes(val)){ |
| 145 | + data[values.indexOf(val)].n++; |
| 146 | + } else { |
| 147 | + values.push(val); |
| 148 | + let newRow = {}; |
| 149 | + newRow[column] = val; |
| 150 | + newRow.n = 1; |
| 151 | + data.push(newRow); |
| 152 | + } |
| 153 | + }); |
| 154 | + } |
| 155 | + data.forEach(row => row.percent = (row.n/n*100).toLocaleString() + '%'); |
| 156 | + aggregation.setColumns([ |
| 157 | + {title: app.titleize(dataset + ' ' + column), field: column}, |
| 158 | + {title: 'Number of ' + app.titleize(dataset) + 's', field: 'n'}, |
| 159 | + {title: 'Percentage', field: 'percent', headerSort: false} |
| 160 | + ]); |
| 161 | + aggregation.setData(data); |
| 162 | + if(!binned){ |
| 163 | + aggregation.setSort([ |
| 164 | + {column: 'n', dir: 'desc'}, |
| 165 | + {column: column, dir: 'asc'} |
| 166 | + ]); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + $('#aggregation-settings-toggle').click(function(){ |
| 171 | + let pane = $('#aggregation-settings-pane'); |
| 172 | + if(pane.is(':visible')){ |
| 173 | + pane.animate({left: '-400px'}, function(){ pane.hide(); }); |
| 174 | + } else { |
| 175 | + pane.show(0, function(){ pane.animate({left: '0px'}); }); |
| 176 | + } |
| 177 | + }); |
| 178 | + |
| 179 | + $('input[name="aggregate-dataset"]').on('change', e => { |
| 180 | + updateColumns(); |
| 181 | + updateTable(); |
| 182 | + }); |
| 183 | + |
| 184 | + $('#aggregate-variable').on('change', updateTable); |
| 185 | + |
| 186 | + $('[name="aggregate-binning"]').on('change', function(){ |
| 187 | + let val = $(this).data('value'); |
| 188 | + if(val == 'on'){ |
| 189 | + $('#aggregate-bins-row').css('display', 'flex'); |
| 190 | + } else { |
| 191 | + $('#aggregate-bins-row').slideUp(); |
| 192 | + } |
| 193 | + updateTable(); |
| 194 | + }); |
| 195 | + |
| 196 | + $('#aggregate-bins').on('change', updateTable); |
| 197 | + |
| 198 | + $('#aggregation-export').click(function(){ |
| 199 | + let format = $('#export-aggregation-file-type').val(); |
| 200 | + let name = $('#export-aggregation-file-name').val(); |
| 201 | + if(format === 'csv'){ |
| 202 | + let blob = new Blob([Papa.unparse(data)], {type: 'text/csv;charset=utf-8'}); |
| 203 | + saveAs(blob, name + '.' + format); |
| 204 | + } else if(format === 'xlsx'){ |
| 205 | + let wb = XLSX.utils.book_new(); |
| 206 | + let ws = XLSX.utils.json_to_sheet(data); |
| 207 | + XLSX.utils.book_append_sheet(wb, ws, name); |
| 208 | + XLSX.writeFile(wb, name + '.' + format); |
| 209 | + } else { |
| 210 | + let blob = new Blob([JSON.stringify(data)], {type: 'application/json;charset=utf-8'}); |
| 211 | + saveAs(blob, name + '.' + format); |
| 212 | + } |
| 213 | + }); |
| 214 | + |
| 215 | + $('#aggregation').parent().css('z-index', 1000); |
| 216 | + |
| 217 | + $(window) |
| 218 | + .on('link-threshold-change', updateTable); |
| 219 | + |
| 220 | + updateColumns(); |
| 221 | + updateTable(); |
| 222 | + })(); |
| 223 | + </script> |
0 commit comments