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;