Skip to content

Instantly share code, notes, and snippets.

@kleem
Last active June 13, 2016 13:25
Show Gist options
  • Save kleem/6b35690b61a9db29686a96b1eadb5539 to your computer and use it in GitHub Desktop.
Save kleem/6b35690b61a9db29686a96b1eadb5539 to your computer and use it in GitHub Desktop.
Filtered matrix
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