| svg = d3.select 'svg' | |
| width = svg.node().getBoundingClientRect().width | |
| height = svg.node().getBoundingClientRect().height | |
| matrix_space_width = 0.8 * width | |
| matrix_space_height = 0.8 * height | |
| # Group for zoomable content | |
| zoomable_layer = svg.append('g') | |
| # Zoom behavior definition | |
| zoom = d3.behavior.zoom() | |
| .scaleExtent [0,4] | |
| .on 'zoom', () -> | |
| zoomable_layer | |
| .attr | |
| transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})" | |
| svg.call(zoom) | |
| matrix = zoomable_layer.append 'g' | |
| # DATA | |
| # ---- | |
| data_a = [ | |
| {id: 1, label: 'A'}, | |
| {id: 2, label: 'B'}, | |
| {id: 3, label: 'C'}, | |
| {id: 4, label: 'D'}, | |
| {id: 5, label: 'E'} | |
| ] | |
| data_b = [ | |
| {id: 1, label: 'W'}, | |
| {id: 2, label: 'X'}, | |
| {id: 3, label: 'Y'}, | |
| {id: 4, label: 'Z'} | |
| ] | |
| data_cells = [ | |
| {a_id: 1, b_id: 1, weight: 2}, | |
| {a_id: 1, b_id: 2, weight: 12}, | |
| {a_id: 1, b_id: 3, weight: 1}, | |
| {a_id: 1, b_id: 4, weight: 22}, | |
| {a_id: 2, b_id: 1, weight: 10}, | |
| {a_id: 2, b_id: 2, weight: 1}, | |
| {a_id: 2, b_id: 4, weight: 8}, | |
| {a_id: 3, b_id: 1, weight: 1}, | |
| {a_id: 3, b_id: 2, weight: 8}, | |
| {a_id: 3, b_id: 3, weight: 6}, | |
| {a_id: 4, b_id: 4, weight: 7}, | |
| {a_id: 5, b_id: 1, weight: 2}, | |
| {a_id: 5, b_id: 2, weight: 2}, | |
| {a_id: 5, b_id: 3, weight: 5}, | |
| {a_id: 5, b_id: 4, weight: 3} | |
| ] | |
| # objectify the matrix | |
| data_a_index = {} | |
| data_a.forEach (d) -> data_a_index[d.id] = d | |
| data_b_index = {} | |
| data_b.forEach (d) -> data_b_index[d.id] = d | |
| data_a.forEach (d) -> d.cells = [] | |
| data_b.forEach (d) -> d.cells = [] | |
| data_cells.forEach (d) -> | |
| d.a = data_a_index[d.a_id] | |
| d.b = data_b_index[d.b_id] | |
| d.a.cells.push d | |
| d.b.cells.push d | |
| # selection functions | |
| selected = | |
| as: {} | |
| bs: {} | |
| toggle = (d, abs) -> | |
| if d.id of selected[abs] | |
| delete selected[abs][d.id] | |
| else | |
| selected[abs][d.id] = d | |
| update_selection() | |
| update_selection = () -> | |
| data_a.forEach (d) -> d.selected = d.id of selected.as or Object.keys(selected.as).length is 0 | |
| data_b.forEach (d) -> d.selected = d.id of selected.bs or Object.keys(selected.bs).length is 0 | |
| data_cells.forEach (d) -> d.selected = d.a.selected and d.b.selected | |
| update_selection() | |
| # FILTER | |
| # ------ | |
| filter_active = false | |
| # VIS | |
| # --- | |
| LABEL_PAD = 8 | |
| ANIMATION_DURATION = 800 | |
| redraw = () -> | |
| if filter_active | |
| filtered_data_cells = data_cells.filter (d) -> d.selected | |
| filtered_data_a = data_a.filter (d) -> d.selected | |
| filtered_data_b = data_b.filter (d) -> d.selected | |
| else | |
| filtered_data_cells = data_cells | |
| filtered_data_a = data_a | |
| filtered_data_b = data_b | |
| # LAYOUT | |
| cell = Math.min(matrix_space_width / filtered_data_b.length, matrix_space_height / filtered_data_a.length) | |
| matrix_width = cell * filtered_data_b.length | |
| matrix_height = cell * filtered_data_a.length | |
| x = d3.scale.ordinal() | |
| .domain(filtered_data_b.map (d) -> d.id) | |
| .rangeBands([0, matrix_width]) | |
| y = d3.scale.ordinal() | |
| .domain(filtered_data_a.map (d) -> d.id) | |
| .rangeBands([0, matrix_height]) | |
| radius = d3.scale.sqrt() | |
| .domain([0, d3.max data_cells, (d) -> d.weight]) | |
| .range([0, cell/2]) | |
| # VIS | |
| matrix.transition() | |
| .delay(ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| transform: "translate(#{width/2-matrix_width/2},#{height/2-matrix_height/2})" | |
| cells = matrix.selectAll(".cell") | |
| .data filtered_data_cells, (d) -> "#{d.a.id}__#{d.b.id}" | |
| cells.enter().append("circle") | |
| .attr | |
| class: "cell" | |
| opacity: 0 | |
| cells | |
| .classed 'selected', (d) -> d.selected | |
| # ANIMATIONS | |
| cells.exit().transition() | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 0 | |
| .remove() | |
| cells.transition() | |
| .delay(ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| cx: (d) -> x(d.b.id)+cell/2 | |
| cy: (d) -> y(d.a.id)+cell/2 | |
| r: (d) -> radius(d.weight) | |
| cells.transition() | |
| .delay(2*ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 1 | |
| # labels | |
| x_labels = matrix.selectAll(".x_label") | |
| .data filtered_data_b, (d) -> d.id | |
| enter_x_labels = x_labels.enter().append("text") | |
| .text (d) -> d.label | |
| .attr | |
| class: "x_label" | |
| x: 0 | |
| y: -LABEL_PAD | |
| opacity: 0 | |
| .on 'click', (d) -> | |
| if not filter_active | |
| toggle(d,'bs') | |
| redraw() | |
| x_labels | |
| .classed 'selected', (d) -> d.selected | |
| # ANIMATIONS | |
| x_labels.exit().transition() | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 0 | |
| .remove() | |
| x_labels.transition() | |
| .delay(ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| x: (d) -> x(d.id)+cell/2 | |
| enter_x_labels.transition() | |
| .delay(2*ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 1 | |
| y_labels = matrix.selectAll(".y_label") | |
| .data filtered_data_a, (d) -> d.id | |
| enter_y_labels = y_labels.enter().append("text") | |
| .text (d) -> d.label | |
| .attr | |
| class: "y_label" | |
| x: -LABEL_PAD | |
| y: 0 | |
| dy: '0.35em' | |
| opacity: 0 | |
| .on 'click', (d) -> | |
| if not filter_active | |
| toggle(d,'as') | |
| redraw() | |
| y_labels | |
| .classed 'selected', (d) -> d.selected | |
| # ANIMATIONS | |
| y_labels.exit().transition() | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 0 | |
| .remove() | |
| y_labels.transition() | |
| .delay(ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| y: (d) -> y(d.id)+cell/2 | |
| enter_y_labels.transition() | |
| .delay(2*ANIMATION_DURATION) | |
| .duration(ANIMATION_DURATION) | |
| .attr | |
| opacity: 1 | |
| # FILTER reprise | |
| # -------------- | |
| d3.select('.filter_box > input').on 'click', () -> | |
| filter_active = not filter_active | |
| redraw() | |
| redraw() |
| html, body { | |
| padding: 0; | |
| margin: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| svg { | |
| width: 100%; | |
| height: 100%; | |
| background: white; | |
| } | |
| .x_label, .y_label { | |
| font-family: sans-serif; | |
| font-size: 24px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .x_label { | |
| text-anchor: middle; | |
| } | |
| .y_label { | |
| text-anchor: end; | |
| } | |
| .cell, .x_label, .y_label { | |
| fill: #BBB; | |
| } | |
| .selected { | |
| fill: black; | |
| } | |
| .filter_box { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| font-family: sans-serif; | |
| } |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Filtered matrix</title> | |
| <link type="text/css" href="index.css" rel="stylesheet"/> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| </head> | |
| <body> | |
| <svg></svg> | |
| <div class="filter_box"><input type="checkbox"/>Hide unselected</div> | |
| <script src="index.js"></script> | |
| </body> | |
| </html> |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| var ANIMATION_DURATION, LABEL_PAD, data_a, data_a_index, data_b, data_b_index, data_cells, filter_active, height, matrix, matrix_space_height, matrix_space_width, redraw, selected, svg, toggle, update_selection, width, zoom, zoomable_layer; | |
| svg = d3.select('svg'); | |
| width = svg.node().getBoundingClientRect().width; | |
| height = svg.node().getBoundingClientRect().height; | |
| matrix_space_width = 0.8 * width; | |
| matrix_space_height = 0.8 * height; | |
| zoomable_layer = svg.append('g'); | |
| zoom = d3.behavior.zoom().scaleExtent([0, 4]).on('zoom', function() { | |
| return zoomable_layer.attr({ | |
| transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")" | |
| }); | |
| }); | |
| svg.call(zoom); | |
| matrix = zoomable_layer.append('g'); | |
| data_a = [ | |
| { | |
| id: 1, | |
| label: 'A' | |
| }, { | |
| id: 2, | |
| label: 'B' | |
| }, { | |
| id: 3, | |
| label: 'C' | |
| }, { | |
| id: 4, | |
| label: 'D' | |
| }, { | |
| id: 5, | |
| label: 'E' | |
| } | |
| ]; | |
| data_b = [ | |
| { | |
| id: 1, | |
| label: 'W' | |
| }, { | |
| id: 2, | |
| label: 'X' | |
| }, { | |
| id: 3, | |
| label: 'Y' | |
| }, { | |
| id: 4, | |
| label: 'Z' | |
| } | |
| ]; | |
| data_cells = [ | |
| { | |
| a_id: 1, | |
| b_id: 1, | |
| weight: 2 | |
| }, { | |
| a_id: 1, | |
| b_id: 2, | |
| weight: 12 | |
| }, { | |
| a_id: 1, | |
| b_id: 3, | |
| weight: 1 | |
| }, { | |
| a_id: 1, | |
| b_id: 4, | |
| weight: 22 | |
| }, { | |
| a_id: 2, | |
| b_id: 1, | |
| weight: 10 | |
| }, { | |
| a_id: 2, | |
| b_id: 2, | |
| weight: 1 | |
| }, { | |
| a_id: 2, | |
| b_id: 4, | |
| weight: 8 | |
| }, { | |
| a_id: 3, | |
| b_id: 1, | |
| weight: 1 | |
| }, { | |
| a_id: 3, | |
| b_id: 2, | |
| weight: 8 | |
| }, { | |
| a_id: 3, | |
| b_id: 3, | |
| weight: 6 | |
| }, { | |
| a_id: 4, | |
| b_id: 4, | |
| weight: 7 | |
| }, { | |
| a_id: 5, | |
| b_id: 1, | |
| weight: 2 | |
| }, { | |
| a_id: 5, | |
| b_id: 2, | |
| weight: 2 | |
| }, { | |
| a_id: 5, | |
| b_id: 3, | |
| weight: 5 | |
| }, { | |
| a_id: 5, | |
| b_id: 4, | |
| weight: 3 | |
| } | |
| ]; | |
| data_a_index = {}; | |
| data_a.forEach(function(d) { | |
| return data_a_index[d.id] = d; | |
| }); | |
| data_b_index = {}; | |
| data_b.forEach(function(d) { | |
| return data_b_index[d.id] = d; | |
| }); | |
| data_a.forEach(function(d) { | |
| return d.cells = []; | |
| }); | |
| data_b.forEach(function(d) { | |
| return d.cells = []; | |
| }); | |
| data_cells.forEach(function(d) { | |
| d.a = data_a_index[d.a_id]; | |
| d.b = data_b_index[d.b_id]; | |
| d.a.cells.push(d); | |
| return d.b.cells.push(d); | |
| }); | |
| selected = { | |
| as: {}, | |
| bs: {} | |
| }; | |
| toggle = function(d, abs) { | |
| if (d.id in selected[abs]) { | |
| delete selected[abs][d.id]; | |
| } else { | |
| selected[abs][d.id] = d; | |
| } | |
| return update_selection(); | |
| }; | |
| update_selection = function() { | |
| data_a.forEach(function(d) { | |
| return d.selected = d.id in selected.as || Object.keys(selected.as).length === 0; | |
| }); | |
| data_b.forEach(function(d) { | |
| return d.selected = d.id in selected.bs || Object.keys(selected.bs).length === 0; | |
| }); | |
| return data_cells.forEach(function(d) { | |
| return d.selected = d.a.selected && d.b.selected; | |
| }); | |
| }; | |
| update_selection(); | |
| filter_active = false; | |
| LABEL_PAD = 8; | |
| ANIMATION_DURATION = 800; | |
| redraw = function() { | |
| var cell, cells, enter_x_labels, enter_y_labels, filtered_data_a, filtered_data_b, filtered_data_cells, matrix_height, matrix_width, radius, x, x_labels, y, y_labels; | |
| if (filter_active) { | |
| filtered_data_cells = data_cells.filter(function(d) { | |
| return d.selected; | |
| }); | |
| filtered_data_a = data_a.filter(function(d) { | |
| return d.selected; | |
| }); | |
| filtered_data_b = data_b.filter(function(d) { | |
| return d.selected; | |
| }); | |
| } else { | |
| filtered_data_cells = data_cells; | |
| filtered_data_a = data_a; | |
| filtered_data_b = data_b; | |
| } | |
| cell = Math.min(matrix_space_width / filtered_data_b.length, matrix_space_height / filtered_data_a.length); | |
| matrix_width = cell * filtered_data_b.length; | |
| matrix_height = cell * filtered_data_a.length; | |
| x = d3.scale.ordinal().domain(filtered_data_b.map(function(d) { | |
| return d.id; | |
| })).rangeBands([0, matrix_width]); | |
| y = d3.scale.ordinal().domain(filtered_data_a.map(function(d) { | |
| return d.id; | |
| })).rangeBands([0, matrix_height]); | |
| radius = d3.scale.sqrt().domain([ | |
| 0, d3.max(data_cells, function(d) { | |
| return d.weight; | |
| }) | |
| ]).range([0, cell / 2]); | |
| matrix.transition().delay(ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| transform: "translate(" + (width / 2 - matrix_width / 2) + "," + (height / 2 - matrix_height / 2) + ")" | |
| }); | |
| cells = matrix.selectAll(".cell").data(filtered_data_cells, function(d) { | |
| return d.a.id + "__" + d.b.id; | |
| }); | |
| cells.enter().append("circle").attr({ | |
| "class": "cell", | |
| opacity: 0 | |
| }); | |
| cells.classed('selected', function(d) { | |
| return d.selected; | |
| }); | |
| cells.exit().transition().duration(ANIMATION_DURATION).attr({ | |
| opacity: 0 | |
| }).remove(); | |
| cells.transition().delay(ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| cx: function(d) { | |
| return x(d.b.id) + cell / 2; | |
| }, | |
| cy: function(d) { | |
| return y(d.a.id) + cell / 2; | |
| }, | |
| r: function(d) { | |
| return radius(d.weight); | |
| } | |
| }); | |
| cells.transition().delay(2 * ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| opacity: 1 | |
| }); | |
| x_labels = matrix.selectAll(".x_label").data(filtered_data_b, function(d) { | |
| return d.id; | |
| }); | |
| enter_x_labels = x_labels.enter().append("text").text(function(d) { | |
| return d.label; | |
| }).attr({ | |
| "class": "x_label", | |
| x: 0, | |
| y: -LABEL_PAD, | |
| opacity: 0 | |
| }).on('click', function(d) { | |
| if (!filter_active) { | |
| toggle(d, 'bs'); | |
| return redraw(); | |
| } | |
| }); | |
| x_labels.classed('selected', function(d) { | |
| return d.selected; | |
| }); | |
| x_labels.exit().transition().duration(ANIMATION_DURATION).attr({ | |
| opacity: 0 | |
| }).remove(); | |
| x_labels.transition().delay(ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| x: function(d) { | |
| return x(d.id) + cell / 2; | |
| } | |
| }); | |
| enter_x_labels.transition().delay(2 * ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| opacity: 1 | |
| }); | |
| y_labels = matrix.selectAll(".y_label").data(filtered_data_a, function(d) { | |
| return d.id; | |
| }); | |
| enter_y_labels = y_labels.enter().append("text").text(function(d) { | |
| return d.label; | |
| }).attr({ | |
| "class": "y_label", | |
| x: -LABEL_PAD, | |
| y: 0, | |
| dy: '0.35em', | |
| opacity: 0 | |
| }).on('click', function(d) { | |
| if (!filter_active) { | |
| toggle(d, 'as'); | |
| return redraw(); | |
| } | |
| }); | |
| y_labels.classed('selected', function(d) { | |
| return d.selected; | |
| }); | |
| y_labels.exit().transition().duration(ANIMATION_DURATION).attr({ | |
| opacity: 0 | |
| }).remove(); | |
| y_labels.transition().delay(ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| y: function(d) { | |
| return y(d.id) + cell / 2; | |
| } | |
| }); | |
| return enter_y_labels.transition().delay(2 * ANIMATION_DURATION).duration(ANIMATION_DURATION).attr({ | |
| opacity: 1 | |
| }); | |
| }; | |
| d3.select('.filter_box > input').on('click', function() { | |
| filter_active = !filter_active; | |
| return redraw(); | |
| }); | |
| redraw(); | |
| }).call(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment