From 09bdb7d61a175dfc5aea97030ae1f532cbaea1c7 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 8 Jul 2015 00:36:57 +0200 Subject: [PATCH 1/5] basic filter support --- lib/datadistributor.js | 76 ++++++++++++++++++++++++++++++++++++++ lib/filters/filtergui.js | 34 +++++++++++++++++ lib/filters/genericnode.js | 32 ++++++++++++++++ lib/filters/nodefilter.js | 33 +++++++++++++++++ lib/gui.js | 46 ++++++++++------------- lib/proportions.js | 34 +++++++++++------ scss/_filters.scss | 28 ++++++++++++++ scss/main.scss | 1 + 8 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 lib/datadistributor.js create mode 100644 lib/filters/filtergui.js create mode 100644 lib/filters/genericnode.js create mode 100644 lib/filters/nodefilter.js create mode 100644 scss/_filters.scss diff --git a/lib/datadistributor.js b/lib/datadistributor.js new file mode 100644 index 0000000..e19dc4c --- /dev/null +++ b/lib/datadistributor.js @@ -0,0 +1,76 @@ +define([], function () { + return function () { + var targets = [] + var filterObservers = [] + var filters = [] + var filteredData + var data + + function remove(d) { + targets = targets.filter( function (e) { return d !== e } ) + } + + function add(d) { + targets.push(d) + + if (filteredData !== undefined) + d.setData(filteredData) + } + + function setData(d) { + data = d + refresh() + } + + function refresh() { + if (data === undefined) + return + + filteredData = filters.reduce( function (a, f) { + return f.run(a) + }, data) + + targets.forEach( function (t) { + t.setData(filteredData) + }) + } + + function notifyObservers() { + filterObservers.forEach( function (d) { + d.filtersChanged(filters) + }) + } + + function addFilter(d) { + filters.push(d) + notifyObservers() + d.setRefresh(refresh) + refresh() + } + + function removeFilter(d) { + filters = filters.filter( function (e) { return d !== e } ) + notifyObservers() + refresh() + } + + function watchFilters(d) { + filterObservers.push(d) + + d.filtersChanged(filters) + + return function () { + filterObservers = filterObservers.filter( function (e) { return d !== e }) + } + } + + return { add: add, + remove: remove, + setData: setData, + addFilter: addFilter, + removeFilter: removeFilter, + watchFilters: watchFilters, + refresh: refresh + } + } +}) diff --git a/lib/filters/filtergui.js b/lib/filters/filtergui.js new file mode 100644 index 0000000..daaf4d6 --- /dev/null +++ b/lib/filters/filtergui.js @@ -0,0 +1,34 @@ +define([], function () { + return function (distributor) { + var container = document.createElement("ul") + container.classList.add("filters") + + function render(el) { + el.appendChild(container) + } + + function filtersChanged(filters) { + while (container.firstChild) + container.removeChild(container.firstChild) + + filters.forEach( function (d) { + var li = document.createElement("li") + var div = document.createElement("div") + container.appendChild(li) + li.appendChild(div) + d.render(div) + + var button = document.createElement("button") + button.textContent = "" + button.onclick = function () { + distributor.removeFilter(d) + } + li.appendChild(button) + }) + } + + return { render: render, + filtersChanged: filtersChanged + } + } +}) diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js new file mode 100644 index 0000000..9a569f9 --- /dev/null +++ b/lib/filters/genericnode.js @@ -0,0 +1,32 @@ +define(["filters/nodefilter"], function (nodefilter) { + return function (name, key, value, f) { + function run(d) { + var o = dictGet(d, key.slice(0)) + + if (f) + o = f(o) + + return o === value + } + + function setRefresh() { + } + + function render(el) { + var label = document.createElement("label") + label.textContent = name + " " + + var strong = document.createElement("strong") + strong.textContent = value + + label.appendChild(strong) + + el.appendChild(label) + } + + return { run: nodefilter(run), + setRefresh: setRefresh, + render: render + } + } +}) diff --git a/lib/filters/nodefilter.js b/lib/filters/nodefilter.js new file mode 100644 index 0000000..e4e979d --- /dev/null +++ b/lib/filters/nodefilter.js @@ -0,0 +1,33 @@ +define([], function () { + return function (filter) { + return function (data) { + var n = Object.create(data) + n.nodes = {} + + for (var key in data.nodes) { + n.nodes[key] = data.nodes[key].filter(filter) + } + + var filteredIds = new Set() + + n.graph = {} + n.graph.nodes = data.graph.nodes.filter( function (d) { + if (!d.node) + return true + + var r = filter(d.node) + + if (r) + filteredIds.add(d.id) + + return r + }) + + n.graph.links = data.graph.links.filter( function (d) { + return filteredIds.has(d.source.id) && filteredIds.has(d.target.id) + }) + + return n + } + } +}) diff --git a/lib/gui.js b/lib/gui.js index a3fcbc2..473eb84 100644 --- a/lib/gui.js +++ b/lib/gui.js @@ -1,13 +1,13 @@ define([ "chroma-js", "map", "sidebar", "tabs", "container", "meshstats", "legend", "linklist", "nodelist", "simplenodelist", "infobox/main", - "proportions", "forcegraph", "title", "about" ], + "proportions", "forcegraph", "title", "about", "datadistributor", + "filters/filtergui" ], function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph, - Title, About) { + Title, About, DataDistributor, FilterGUI) { return function (config, router) { var self = this - var dataTargets = [] - var latestData + var fanout = new DataDistributor() var content var contentDiv @@ -17,16 +17,13 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, var buttons = document.createElement("div") buttons.classList.add("buttons") - function dataTargetRemove(d) { - dataTargets = dataTargets.filter( function (e) { return d !== e }) - } - function removeContent() { if (!content) return router.removeTarget(content) - dataTargetRemove(content) + fanout.remove(content) + content.destroy() content = null @@ -38,10 +35,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, content = new K(config, linkScale, sidebar.getWidth, router, buttons) content.render(contentDiv) - if (latestData) - content.setData(latestData) - - dataTargets.push(content) + fanout.add(content) router.addTarget(content) } @@ -82,15 +76,15 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, var lostnodeslist = new SimpleNodelist("lost", "lastseen", router, "Verschwundene Knoten") var nodelist = new Nodelist(router) var linklist = new Linklist(linkScale, router) - var statistics = new Proportions(config) + var statistics = new Proportions(config, fanout) var about = new About() - dataTargets.push(meshstats) - dataTargets.push(newnodeslist) - dataTargets.push(lostnodeslist) - dataTargets.push(nodelist) - dataTargets.push(linklist) - dataTargets.push(statistics) + fanout.add(meshstats) + fanout.add(newnodeslist) + fanout.add(lostnodeslist) + fanout.add(nodelist) + fanout.add(linklist) + fanout.add(statistics) sidebar.add(header) header.add(meshstats) @@ -99,6 +93,10 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, overview.add(newnodeslist) overview.add(lostnodeslist) + var filterGUI = new FilterGUI(fanout) + fanout.watchFilters(filterGUI) + header.add(filterGUI) + sidebar.add(tabs) tabs.add("Aktuelles", overview) tabs.add("Knoten", nodelist) @@ -114,13 +112,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, router.view("m") - self.setData = function (data) { - latestData = data - - dataTargets.forEach(function (d) { - d.setData(data) - }) - } + self.setData = fanout.setData return self } diff --git a/lib/proportions.js b/lib/proportions.js index b615ac1..d9df65e 100644 --- a/lib/proportions.js +++ b/lib/proportions.js @@ -1,7 +1,7 @@ -define(["chroma-js", "virtual-dom", "numeral-intl", "vercomp" ], - function (Chroma, V, numeral, vercomp) { +define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "vercomp" ], + function (Chroma, V, numeral, Filter, vercomp) { - return function (config) { + return function (config, filterManager) { var self = this var scale = Chroma.scale("YlGnBu").mode("lab") @@ -68,10 +68,18 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "vercomp" ], dict[v] = 1 + (v in dict ? dict[v] : 0) }) - return Object.keys(dict).map(function (d) { return [d, dict[d]] }) + return Object.keys(dict).map(function (d) { return [d, dict[d], key, f] }) } - function fillTable(table, data) { + function addFilter(filter) { + return function () { + filterManager.addFilter(filter) + + return false + } + } + + function fillTable(name, table, data) { if (!table.last) table.last = V.h("table") @@ -86,7 +94,11 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "vercomp" ], var c1 = Chroma.contrast(scale(v), "white") var c2 = Chroma.contrast(scale(v), "black") - var th = V.h("th", d[0]) + var filter = new Filter(name, d[2], d[0], d[3]) + + var a = V.h("a", { href: "#", onclick: addFilter(filter) }, d[0]) + + var th = V.h("th", a) var td = V.h("td", V.h("span", {style: { width: Math.round(v * 100) + "%", backgroundColor: scale(v).hex(), @@ -127,11 +139,11 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "vercomp" ], return "(deaktiviert)" }) - fillTable(statusTable, statusDict.sort(function (a, b) { return b[1] - a[1] })) - fillTable(fwTable, fwDict.sort(function (a, b) { return vercomp(b[0], a[0]) })) - fillTable(hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] })) - fillTable(geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] })) - fillTable(autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] })) + fillTable("Status", statusTable, statusDict.sort(function (a, b) { return b[1] - a[1] })) + fillTable("Firmware", fwTable, fwDict.sort(function (a, b) { return vercomp(b[0], a[0]) })) + fillTable("Hardware", hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] })) + fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] })) + fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] })) } self.render = function (el) { diff --git a/scss/_filters.scss b/scss/_filters.scss new file mode 100644 index 0000000..d8de370 --- /dev/null +++ b/scss/_filters.scss @@ -0,0 +1,28 @@ +.filters { + margin: 0; + padding: 0 !important; + + li { + display: flex; + padding: 6pt 0 6pt 12pt; + align-items: center; + + & > div { + flex-grow: 1; + } + + @include shadow(1); + } + + button { + box-shadow: none; + margin: 0; + padding: 0; + background: none; + font-size: 1.41em; + + &:hover { + box-shadow: none !important; + } + } +} diff --git a/scss/main.scss b/scss/main.scss index 29db91b..81084ac 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -3,6 +3,7 @@ @import '_base'; @import '_leaflet'; @import '_leaflet.label'; +@import '_filters'; $minscreenwidth: 630pt; $sidebarwidth: 420pt; From caf8383b6ff89ef554071a864485ea2407a77f97 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 8 Jul 2015 17:20:56 +0200 Subject: [PATCH 2/5] filters: negation, styling --- lib/filters/filtergui.js | 8 +++++- lib/filters/genericnode.js | 25 ++++++++++++++++-- scss/_filters.scss | 53 ++++++++++++++++++++++++++++---------- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/lib/filters/filtergui.js b/lib/filters/filtergui.js index daaf4d6..f6c6dac 100644 --- a/lib/filters/filtergui.js +++ b/lib/filters/filtergui.js @@ -2,9 +2,10 @@ define([], function () { return function (distributor) { var container = document.createElement("ul") container.classList.add("filters") + var div = document.createElement("div") function render(el) { - el.appendChild(container) + el.appendChild(div) } function filtersChanged(filters) { @@ -25,6 +26,11 @@ define([], function () { } li.appendChild(button) }) + + if (container.parentNode === div && filters.length === 0) + div.removeChild(container) + else if (filters.length > 0) + div.appendChild(container) } return { render: render, diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js index 9a569f9..832823c 100644 --- a/lib/filters/genericnode.js +++ b/lib/filters/genericnode.js @@ -1,15 +1,26 @@ define(["filters/nodefilter"], function (nodefilter) { return function (name, key, value, f) { + var negate = false + var refresh + function run(d) { var o = dictGet(d, key.slice(0)) if (f) o = f(o) - return o === value + return o === value ? !negate : negate } - function setRefresh() { + function setRefresh(f) { + refresh = f + } + + function draw(el) { + if (negate) + el.parentNode.classList.add("not") + else + el.parentNode.classList.remove("not") } function render(el) { @@ -19,7 +30,17 @@ define(["filters/nodefilter"], function (nodefilter) { var strong = document.createElement("strong") strong.textContent = value + draw(el) + label.appendChild(strong) + label.onclick = function () { + negate = !negate + + draw(el) + + if (refresh) + refresh() + } el.appendChild(label) } diff --git a/scss/_filters.scss b/scss/_filters.scss index d8de370..2e69dde 100644 --- a/scss/_filters.scss +++ b/scss/_filters.scss @@ -1,28 +1,53 @@ .filters { margin: 0; - padding: 0 !important; + display: flex; + flex-wrap: wrap; + font-family: Roboto; + font-size: 0.83em; + font-weight: bold; + padding: 0 6pt 6pt !important; li { + border-radius: 20pt; display: flex; - padding: 6pt 0 6pt 12pt; + padding: 0pt 0 0pt 8pt; + margin: 3pt; align-items: center; + background: #009ee0; + color: rgba(255, 255, 255, 0.8); - & > div { - flex-grow: 1; + label { + cursor: pointer; + + strong { + color: rgba(255, 255, 255, 1); + } } - @include shadow(1); - } + &.not { + background: #dc0067; + } - button { - box-shadow: none; - margin: 0; - padding: 0; - background: none; - font-size: 1.41em; + button { + box-shadow: none; + margin: 2pt; + padding: 0; + width: 18pt; + height: 18pt; + background: rgba(255, 255, 255, 0.0); + font-size: 12pt; + vertical-align: middle; + color: rgba(255, 255, 255, 0.8); - &:hover { - box-shadow: none !important; + &:hover { + box-shadow: none !important; + color: #dc0067; + background: rgba(255, 255, 255, 0.9); + } + + &:active { + box-shadow: none; + } } } } From 036b6d609933e0cc63b987482ef1f6f920813fee Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 8 Jul 2015 20:50:26 +0200 Subject: [PATCH 3/5] genericnode: show negation in label --- lib/filters/genericnode.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js index 832823c..936e71d 100644 --- a/lib/filters/genericnode.js +++ b/lib/filters/genericnode.js @@ -3,6 +3,11 @@ define(["filters/nodefilter"], function (nodefilter) { var negate = false var refresh + var label = document.createElement("label") + var strong = document.createElement("strong") + label.textContent = name + " " + label.appendChild(strong) + function run(d) { var o = dictGet(d, key.slice(0)) @@ -21,18 +26,14 @@ define(["filters/nodefilter"], function (nodefilter) { el.parentNode.classList.add("not") else el.parentNode.classList.remove("not") + + strong.textContent = (negate ? "¬" : "" ) + value } function render(el) { - var label = document.createElement("label") - label.textContent = name + " " - - var strong = document.createElement("strong") - strong.textContent = value - + el.appendChild(label) draw(el) - label.appendChild(strong) label.onclick = function () { negate = !negate @@ -41,8 +42,6 @@ define(["filters/nodefilter"], function (nodefilter) { if (refresh) refresh() } - - el.appendChild(label) } return { run: nodefilter(run), From 653a329698a27fd2917d28cc33cd2591018f987d Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 8 Jul 2015 21:17:00 +0200 Subject: [PATCH 4/5] filters: combine filters first --- lib/datadistributor.js | 12 ++++++++---- lib/filters/genericnode.js | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/datadistributor.js b/lib/datadistributor.js index e19dc4c..67cf7eb 100644 --- a/lib/datadistributor.js +++ b/lib/datadistributor.js @@ -1,4 +1,4 @@ -define([], function () { +define(["filters/nodefilter"], function (NodeFilter) { return function () { var targets = [] var filterObservers = [] @@ -26,9 +26,13 @@ define([], function () { if (data === undefined) return - filteredData = filters.reduce( function (a, f) { - return f.run(a) - }, data) + var filter = filters.reduce( function (a, f) { + return function (d) { + return a(d) && f.run(d) + } + }, function () { return true }) + + filteredData = new NodeFilter(filter)(data) targets.forEach( function (t) { t.setData(filteredData) diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js index 936e71d..4c2a09d 100644 --- a/lib/filters/genericnode.js +++ b/lib/filters/genericnode.js @@ -1,4 +1,4 @@ -define(["filters/nodefilter"], function (nodefilter) { +define([], function () { return function (name, key, value, f) { var negate = false var refresh @@ -44,7 +44,7 @@ define(["filters/nodefilter"], function (nodefilter) { } } - return { run: nodefilter(run), + return { run: run, setRefresh: setRefresh, render: render } From 8f634b4ba2b2c2ebb3d6337ad3df1270e7fd8328 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 8 Jul 2015 21:28:00 +0200 Subject: [PATCH 5/5] gui: use two datadistributors, one for filtered data --- lib/gui.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/gui.js b/lib/gui.js index 473eb84..ff52361 100644 --- a/lib/gui.js +++ b/lib/gui.js @@ -7,7 +7,6 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, Title, About, DataDistributor, FilterGUI) { return function (config, router) { var self = this - var fanout = new DataDistributor() var content var contentDiv @@ -17,6 +16,10 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, var buttons = document.createElement("div") buttons.classList.add("buttons") + var fanout = new DataDistributor() + var fanoutUnfiltered = new DataDistributor() + fanoutUnfiltered.add(fanout) + function removeContent() { if (!content) return @@ -79,9 +82,9 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, var statistics = new Proportions(config, fanout) var about = new About() - fanout.add(meshstats) - fanout.add(newnodeslist) - fanout.add(lostnodeslist) + fanoutUnfiltered.add(meshstats) + fanoutUnfiltered.add(newnodeslist) + fanoutUnfiltered.add(lostnodeslist) fanout.add(nodelist) fanout.add(linklist) fanout.add(statistics) @@ -112,7 +115,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist, router.view("m") - self.setData = fanout.setData + self.setData = fanoutUnfiltered.setData return self }