diff --git a/package.json b/package.json index a986dda..bdcf235 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iknow-entity-browser", - "version": "0.0.4", + "version": "0.0.5", "description": "Visualizer for iKnow entities", "main": "gulpfile.babel.js", "scripts": { diff --git a/src/static/js/graph/index.js b/src/static/js/graph/index.js new file mode 100644 index 0000000..aea9dec --- /dev/null +++ b/src/static/js/graph/index.js @@ -0,0 +1,182 @@ +import { updateSelectedNodes } from "../tabular"; +import { getGraphData } from "../model"; + +export function update () { + + var graph = getGraphData(); + + var shiftKey, ctrlKey, + width = window.innerWidth, + height = window.innerHeight; + + var zoomer = d3.zoom() + .scaleExtent([1/4, 40]) + .on("zoom", () => { + view.attr("transform", d3.event.transform); + }); + + var dragger = d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended); + + d3.select(window) + .on("keydown", keyDown) + .on("keyup", keyUp); + + var svg = d3.select("#graph") + .call(zoomer); + + var view = svg + .append("g") + .attr("class", "view"); + + var brush = view.append("g") + .datum(() => { return { selected: false, previouslySelected: false }; }) + .attr("class", "brush"); + + // var color = d3.scaleOrdinal(d3.schemeCategory20); + + var simulation = d3.forceSimulation() + .force("link", + d3.forceLink() + .distance(d => 50 + (d.source.radius + d.target.radius) * 2) + .id(d => d.id) + ) + .force("charge", + d3.forceManyBody() + .strength(d => { return -10 * d.radius; }) + ) + .force("center", d3.forceCenter(width / 2, height / 2)); + + var link = view.append("g") + .attr("class", "links") + .selectAll("line") + .data(graph.edges) + .enter().append("line") + .attr("class", d => d.type === "similar" + ? "similar" + : d.type === "related" + ? "related" + : "other" + ); + + var node = view.append("g") + .attr("class", "nodes") + .selectAll(".node") + .data(graph.nodes) + .enter().append("g") + .attr("class", "node") + .call(dragger) + .on("dblclick", () => d3.event.stopPropagation()) + .on("click", function (d) { + if (d3.event.defaultPrevented) return; + if (!ctrlKey) { + node.classed("selected", (p) => p.selected = p.previouslySelected = false) + } + d3.select(this).classed("selected", d.selected = !d.selected); // (!prevSel) + updateSelectedNodes(); + }); + + var circle = node.append("circle") + .attr("r", d => d.radius); + + node.append("text") + .attr("dy", ".3em") + .attr("style", d => `font-size:${ Math.round(d.radius / 2) }px`) + .text(d => d.label); + + simulation + .nodes(graph.nodes) + .on("tick", ticked); + + simulation.force("link") + .links(graph.edges); + + var brusher = d3.brush() + .extent([[-9999999, -9999999], [9999999, 9999999]]) + .on("start.brush", () => { + if (!d3.event.sourceEvent) return; + node.each((d) => { + d.previouslySelected = ctrlKey && d.selected; + }); + }) + .on("brush.brush", () => { + if (!d3.event.sourceEvent) return; + var extent = d3.event.selection; + if (!extent) + return; + node.classed("selected", (d) => { + return d.selected = d.previouslySelected ^ + (extent[0][0] <= d.x && d.x < extent[1][0] + && extent[0][1] <= d.y && d.y < extent[1][1]); + }); + }) + .on("end.brush", () => { + if (!d3.event.sourceEvent) return; + setTimeout(() => { + brush.call(brusher.move, null); + updateSelectedNodes(); + }, 25); + }); + + brush.call(brusher) + .on(".brush", null); + + brush.select('.overlay').style('cursor', 'auto'); + + for (var i = 100; i > 0; --i) simulation.tick(); + + function ticked () { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + node + .attr("transform", (d) => `translate(${ d.x },${ d.y })`) + } + + function dragstarted (d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged (d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended (d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + function keyDown () { + shiftKey = d3.event.shiftKey || d3.event.metaKey; + ctrlKey = d3.event.ctrlKey; + + if (d3.event.keyCode == 67) { // the 'c' key + // do stuff + } + + if (ctrlKey) { + brush.select('.overlay').style('cursor', 'crosshair'); + brush.call(brusher); + d3.event.preventDefault(); + } + } + + function keyUp () { + shiftKey = d3.event.shiftKey || d3.event.metaKey; + ctrlKey = d3.event.ctrlKey; + + brush.call(brusher) + .on(".brush", null); + + brush.select('.overlay').style('cursor', 'auto'); + } + +} \ No newline at end of file diff --git a/src/static/js/index.js b/src/static/js/index.js index daaa066..576e95d 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -1,222 +1,9 @@ -import sampleData from "./sample_output2.json"; -import { uiState } from "./ui"; -import { csv } from "./export"; - -var graph = preprocess(sampleData.graph); - -function preprocess (graph) { - graph.nodes.forEach(node => node.radius = 5 + Math.sqrt(node.entities[0].score/4 || 25)); - return graph; -} - -function updateSelectedNodes () { - if (!uiState.tableToggled) - return; - let data = graph.nodes.filter(node => !!node.selected).sort((a, b) => - a.entities[0].score > b.entities[0].score ? -1 : 1 - ), - table = document.querySelector("#table table tbody"); - table.textContent = ""; - for (let i = 0; i < data.length; i++) { - let row = table.insertRow(i), - node = data[i]; - row.insertCell(0).textContent = node.id; - row.insertCell(1).textContent = node.label; - row.insertCell(2).textContent = node.entities[0].score; - row.insertCell(3).textContent = node.entities[0].frequency; - row.insertCell(4).textContent = node.entities[0].spread; - } -} +import { update } from "./graph"; +import * as tabular from "./tabular"; window.init = () => { - var shiftKey, ctrlKey, - width = window.innerWidth, - height = window.innerHeight; - - var zoomer = d3.zoom() - .scaleExtent([1/4, 40]) - .on("zoom", () => { - view.attr("transform", d3.event.transform); - }); - - var dragger = d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended); - - d3.select(window) - .on("keydown", keyDown) - .on("keyup", keyUp); - - var svg = d3.select("#graph") - .call(zoomer); - - var view = svg - .append("g") - .attr("class", "view"); - - var brush = view.append("g") - .datum(() => { return { selected: false, previouslySelected: false }; }) - .attr("class", "brush"); - - // var color = d3.scaleOrdinal(d3.schemeCategory20); - - var simulation = d3.forceSimulation() - .force("link", - d3.forceLink() - .distance(d => 50 + (d.source.radius + d.target.radius) * 2) - .id(d => d.id) - ) - .force("charge", - d3.forceManyBody() - .strength(d => { return -10 * d.radius; }) - ) - .force("center", d3.forceCenter(width / 2, height / 2)); - - var link = view.append("g") - .attr("class", "links") - .selectAll("line") - .data(graph.edges) - .enter().append("line") - .attr("class", d => d.type === "similar" - ? "similar" - : d.type === "related" - ? "related" - : "other" - ); - - var node = view.append("g") - .attr("class", "nodes") - .selectAll(".node") - .data(graph.nodes) - .enter().append("g") - .attr("class", "node") - .call(dragger) - .on("dblclick", () => d3.event.stopPropagation()) - .on("click", function (d) { - if (d3.event.defaultPrevented) return; - if (!ctrlKey) { - node.classed("selected", (p) => p.selected = p.previouslySelected = false) - } - d3.select(this).classed("selected", d.selected = !d.selected); // (!prevSel) - updateSelectedNodes(); - }); - - var circle = node.append("circle") - .attr("r", d => d.radius); - - node.append("text") - .attr("dy", ".3em") - .attr("style", d => `font-size:${ Math.round(d.radius / 2) }px`) - .text(d => d.label); - - simulation - .nodes(graph.nodes) - .on("tick", ticked); - - simulation.force("link") - .links(graph.edges); - - var brusher = d3.brush() - .extent([[-9999999, -9999999], [9999999, 9999999]]) - .on("start.brush", () => { - if (!d3.event.sourceEvent) return; - node.each((d) => { - d.previouslySelected = ctrlKey && d.selected; - }); - }) - .on("brush.brush", () => { - if (!d3.event.sourceEvent) return; - var extent = d3.event.selection; - if (!extent) - return; - node.classed("selected", (d) => { - return d.selected = d.previouslySelected ^ - (extent[0][0] <= d.x && d.x < extent[1][0] - && extent[0][1] <= d.y && d.y < extent[1][1]); - }); - }) - .on("end.brush", () => { - if (!d3.event.sourceEvent) return; - setTimeout(() => { - brush.call(brusher.move, null); - updateSelectedNodes(); - }, 25); - }); - - brush.call(brusher) - .on(".brush", null); - - brush.select('.overlay').style('cursor', 'auto'); - - for (var i = 100; i > 0; --i) simulation.tick(); - - function ticked () { - link - .attr("x1", d => d.source.x) - .attr("y1", d => d.source.y) - .attr("x2", d => d.target.x) - .attr("y2", d => d.target.y); - node - .attr("transform", (d) => `translate(${ d.x },${ d.y })`) - } - - function dragstarted (d) { - if (!d3.event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - function dragged (d) { - d.fx = d3.event.x; - d.fy = d3.event.y; - } - - function dragended (d) { - if (!d3.event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } - - function keyDown () { - shiftKey = d3.event.shiftKey || d3.event.metaKey; - ctrlKey = d3.event.ctrlKey; - - if (d3.event.keyCode == 67) { // the 'c' key - // do stuff - } - - if (ctrlKey) { - brush.select('.overlay').style('cursor', 'crosshair'); - brush.call(brusher); - d3.event.preventDefault(); - } - } - - function keyUp () { - shiftKey = d3.event.shiftKey || d3.event.metaKey; - ctrlKey = d3.event.ctrlKey; - - brush.call(brusher) - .on(".brush", null); - - brush.select('.overlay').style('cursor', 'auto'); - } - - d3.select("#tableToggle") - .data([uiState]) - .on("click", function (d) { - d.tableToggled = !d.tableToggled; - d3.select(this).classed("toggled", d.tableToggled); - d3.select("#table").classed("active", d.tableToggled); - updateSelectedNodes(); - }); - - d3.select("#exportCSV").on("click", () => { - csv([].slice.call(document.querySelector("#table table").rows).map(row => - [].slice.call(row.cells).map(cell => cell.textContent) - )); - }); + update(); + tabular.init(); }; \ No newline at end of file diff --git a/src/static/js/model/index.js b/src/static/js/model/index.js new file mode 100644 index 0000000..d771d67 --- /dev/null +++ b/src/static/js/model/index.js @@ -0,0 +1,16 @@ +import sampleData from "../sample_output2.json"; + +function preprocess (graph) { + graph.nodes.forEach(node => node.radius = 5 + Math.sqrt(node.entities[0].frequency / 4 || 25)); + return graph; +} + +var graph = preprocess(sampleData.graph); + +export function getGraphData () { + return graph; +} + +export var uiState = { + tableToggled: false +}; diff --git a/src/static/js/sampleData.json b/src/static/js/sample_output.json similarity index 100% rename from src/static/js/sampleData.json rename to src/static/js/sample_output.json diff --git a/src/static/js/sample_output2.json b/src/static/js/sample_output2.json index 7ea9d85..cfe12e3 100644 --- a/src/static/js/sample_output2.json +++ b/src/static/js/sample_output2.json @@ -3,7 +3,7 @@ "nodes": [ { "id": 0, - "label": "patient", + "label": "medicine", "type": "entity", "entities": [ { @@ -11,7 +11,7 @@ "id": 8378, "score": 3626, "spread": 214, - "value": "patient" + "value": "medicine" } ] }, diff --git a/src/static/js/export.js b/src/static/js/tabular/export.js similarity index 100% rename from src/static/js/export.js rename to src/static/js/tabular/export.js diff --git a/src/static/js/tabular/index.js b/src/static/js/tabular/index.js new file mode 100644 index 0000000..71390fa --- /dev/null +++ b/src/static/js/tabular/index.js @@ -0,0 +1,44 @@ +import { csv } from "./export"; +import * as model from "../model"; + +var graph; + +export function updateSelectedNodes () { + if (!model.uiState.tableToggled) + return; + let data = graph.nodes.filter(node => !!node.selected).sort((a, b) => + a.entities[0].score > b.entities[0].score ? -1 : 1 + ), + table = document.querySelector("#table table tbody"); + table.textContent = ""; + for (let i = 0; i < data.length; i++) { + let row = table.insertRow(i), + node = data[i]; + row.insertCell(0).textContent = node.id; + row.insertCell(1).textContent = node.label; + row.insertCell(2).textContent = node.entities[0].score; + row.insertCell(3).textContent = node.entities[0].frequency; + row.insertCell(4).textContent = node.entities[0].spread; + } +} + +export function init () { + + graph = model.getGraphData(); + + d3.select("#tableToggle") + .data([model.uiState]) + .on("click", function (d) { + d.tableToggled = !d.tableToggled; + d3.select(this).classed("toggled", d.tableToggled); + d3.select("#table").classed("active", d.tableToggled); + updateSelectedNodes(); + }); + + d3.select("#exportCSV").on("click", () => { + csv([].slice.call(document.querySelector("#table table").rows).map(row => + [].slice.call(row.cells).map(cell => cell.textContent) + )); + }); + +} \ No newline at end of file diff --git a/src/static/js/ui.js b/src/static/js/ui.js deleted file mode 100644 index bf79663..0000000 --- a/src/static/js/ui.js +++ /dev/null @@ -1,3 +0,0 @@ -export var uiState = { - tableToggled: false -};