Skip to content

Commit

Permalink
Merge pull request projectmesa#465 from RMKD/feature/tooltips
Browse files Browse the repository at this point in the history
Added mouse interactionHandler to close projectmesa#457, fixed hexgrid drawLines
  • Loading branch information
jackiekazil authored Oct 10, 2018
2 parents af4170c + 1059369 commit c40c418
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 27 deletions.
2 changes: 1 addition & 1 deletion mesa/visualization/modules/CanvasGridVisualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CanvasGrid(VisualizationElement):
template: "canvas_module.html" stores the module's HTML template.
"""
package_includes = ["GridDraw.js", "CanvasModule.js"]
package_includes = ["GridDraw.js", "CanvasModule.js", "InteractionHandler.js"]

def __init__(self, portrayal_method, grid_width, grid_height,
canvas_width=500, canvas_height=500):
Expand Down
2 changes: 1 addition & 1 deletion mesa/visualization/modules/HexGridVisualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class CanvasHexGrid(VisualizationElement):
template: "canvas_module.html" stores the module's HTML template.
"""
package_includes = ["HexDraw.js", "CanvasHexModule.js"]
package_includes = ["HexDraw.js", "CanvasHexModule.js", "InteractionHandler.js"]
portrayal_method = None # Portrayal function
canvas_width = 500
canvas_height = 500
Expand Down
12 changes: 12 additions & 0 deletions mesa/visualization/templates/css/visualization.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ div.tooltip {
border-radius: 8px;
pointer-events: none;
}

canvas.world-grid {
position:absolute;
top:50px;
left:15px;
border:1px dotted
}

canvas.chartjs-render-monitor {
position: absolute;
top: 560px;
}
13 changes: 8 additions & 5 deletions mesa/visualization/templates/js/CanvasHexModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ var CanvasHexModule = function(canvas_width, canvas_height, grid_width, grid_hei
// Create the element
// ------------------

// Create the tag:
var canvas_tag = "<canvas width='" + canvas_width + "' height='" + canvas_height + "' ";
canvas_tag += "style='border:1px dotted'></canvas>";
// Create the tag with absolute positioning :
var canvas_tag = `<canvas width="${canvas_width}" height="${canvas_height}" class="world-grid"/>`
// Append it to body:
var canvas = $(canvas_tag)[0];
var interaction_canvas = $(canvas_tag)[0];
//$("body").append(canvas);
$("#elements").append(canvas);
$("#elements").append(interaction_canvas);

// Create the context and the drawing controller:
var context = canvas.getContext("2d");

var canvasDraw = new HexVisualization(canvas_width, canvas_height, grid_width, grid_height, context);
var interactionHandler = new InteractionHandler(canvas_width, canvas_height, grid_width, grid_height, interaction_canvas.getContext("2d"));

var canvasDraw = new HexVisualization(canvas_width, canvas_height, grid_width, grid_height, context, interactionHandler);

this.render = function(data) {
canvasDraw.resetCanvas();
for (var layer in data)
canvasDraw.drawLayer(data[layer]);
// canvasDraw.drawGridLines("#eee");
canvasDraw.drawGridLines("#eee");
};

this.reset = function() {
Expand Down
17 changes: 12 additions & 5 deletions mesa/visualization/templates/js/CanvasModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ var CanvasModule = function(canvas_width, canvas_height, grid_width, grid_height
// Create the element
// ------------------

// Create the tag:
var canvas_tag = "<canvas width='" + canvas_width + "' height='" + canvas_height + "' ";
canvas_tag += "style='border:1px dotted'></canvas>";
// Create the tag with absolute positioning :
var canvas_tag = `<canvas width="${canvas_width}" height="${canvas_height}" class="world-grid"/>`


// Append it to body:
var canvas = $(canvas_tag)[0];
var interaction_canvas = $(canvas_tag)[0];

//$("body").append(canvas);
$("#elements").append(canvas);
$("#elements").append(interaction_canvas);

// Create the context and the drawing controller:
// Create the context for the agents and interactions and the drawing controller:
var context = canvas.getContext("2d");
var canvasDraw = new GridVisualization(canvas_width, canvas_height, grid_width, grid_height, context);

// Create an interaction handler using the
var interactionHandler = new InteractionHandler(canvas_width, canvas_height, grid_width, grid_height, interaction_canvas.getContext("2d"));
var canvasDraw = new GridVisualization(canvas_width, canvas_height, grid_width, grid_height, context, interactionHandler);

this.render = function(data) {
canvasDraw.resetCanvas();
Expand Down
10 changes: 9 additions & 1 deletion mesa/visualization/templates/js/GridDraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ other agent locations, represented by circles:
*/

var GridVisualization = function(width, height, gridWidth, gridHeight, context) {
var GridVisualization = function(width, height, gridWidth, gridHeight, context, interactionHandler) {

// Find cell size:
var cellWidth = Math.floor(width / gridWidth);
Expand All @@ -50,6 +50,9 @@ var GridVisualization = function(width, height, gridWidth, gridHeight, context)

// Calls the appropriate shape(agent)
this.drawLayer = function(portrayalLayer) {
// Re-initialize the lookup table
(interactionHandler) ? interactionHandler.mouseoverLookupTable.init() : null

for (var i in portrayalLayer) {
var p = portrayalLayer[i];

Expand All @@ -63,6 +66,9 @@ var GridVisualization = function(width, height, gridWidth, gridHeight, context)
// normally keep y-axis in plots from bottom to top.
p.y = gridHeight - p.y - 1;

// if a handler exists, add coordinates for the portrayalLayer index
(interactionHandler) ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i) : null;

// If the stroke color is not defined, then the first color in the colors array is the stroke color.
if (!p.stroke_color)
p.stroke_color = p.Color[0]
Expand All @@ -76,6 +82,8 @@ var GridVisualization = function(width, height, gridWidth, gridHeight, context)
else
this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color)
}
// if a handler exists, update its mouse listeners with the new data
(interactionHandler) ? interactionHandler.updateMouseListeners(portrayalLayer): null;
};

// DRAWING METHODS
Expand Down
49 changes: 35 additions & 14 deletions mesa/visualization/templates/js/HexDraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ other agent locations, represented by circles:
*/

var HexVisualization = function(width, height, gridWidth, gridHeight, context) {
var HexVisualization = function(width, height, gridWidth, gridHeight, context, interactionHandler) {

// Find cell size:
var cellWidth = Math.floor(width / gridWidth);
Expand All @@ -48,14 +48,23 @@ var HexVisualization = function(width, height, gridWidth, gridHeight, context) {
// cell of the grid.
var maxR = Math.min(cellHeight, cellWidth)/2 - 1;

// Configure the interaction handler to use a hex coordinate mapper
(interactionHandler) ? interactionHandler.setCoordinateMapper("hex") : null;

// Calls the appropriate shape(agent)
this.drawLayer = function(portrayalLayer) {
// Re-initialize the lookup table
(interactionHandler) ? interactionHandler.mouseoverLookupTable.init() : null
for (var i in portrayalLayer) {
var p = portrayalLayer[i];
// Does the inversion of y positioning because of html5
// canvas y direction is from top to bottom. But we
// normally keep y-axis in plots from bottom to top.
p.y = gridHeight - p.y - 1;

// if a handler exists, add coordinates for the portrayalLayer index
(interactionHandler) ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i) : null;

if (p.Shape == "hex")
this.drawHex(p.x, p.y, p.r, p.Color, p.Filled, p.text, p.text_color);
else if (p.Shape == "circle")
Expand All @@ -65,6 +74,8 @@ var HexVisualization = function(width, height, gridWidth, gridHeight, context) {
else
this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color)
}
// if a handler exists, update its mouse listeners with the new data
(interactionHandler) ? interactionHandler.updateMouseListeners(portrayalLayer): null;
};

// DRAWING METHODS
Expand Down Expand Up @@ -202,21 +213,31 @@ var HexVisualization = function(width, height, gridWidth, gridHeight, context) {
Draw Grid lines in the full gird
*/

this.drawGridLines = function() {
this.drawGridLines = function(strokeColor) {
context.beginPath();
context.strokeStyle = "#eee";
maxX = cellWidth * gridWidth;
maxY = cellHeight * gridHeight;

// Draw horizontal grid lines:
for(var y=0; y<=maxY; y+=cellHeight) {
context.moveTo(0, y+0.5);
context.lineTo(maxX, y+0.5);
}
context.strokeStyle = strokeColor || "#eee";
const maxX = cellWidth * gridWidth;
const maxY = cellHeight * gridHeight;

const xStep = cellWidth * 0.33;
const yStep = cellHeight * 0.5;

var yStart = yStep;
for(var x=cellWidth/2; x<=maxX; x+= cellWidth) {
for(var y=yStart; y<=maxY; y+=cellHeight) {

context.moveTo(x - 2 * xStep, y);

context.lineTo(x - xStep, y - yStep)
context.lineTo(x + xStep, y - yStep)
context.lineTo(x + 2 * xStep, y )

context.lineTo(x + xStep, y + yStep )
context.lineTo(x - xStep, y + yStep )
context.lineTo(x - 2 * xStep, y)

for(var x=0; x<=maxX; x+= cellWidth) {
context.moveTo(x+0.5, 0);
context.lineTo(x+0.5, maxY);
}
yStart = (yStart === 0) ? yStep: 0;
}

context.stroke();
Expand Down
165 changes: 165 additions & 0 deletions mesa/visualization/templates/js/InteractionHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@

/**
Mesa Visualization InteractionHandler
====================================================================
This uses the context of an additional canvas laid overtop of another canvas
visualization and maps mouse movements to agent position, displaying any agent
attributes included in the portrayal that are not listed in the ignoredFeatures.
The following portrayal will yield tooltips with wealth, id, and pos:
portrayal = {
"Shape": "circle",
"Filled": "true",
"Layer": 0,
"Color": colors[agent.wealth] if agent.wealth < len(colors) else '#a0a',
"r": 0.3 + 0.1 * agent.wealth,
"wealth": agent.wealth,
"id": agent.unique_id,
'pos': agent.pos
}
**/

var InteractionHandler = function(width, height, gridWidth, gridHeight, ctx){

// Find cell size:
const cellWidth = Math.floor(width / gridWidth);
const cellHeight = Math.floor(height / gridHeight);

const lineHeight = 10;

// list of standard rendering features to ignore (and key-values in the portrayal will be added )
const ignoredFeatures = [
'Shape',
'Filled',
'Color',
'r',
'x',
'y',
'w',
'h',
'width',
'height',
'heading_x',
'heading_y',
'stroke_color',
'text_color'
];

// Set a variable to hold the lookup table and make it accessible to draw scripts
var mouseoverLookupTable = this.mouseoverLookupTable = buildLookupTable(gridWidth, gridHeight);
function buildLookupTable(gridWidth, gridHeight){
var lookupTable;
this.init = function(){
lookupTable = [...Array(gridHeight).keys()].map(i => Array(gridWidth));
}

this.set = function(x, y, value){
if(lookupTable[y][x])
lookupTable[y][x].push(value);
else
lookupTable[y][x] = [value];
}

this.get = function(x, y){
if(lookupTable[y])
return lookupTable[y][x] || []
return [];
}

return this;
}

var coordinateMapper;
this.setCoordinateMapper = function(mapperName){
if(mapperName === "hex"){
coordinateMapper = function(event){
const x = Math.floor(event.offsetX/cellWidth);
const y = (x % 2 === 0)
? Math.floor(event.offsetY/cellHeight)
: Math.floor((event.offsetY - cellHeight/2 )/cellHeight)
return {x: x, y: y};
}
return;
}

// default coordinate mapper for grids
coordinateMapper = function(event){
return {
x: Math.floor(event.offsetX/cellWidth),
y: Math.floor(event.offsetY/cellHeight)
};
};
};

this.setCoordinateMapper('grid');


// wrap the rect styling in a function
function drawTooltipBox(ctx, x, y, width, height){
ctx.fillStyle = "#F0F0F0";
ctx.beginPath();
ctx.shadowOffsetX = -3;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 6;
ctx.shadowColor = "#33333377";
ctx.rect(x, y, width, height);
ctx.fill();
ctx.shadowColor = "transparent";
}

var listener; var tmp
this.updateMouseListeners = function(portrayalLayer){tmp = portrayalLayer

// Remove the prior event listener to avoid creating a new one every step
ctx.canvas.removeEventListener("mousemove", listener);

// define the event litser for this step
listener = function(event){
// clear the previous interaction
ctx.clearRect(0, 0, width, height);

// map the event to x,y coordinates
const position = coordinateMapper(event);
const yPosition = Math.floor(event.offsetY/cellHeight);
const xPosition = Math.floor(event.offsetX/cellWidth);

// look up the portrayal items the coordinates refer to and draw a tooltip
mouseoverLookupTable.get(position.x, position.y).forEach((portrayalIndex, nthAgent) => {
const agent = portrayalLayer[portrayalIndex];
const features = Object.keys(agent).filter(k => ignoredFeatures.indexOf(k) < 0);
const textWidth = Math.max.apply(null, features.map(k => ctx.measureText(`${k}: ${agent[k]}`).width));
const textHeight = features.length * lineHeight;
const y = Math.max(lineHeight * 2, Math.min(height - textHeight, event.offsetY - textHeight/2));
const rectMargin = 2 * lineHeight;
var x = 0;
var rectX = 0;

if(event.offsetX < width/2){
x = event.offsetX + rectMargin + nthAgent * (textWidth + rectMargin);
ctx.textAlign = "left";
rectX = x - rectMargin/2;
} else {
x = event.offsetX - rectMargin - nthAgent * (textWidth + rectMargin + lineHeight );
ctx.textAlign = "right";
rectX = x - textWidth - rectMargin/2;
}

// draw a background box
drawTooltipBox(ctx, rectX, y - rectMargin, textWidth + rectMargin, textHeight + rectMargin);

// set the color and draw the text
ctx.fillStyle = "black";
features.forEach((k,i) => {
ctx.fillText(`${k}: ${agent[k]}`, x, y + i * lineHeight)
})
})

};
ctx.canvas.addEventListener("mousemove", listener);
};

return this;
}

0 comments on commit c40c418

Please sign in to comment.