Commit 61a34d05 authored by Themousaillon's avatar Themousaillon

improve mapVisu, add Time chart

parent 93b39d89
......@@ -101,6 +101,7 @@
},
"dependencies": {
"angular": "1.5.11",
"chart.js": "^2.9.3",
"d3-force": "^1.0.6",
"gl-mat4": "^1.1.4",
"jquery-contextmenu": "^2.6.3",
......
This diff is collapsed.
......@@ -146,13 +146,13 @@ class NodeFilters {
filterByKeywords = keywords => node => {
const filterable = this._settings.filterConf.keywords.filterable
const filterField = this._settings.filterConf.keywords.field
const content = filterField === "label" ? node.label: node.data[filterField]
const content = filterField === "label" || !node.data[filterField] ? node.label: node.data[filterField].toLowerCase()
if (node.collapsed)
return false
if (keywords.length === 0)
return true
else if (filterable.includes(node.type))
return keywords.every(keyword => content.includes(keywords))
return keywords.every(keyword => content.includes(keyword))
else
return true
}
......@@ -186,6 +186,9 @@ export class Graph2d {
this._cullList = cullList
this._queryEngine = new QueryEngine(dbConnector)
this._pending = []
this._events = {
change: []
}
this._filters = new NodeFilters(dbConnector.settings)
if (idList.length !== 0)
this.fetchFromIds(idList)(x => x)
......@@ -203,12 +206,19 @@ export class Graph2d {
const [h, ...t] = tail
if (func)
func().then(() => sub(h, t))
else
else {
this._events.change.forEach(event => event())
resolve()
}
}
sub(head, tail)
})
on = (eventType, handler) => {
if (eventType in this._events)
this._events[eventType].push(handler)
}
fetchFromIds = ids => f => this._pending.push(() => {
const idList = ids.filter(id => !this._currentNodes.has(id))
ids.forEach(id => {
......@@ -284,12 +294,36 @@ export class Graph2d {
getByNodeTypes = (nodeTypes) => this._nodes.collectFromKeys([...this._currentNodes]).filter(n => nodeTypes.includes(n.type))
getAlongRelationType = (nodes, relationType, includeHidden=false) => {
const getTargetIds = node => node.targets.toArray(t => t.type === relationType).map(r => r.id)
const getParentsIds = node => node.parents.toArray(p => p.type === relationType).map(r => r.id)
return this._nodes.collectFromKeys(
getAlongRelationType = (nodes, relationType, aggregateFields=[], includeHidden=false) => {
let extraFields = []
const getTargetIds = node => {
const targets = node.targets.toArray(t => t.type === relationType).map(r => r.id)
for (let _ in targets){
let extra = {}
aggregateFields.forEach(f => extra[f] = node.data[f])
extraFields.push(extra)
}
return targets
}
const getParentsIds = node => {
const parents = node.parents.toArray(p => p.type === relationType).map(r => r.id)
for (let _ in parents){
let extra = {}
aggregateFields.forEach(f => extra[f] = node.data[f])
extraFields.push(extra)
}
return parents
}
const results = this._nodes.collectFromKeys(
nodes.filter(n => !n.hide || includeHidden).map(n => getParentsIds(n).concat(getTargetIds(n))).flat()
)
if (aggregateFields.length > 0)
results.forEach(
(node, idx) => Object.entries(extraFields[idx]).forEach(
([f, v]) => node.data[f] = v)
)
return results
}
......
......@@ -421,11 +421,32 @@ intergraph .tooltip-arrow {
z-index: 2;
}
#mapVisu > .ol-viewport > .ol-overlaycontainer {
#mapCanvasContainer > .ol-viewport > .ol-overlaycontainer {
z-index: 1 !important;
}
#mapVisu {
position: absolute;
height: 100vh;
width: 100vw;
}
#mapCanvasContainer {
height: 60vh;
width: 100vw;
}
#chartCanvas {
width: 100vw !important;
height: 25vh !important;
margin-bottom: 100px;
}
.tooltip-mapVisu {
position: absolute;
z-index: 100000;
pointer-events: none;
font-weight: bold;
font-size: large;
}
\ No newline at end of file
......@@ -6,7 +6,8 @@ import { testGraph2d } from "./2d-graphosaurus/test"
import { Graph2d } from "./2d-graphosaurus/graph";
import { Graph2D } from "./graph2D";
import { EntrySearch } from '../searchTools/entrySearch'
import { MapVisu } from './map_visu'
import { MapVisu } from './mapVisu/map_visu'
import { TimeChart } from './mapVisu/timeChart'
const dbConnectorConstructor = require('../database/DatabaseConnector'),
NodesList = require('../misc/nodes-list'),
......@@ -30,7 +31,7 @@ const toIso = (dateValue) => {
return date
}
function setupDateFilter(app, newGraph, mapVisu, id, start, end) {
function setupDateFilter(app, newGraph, mapVisu, timeChart, id, start, end) {
let dateSlider = document.getElementById(id + '-slider');
// Create a string representation of the date.
......@@ -81,6 +82,7 @@ function setupDateFilter(app, newGraph, mapVisu, id, start, end) {
dateValues[handle].innerHTML = values[handle];
app.updateFilter();
mapVisu.drawSources()
timeChart.plot()
newGraph.filterGraph();
newGraph.applyFilterToGraph();
});
......@@ -91,8 +93,8 @@ function start(settings, login, password) {
let url = settings.database.url;
let dbConnector = new dbConnectorConstructor(settings, url, login, password);
document.getElementById("load-autocompletion").style.display = "flex";
document.getElementById("load-autocompletion").style.display = "none";
const entrySearch = new EntrySearch(dbConnector, () => {
document.getElementById("load-autocompletion").style.display = "none";
$('#keyword').focus();
});
......@@ -105,7 +107,8 @@ function start(settings, login, password) {
let app = new App(dbConnector);
const graph2D = new Graph2D(dbConnector, [], []);
// setup the map canevas
const mapVisu = new MapVisu("mapVisu", graph2D, settings);
const mapVisu = new MapVisu("mapVisu", "mapCanvasContainer", graph2D, settings);
const timeChart = new TimeChart("chartCanvas", graph2D, settings);
testGraph2d(dbConnector)
// Setup interface
......@@ -164,7 +167,7 @@ function start(settings, login, password) {
if (!!option.filter) {
switch (option.type) {
case "date":
setupDateFilter(app, graph2D, mapVisu, id, option.filter.start, option.filter.end);
setupDateFilter(app, graph2D, mapVisu,timeChart, id, option.filter.start, option.filter.end);
break;
case 'category':
let list = [option.filter, 'others'];
......@@ -261,6 +264,7 @@ function start(settings, login, password) {
$('#right-panel').hide()
$('#mapVisu').show();
mapVisu.drawSources()
timeChart.plot()
$('#graph').hide();
$('#generate-btn').hide();
$('#graph2D').hide();
......@@ -465,6 +469,7 @@ function start(settings, login, password) {
graph2D.filterGraph();
graph2D.applyFilterToGraph();
mapVisu.drawSources();
timeChart.plot()
})
$("#filter-content").attr("placeholder",
......
......@@ -111,6 +111,8 @@
</div>
</div>
<div id="mapVisu">
<div id="mapCanvasContainer"></div>
<canvas id="chartCanvas" width="800" height="100"></canvas>
</div>
<div id="filters">
<table>
......
export class Tooltip {
constructor(tooltipId){
$(document.body).append(`<div id="${tooltipId}"></div>`)
this._tooltip = $(`#${tooltipId}`)
this._tooltip.addClass("tooltip-mapVisu")
this._x = 0
this._y = 0
this.offset = 10
window.addEventListener("mousemove", this.translate)
this.displayed = false
}
translate = (ev) => {
const translateX = ev.clientX - this._x
const translateY = ev.clientY - this._y
if (this.displayed)
this._tooltip.css("transform", `translate(${translateX}px, ${translateY}px)`)
}
setLegend = legendString => {
this._tooltip.html(legendString)
}
show = (x, y) => {
this._tooltip.show()
this.displayed = true
this._tooltip.html("petit test des familles")
this._tooltip.css("left", `${x + this.offset}px`)
this._tooltip.css("top", `${y + this.offset}px`)
this._x = x
this._y = y
}
hide = () => {
this._tooltip.hide()
this.displayed = false
}
}
\ No newline at end of file
......@@ -5,14 +5,45 @@ import TileLayer from 'ol/layer/Tile'
import OMS from 'ol/source/OSM'
import Feature from 'ol/Feature'
import Circle from 'ol/geom/Circle'
import {Style, Fill, Stroke} from 'ol/style'
import { fromLonLat } from 'ol/proj'
import { Tooltip } from '../interractions/Tooltip'
export class MapVisu {
constructor(elementId, graph2D, settings){
constructor(elementId, mapContainerId, graph2D, settings){
this._settings = settings
this._canevas = elementId
this._canvas = elementId
this._tooltip = new Tooltip("tooltip_mapVisu")
this._graph2D = graph2D
this._highlighted = []
this._styles = {
externalCircleStyle: () => new Style({
fill: new Fill({color: 'rgba(255, 127, 0, 0.40)'}),
stroke: new Stroke({
color: "black",
width: 1
}),
zIndex: 5
}),
externalCircleStyleHover: () => new Style({
fill: new Fill({color: 'rgba(255, 127, 0, 0.60)'}),
stroke: new Stroke({
color: "black",
width: 3
}),
zIndex: 5
}),
internalCircleStyle: () => new Style({
fill: new Fill({color: 'rgba(255, 127, 0, 0.60)'}),
stroke: new Stroke({
color: "black",
width: 1
}),
zIndex: 12
})
}
this._graph2D.graph2d.on("change", this.drawSources)
this._sourceVector = new VectorSource()
this._sourceLayer = new VectorLayer({
source: this._sourceVector
......@@ -28,41 +59,76 @@ export class MapVisu {
}),
this._sourceLayer
],
target: this._canevas
target: mapContainerId
});
this._map.on('pointermove', this._pointerMove)
$(`#${mapContainerId}`).on("mouseleave", this._tooltip.hide)
}
_pointerMove = (ev) => {
if (this._highlighted.length === 0)
this._tooltip.hide()
this._highlighted.forEach(f => f.setStyle(this._styles.externalCircleStyle()))
this._highlighted = []
const currentFeatures = this._map.getFeaturesAtPixel(ev.pixel)
const internalFeature = currentFeatures.find(f => f.get("type") === "internal")
if (internalFeature){
this._tooltip.show(...ev.pixel)
this._tooltip.setLegend(internalFeature.get("label"))
internalFeature.setStyle(this._styles.externalCircleStyleHover())
this._highlighted.push(internalFeature)
}
else if (currentFeatures.length > 0){
this._tooltip.show(...ev.pixel)
this._tooltip.setLegend(currentFeatures[0].get("label"))
currentFeatures[0].setStyle(this._styles.externalCircleStyleHover())
this._highlighted.push(currentFeatures[0])
}
}
getGeoInfosByRelation = () => {
const drawAbleTypes = this._settings.mapVisu.nodeTypes
const linkType = this._settings.mapVisu.geoInfo.linkType
const geoNodes = this._graph2D.graph2d.getByNodeTypes(drawAbleTypes)
return this._graph2D.graph2d.getAlongRelationType(geoNodes, linkType).filter(n => n.data.longitude && n.data.latitude)
.map(n => fromLonLat([n.data.longitude, n.data.latitude]))
return this._graph2D.graph2d.getAlongRelationType(geoNodes, linkType)
.filter(n => n.data.longitude && n.data.latitude)
.map(n => ({
coord: fromLonLat([n.data.longitude, n.data.latitude]),
label: n.label,
}))
}
drawSources = () => {
this._sourceVector.clear()
let countPerCoord = {}
this.getGeoInfosByRelation().forEach(([longitude, latitude]) => {
this.getGeoInfosByRelation().forEach(data => {
const [longitude, latitude] = data.coord
const key = `${longitude},${latitude}`
if (key in countPerCoord)
countPerCoord[key].count ++
else {
countPerCoord[key] = {
count: 1,
coord: [longitude, latitude]
coord: [longitude, latitude],
label: data.label
}
}
})
Object.values(countPerCoord).forEach(({coord, count}) =>
this._sourceVector.addFeature(
new Feature(
new Circle(coord, count*10000)
)
)
)
Object.values(countPerCoord).forEach(({coord, count, label}) => {
const external = new Feature(new Circle(coord, Math.min(100000, count*10000)))
external.setStyle(this._styles.externalCircleStyle())
external.setProperties({type: "external", label: `${label}: ${count}`})
const internal = new Feature(new Circle(coord, 10000))
internal.setStyle(this._styles.internalCircleStyle())
internal.setProperties({type: "internal", label: `${label}: ${count}`})
this._sourceVector.addFeature(internal)
this._sourceVector.addFeature(external)
})
}
}
\ No newline at end of file
import Chart from 'chart.js'
export class TimeChart {
constructor(elementId, graph2D, settings) {
this._settings = settings
this._canvas = elementId
this._graph2D = graph2D
this._highlighted = []
this._graph2D.graph2d.on("change", this.plot)
this.data = []
this.chartConfig = {
type: 'line',
data: this.data,
options: {
title: {
text: 'Chart.js Time Scale'
},
scales: {
x: {
type: 'time',
time: {
unit: "year",
parser: "YYYY"
},
scaleLabel: {
display: true,
labelString: 'Date'
}
},
y: {
scaleLabel: {
display: true,
labelString: 'value'
}
}
},
}
};
this.chart = new Chart($(`#${elementId}`), this.chartConfig)
}
toDateString = date => date.getFullYear().toString()
getSourceData = () => {
const backgroundColor = "rgba(255, 127, 0, 0.20)"
const borderColor = "rgb(255, 127, 0)"
let labels = []
let data = {}
const drawAbleTypes = this._settings.mapVisu.nodeTypes
const timeNodes = this._graph2D.graph2d.getByNodeTypes(drawAbleTypes)
.filter(n => !n.hide)
.map(n => [this.toDateString(n.data.startDate), this.toDateString(n.data.endDate)])
timeNodes.forEach(([t1, t2]) => {
if (!(t1 in data)){
data[t1] = 0
labels.push(t1)
}
if (!(t2 in data)){
data[t2] = 0
labels.push(t2)
}
})
timeNodes.forEach(
([t1, t2]) => {
labels.forEach(t => {
if (t1 <= t && t <= t2){
data[t] ++
}
})
})
labels.sort()
const yData = labels.map(t => data[t])
this.data["labels"] = labels
this.data["datasets"] = [{label: "number of regesta over time", data: yData, backgroundColor: backgroundColor, borderColor: borderColor}]
}
plot = () => {
this.getSourceData()
this.chart.update()
}
}
\ No newline at end of file
......@@ -50,7 +50,7 @@ export default class IdSet {
add = objs => objs.forEach(obj => this.addOne(obj))
collectFromKeys = idList => idList.map(id => this._entries[id])
collectFromKeys = idList => idList.map(id => this._entries[id]).filter(x => x)
/**
* if condFn is true, call f1 else call f2
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment