Skip to content

Instantly share code, notes, and snippets.

@michaelwooley
Last active September 18, 2017 17:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaelwooley/98fc0def4c581fe86f53a4d86fe75504 to your computer and use it in GitHub Desktop.
Save michaelwooley/98fc0def4c581fe86f53a4d86fe75504 to your computer and use it in GitHub Desktop.
Drawing With d3.js (Part 4: Data Extraction)

Source Files: Drawing With d3.js Part 4: Data Extraction

This file contains all of the source files needed to create the examples described in Drawing With d3.js Part 4: Data Extraction.

For more info see the accompanying blog post.

Usage and Controls

  • Draw a rectangle: Click + Drag.
  • Select a rectangle: Click on rectangle.
    • Select multiple rectangles: Ctrl + Click
  • Move a rectangle: Click on rectangle edge + drag. (Moves all selected rectangles.)
  • Resize a rectangle: Click on rectangle corner + drag. (Moves only selected rectangle).
  • Zoom in and out of canvas: Mouse scroll or double click.
  • Pan canvas: Shift + Click + Drag.
  • Change the type/color of rectangle: Dropdown menu.
  • View selections: Submit button.

Files

  • index.html: Main html file.
  • drawing-4-main.js: Javascript file with commands for drawing canvas.
  • button-toggle.js: Javascript file for creating buttons.
  • main.css: CSS style file.
/***********************
Button Toggling
************************/
function getRandInt(magnitude = 6) {
// Get a random integer of specified `magnitude`.
//
// Input:
// - magnitude: Magnitude of random int.
//
// Returns:
// - A random integer of specified magnitude.
return Math.floor(Math.random() * (10 ** magnitude));
}
function ButtonToggle(buttons, options) {
//Toggling Button Group (SVG-based).
//
// Inputs:
// - buttons: (Object) Information about buttons
// to be added. Properties are:
// - name: (String) Name of the passed button. Used as both
// button id and as label of button.
// - color: (String) Color of button. Defaults to colormap.
// - options: (Object) Information about button placement, etc.
// Properties are:
// - type: (String) Type of button group to add. Options are
// ['button', 'dropdown']. (Default='button').
// - addTo: (String) CSS selector of where the button should
// be placed. Should be within an <svg> element. (Default: 'svg')
// - groupName: (String) Name of button group.
// (Defaults = 'buttont-toggle-[6-digit random integer]')
// - clickCall: (function) Function to be called when button
// is clicked. Takes one argument, which is the id (name) of
// the clicked button.
//
// Returns:
// - A ButtonToggle object with methods.
// Add properties
this.buttons = buttons;
this.type = options.type || 'button';
this.addTo = options.addTo || 'svg';
this.groupName = options.groupName || 'btn-group-' + getRandInt();
this.buttonHeight = options.buttonHeight || 25;
this.clickCall = options.clickCall || function () {
null;
};
// Add the buttons
if (this.type == 'dropdown') {
this.addDropdown();
} else {
this.addButtons();
}
}
ButtonToggle.prototype.addButtons = function () {
// Add the buttons to the group
var self = this;
inactiveOpacity = 0.75;
// Create the Button Group
self.g = d3.select(self.addTo)
.append('div')
.attr('class', 'btn-group-vertical ' + this.groupName);
function buttonClick() {
// What should happen when the button is clicked?
// Change active designation
self.g
.selectAll('label.active')
.classed('active', false)
.style('opacity', inactiveOpacity);
var ab = d3.select(this)
.classed('active', true)
.style('opacity', 1);
// Call the callback function
self.clickCall(ab.attr('id'));
}
// Add the buttons
for (var ii in self.buttons) {
self.g
.append('label')
.attr('class', 'btn btn-secondary')
.on('click', buttonClick)
.text(self.buttons[ii].name)
.attr('id', self.buttons[ii].name)
.classed('active', ii == 0)
.style('background-color', self.buttons[ii].color)
.style('opacity', inactiveOpacity + (ii == 0) * (1 - inactiveOpacity))
.style('border-color', 'white')
.style('cursor', 'pointer')
.style('color', 'white')
.style('margin-bottom', '1%')
.style('font', 'caption');
}
}
ButtonToggle.prototype.addDropdown = function () {
// Add as a dropdown element
var self = this;
// Add in div
self.g = d3.select(self.addTo)
.append('div')
.attr('class', 'dropdown ' + self.groupName);
self.b = self.g
.append('button')
.attr('class', 'btn btn-secondary dropdown-toggle')
.attr('type', 'button')
.attr('id', 'dropdownMenuButton')
.attr('data-toggle', 'dropdown')
.attr('aria-haspopup', 'true')
.attr('aria-expanded', 'false')
.text('Type: ' + self.buttons[0].name)
.style('color', self.buttons[0].color)
.style('text-align', 'left')
.style('background-color', 'white')
.style('font', 'caption');
// Add the buttons group
self.folder = self.g
.append('div')
.attr('class', 'dropdown-menu')
.attr('aria-labelledby', 'dropdownMenuButton');
// Add each button
for (var ii in self.buttons) {
self.folder
.append('a')
.attr('class', 'dropdown-item')
.attr('id', self.buttons[ii].name)
.style('opacity', 0.9)
.style('color', self.buttons[ii].color)
.style('cursor', 'pointer')
.style('font', 'caption')
.text(self.buttons[ii].name);
}
self.items = self.folder.selectAll('a.dropdown-item');
// Add the actions
//// Open/Close Top Button
self.b
.style('min-width', self.folder.style('min-width'))
.on('click', function () {
if (self.folder.style('display') == 'none') {
self.folder.style('display', 'block');
} else {
self.folder.style('display', 'none');
}
});
//// What should happen on click
self.items.on('click', function () {
var ab = d3.select(this);
// Change the styling
self.folder.style('display', 'none');
self.b
.style('color', ab.style('color'))
.text('Type: ' + this.text);
// Call the callback function
self.clickCall(ab.attr('id'));
})
}
function getRandInt(magnitude = 6) {
// Get a random integer of specified `magnitude`.
//
// Input:
// - magnitude: Magnitude of random int.
//
// Returns:
// - A random integer of specified magnitude.
return Math.floor(Math.random() * (10 ** magnitude));
}
function SVGCanvas(options) {
/*
* An SVG-based drawing app.
* Input:
* - options: An object consisting of:
* - h: The height of the canvas (default: 250px).
* - w: The width of the canvas (default: 250px).
* - addTo: CSS Selector for element on which to add canvas (default: 'body').
* - addBorderRect: (bool) Add a border around the canvas (default: true).
* - dbWidth: Width of selection borders (default=0.75).
* - debug: (bool) run in debug mode (default=false).
* Returns: An SVG object contained in the `addTo` DOM element.
*/
var self = this;
// Set the global SVG options
this.options = options || {};
this.options.h = options.h || 250;
this.options.w = options.w || 250;
this.options.addTo = options.addTo || 'body';
this.options.addBorderRect = options.addBorderRect || true;
this.options.dbWidth = 0.75;
this.options.debug = options.debug || false;
this.canvasID = 'canvas-' + getRandInt();
// Make A Distinct Container
// Set the state (elaborate on more later)
//// All Possible States
this.stateData = options.stateData || this.loadStateData();
// Present State
this.state = this.stateData[0];
// Add the container
this.addContainer();
// Add the menu
this.addMenu();
// Add the Canvas
this.addCanvas();
// Rectangles
//// Current Selection
this.Rect = {
'r': null,
'g': null,
};
//// Collection
this.Shapes = {}; // Collection
// Transformation state
this.transform = d3.zoomTransform(this.zoomG.node());
// Load methods for behaviors
this.makeAddRect(); // Add Rectangle Methods
this.makeZoomPan(); // SVG Zooming and Panning Methods
this.makeDragBehavior();
// Dragging Behavior - account for both addRect and pan.
this.svg.call(
d3.drag()
.on('start', self.dragBehavior.start)
.on('drag', self.dragBehavior.drag)
.on('end', self.dragBehavior.end)
);
// Zooming behavior
this.svg.call(
d3.zoom()
.scaleExtent([1, 10])
.on('zoom', this.zoomPan.zoom)
)
.on('mousedown.zoom', null)
.on('mousemove.zoom', null)
.on('mouseup.zoom', null)
.on('touchstart.zoom', null)
.on('touchmove.zoom', null)
.on('touchend.zoom', null);
// Keydown events
d3.select('body').on('keydown', this.keydownEventHandlers);
}
SVGCanvas.prototype.makeZoomPan = function () {
// Defines zooming and panning behavior from zoom listener
var self = this;
checkBounds = function () {
// Check whether zooming/panning out of bounds and correct transform if needed.
var svgBB = self.svg.node().getBBox();
// Bottom border
if (((-self.transform.y + svgBB.height) / self.transform.k) > svgBB.height) {
self.transform.y = -(svgBB.height * self.transform.k) + svgBB.height;
}
// Top border
if (((self.transform.y + 0) / self.transform.k) > 0) {
self.transform.y = 0;
}
// Left border
if (((-self.transform.x + svgBB.width) / self.transform.k) > svgBB.width) {
self.transform.x = -(svgBB.width * self.transform.k) + svgBB.width;
}
// Right border
if (((self.transform.x + 0) / self.transform.k) > 0) {
self.transform.x = 0;
}
}
zoom = function () {
self.transform = d3.event.transform;
checkBounds();
self.zoomG.attr('transform', self.transform);
}
var pan = function () {
self.transform.x += d3.event.dx;
self.transform.y += d3.event.dy;
checkBounds();
// Update Attribute
d3.select('g.zoom-group').attr('transform', self.transform);
}
self.zoomPan = {
zoom: zoom,
pan: pan
};
}
SVGCanvas.prototype.mouseOffset = function () {
// Utility function for getting mouse offset
return d3.mouse(this.zoomG.node());
}
SVGCanvas.prototype.makeAddRect = function () {
// Methods for adding rectangles to the svg.
var self = this;
var r, g, x0, y0;
start = function () {
//Add a rectangle
// 1. Get mouse location in SVG
var m = self.mouseOffset();
x0 = m[0];
y0 = m[1];
// 2. Add a new group
g = self.zoomG
.append('g')
.attr('class', 'g-rect ' + self.state.id);
// 3. Make a rectangle
r = g
.append('rect') // An SVG `rect` element
.attr('x', x0) // Position at mouse location
.attr('y', y0)
.attr('width', 1) // Make it tiny
.attr('height', 1)
.attr('class', 'rect-main ' + self.state.class + ' ' + self.state.id)
.style('stroke', self.state.color)
.style('fill', 'none');
// 4. Make it active.
self.setActive(self.state.id);
}
drag = function () {
// What to do when mouse is dragged
// 1. Get the new mouse position
var m = self.mouseOffset();
// 2. Update the attributes of the rectangle
r.attr('x', Math.min(x0, m[0]))
.attr('y', Math.min(y0, m[1]))
.attr('width', Math.abs(x0 - m[0]))
.attr('height', Math.abs(y0 - m[1]));
}
end = function () {
// What to do on mouseup
// Add Rectangle Transformation Methods
self.transformRect(r, g);
// Update count and id
self.state.count += 1;
self.state.id = self.state.name + '-' + self.state.count;
}
self.addRect = {
start: start,
drag: drag,
end: end,
};
}
SVGCanvas.prototype.makeDragBehavior = function () {
var self = this;
var set = false; // Disable retroactive re-fitting
var start = function () {
if (!d3.event.sourceEvent.shiftKey) {
self.addRect.start();
set = true;
}
if (d3.event.sourceEvent.shiftKey) {
null;
}
}
var drag = function () {
if (set && !(d3.event.sourceEvent.shiftKey)) {
self.addRect.drag();
}
if (d3.event.sourceEvent.shiftKey) {
self.zoomPan.pan();
}
}
var end = function () {
if (set &
!(d3.event.sourceEvent.shiftKey)) {
self.addRect.end();
set = false;
}
if (d3.event.sourceEvent.shiftKey) {
null;
}
}
self.dragBehavior = {
start: start,
drag: drag,
end: end
};
}
/**********************************
Dragging and resizing rectangles.
**********************************/
function contains(a, obj) {
// See: https://stackoverflow.com/questions/237104/how-do-i-check-if-an-array-includes-an-object-in-javascript
for (var i = 0; i < a.length; i++) {
if (a[i] === obj) {
return true;
}
}
return false;
}
function clone(selector) {
// Clone a d3 selection.
// Source: https://stackoverflow.com/questions/39477740/copy-and-insert-in-d3-selection
var node = d3.select(selector).node();
return d3.select(node.parentNode.insertBefore(node.cloneNode(true), node.nextSibling));
}
SVGCanvas.prototype.transformRect = function (r, g) {
var self = this;
var groupClass, debug, p, dbWidth;
var main = function () {
dbWidth = self.options.dbWidth;
// Set common class
groupClass = self.state.id;
debug = self.options.debug ? (' debug') : ('');
// Add data to the group element
var rBB = r.node().getBBox();
p = {
x: rBB.x,
y: rBB.y,
w: rBB.width,
h: rBB.height,
id: groupClass,
type: self.state.name,
};
g = g.data([p]);
// Add the hidden bounding rectangles
makeRectEdgeCorner();
}
main();
function setCoordsData(d) {
// Set the coordinates of a rectangle-group
var children = d3.selectAll('g.active.' + d.id);
// Main Rectangle
children.select('rect.rect-main')
.attr('x', d.x)
.attr('y', d.y)
.attr('width', d.w)
.attr('height', d.h);
// rectEdge.left
children.select('rect.rectEdge.rectEdge-left')
.attr('x', d.x - (dbWidth / 2))
.attr('y', d.y + (dbWidth / 2))
.attr('width', dbWidth)
.attr('height', Math.abs(d.h - dbWidth));
// rectEdge.right
children.select('rect.rectEdge.rectEdge-right')
.attr('x', d.x + d.w - (dbWidth / 2))
.attr('y', d.y + (dbWidth / 2))
.attr('width', dbWidth)
.attr('height', Math.abs(d.h - dbWidth));
// rectEdge.top
children.select('rect.rectEdge.rectEdge-top')
.attr('x', d.x + (dbWidth / 2))
.attr('y', d.y - (dbWidth / 2))
.attr('width', Math.abs(d.w - dbWidth))
.attr('height', dbWidth);
// rectEdge.bottom
children.select('rect.rectEdge.rectEdge-bottom')
.attr('x', d.x + (dbWidth / 2))
.attr('y', d.y + d.h - (dbWidth / 2))
.attr('width', Math.abs(d.w - dbWidth))
.attr('height', dbWidth);
// rectCorner.topleft
children.select('rect.rectCorner.rectCorner-topleft')
.attr('x', d.x - (dbWidth / 2))
.attr('y', d.y - (dbWidth / 2));
// rectCorner.topright
children.select('rect.rectCorner.rectCorner-topright')
.attr('x', d.x + d.w - (dbWidth / 2))
.attr('y', d.y - (dbWidth / 2));
// rectCorner.botleft
children.select('rect.rectCorner.rectCorner-botleft')
.attr('x', d.x - (dbWidth / 2))
.attr('y', d.y + d.h - (dbWidth / 2));
// rectCorner.botright
children.select('rect.rectCorner.rectCorner-botright')
.attr('x', d.x + d.w - (dbWidth / 2))
.attr('y', d.y + d.h - (dbWidth / 2))
}
// Add move and resize methods
function moveRect() {
// Move the rectangle by dragging edges
var activeG;
function start() {
self.setActive(groupClass);
self.svg.style('cursor', 'move');
activeG = d3.selectAll('g.active');
}
function drag() {
var svgBB = self.svg.node().getBBox();
activeG.each(
function (d, i) {
// Alter Parameters
d.x = Math.max(0, Math.min(svgBB.width - d.w, d.x + d3.event.dx));
d.y = Math.max(0, Math.min(svgBB.height - d.h, d.y + d3.event.dy));
// Set Coordinates
setCoordsData(d);
}
)
}
function end() {
// Undo formatting
self.svg.style('cursor', 'default');
}
// What to do on drag
var dragcontainer = d3.drag()
.on('start', start)
.on('drag', drag)
.on('end', end);
return {
drag: dragcontainer,
}
}
function resizeRect() {
// Resize the rectangle by dragging the corners
function getDragCorners() {
return {
topleft: function (d, bb0, m) {
var svgBB = self.svg.node().getBBox();
d.x = Math.max(0, Math.min(bb0.x + bb0.width, m[0]));
d.y = Math.max(0, Math.min(bb0.y + bb0.height, m[1]));
d.w = (m[0] > 0) ? Math.min(Math.abs(svgBB.width - d.x), Math.abs(bb0.x + bb0.width - m[0])) : d.w;
d.h = (m[1] > 0) ? Math.min(Math.abs(svgBB.height - d.y), Math.abs(bb0.y + bb0.height - m[1])) : d.h;
},
topright: function (d, bb0, m) {
var svgBB = self.svg.node().getBBox();
d.x = Math.max(0, Math.min(bb0.x, m[0]));
d.y = Math.max(0, Math.min(bb0.y + bb0.height, m[1]));
d.w = (m[0] > 0) ? Math.min(Math.abs(svgBB.width - d.x), Math.abs(bb0.x - m[0])) : d.w;
d.h = (m[1] > 0) ? Math.min(Math.abs(svgBB.height - d.y), Math.abs(bb0.y + bb0.height - m[1])) : d.h;
},
botleft: function (d, bb0, m) {
var svgBB = self.svg.node().getBBox();
d.x = Math.max(0, Math.min(bb0.x + bb0.width, m[0]));
d.y = Math.max(0, Math.min(bb0.y, m[1]));
d.w = (m[0] > 0) ? Math.min(Math.abs(svgBB.width - d.x), Math.abs(bb0.x + bb0.width - m[0])) : d.w;
d.h = (m[1] > 0) ? Math.min(Math.abs(svgBB.height - d.y), Math.abs(bb0.y - m[1])) : d.h;
},
botright: function (d, bb0, m) {
var svgBB = self.svg.node().getBBox();
d.x = Math.max(0, Math.min(bb0.x, m[0]));
d.y = Math.max(0, Math.min(bb0.y, m[1]));
d.w = (m[0] > 0) ? Math.min(Math.abs(svgBB.width - d.x), Math.abs(bb0.x - m[0])) : d.w;
d.h = (m[1] > 0) ? Math.min(Math.abs(svgBB.height - d.y), Math.abs(bb0.y - m[1])) : d.h;
}
};
}
var makeContainer = function (id) {
// Make a container, which depends on the corner (specified by `id`)
var dragCorners, cursor, bb0;
// Get the correct transformation function
dragCorners = getDragCorners()[id];
// Get the correct cursor
if (contains(['topleft', 'botright'], id)) {
cursor = 'nwse-resize';
} else {
cursor = 'nesw-resize';
}
var start = function () {
// Set the present group to be active
self.setActive(groupClass, false);
// Get the active groups
activeG = d3.selectAll('g.active');
// Get the initial Bounding Box
bb0 = r.node().getBBox();
// Display correct cursor tip
self.svg.style('cursor', cursor);
}
var drag = function () {
// Mouse position
m = self.mouseOffset();
// Update parameters depending on
dragCorners(g.datum(), bb0, m);
// Set the coordinates
setCoordsData(g.datum());
}
var end = function () {
// Undo formatting
self.svg.style('cursor', 'default');
}
// return the drag container
return d3.drag()
.on('start', start)
.on('drag', drag)
.on('end', end);
}
// Make drag containers for each
return {
makeContainer: makeContainer,
}
}
// Append helper rectEdges and rectCorners to g
function makeRectEdgeCorner() {
// Adds edges and corners to rectangle for drag move and resize.
// "Prototype" elements
var proto = [
// Rectangular edges
g.append('rect')
.attr('class', 'rectEdge cornerEdge ' + groupClass + debug)
,
// Circular corners - NWSE cursor
g.append('rect')
.attr('height', dbWidth)
.attr('width', dbWidth)
.attr('id', 'topright')
.attr('class', 'rectCorner cornerEdge nwse ' + groupClass + debug)
,
// Circular corners - NESW cursor
g.append('rect')
.attr('height', dbWidth)
.attr('width', dbWidth)
.attr('id', 'topright')
.attr('class', 'rectCorner cornerEdge nesw ' + groupClass + debug)
,
];
// Behaviors to attach to corners and edges
var move = moveRect();
var resize = resizeRect();
// Create Edges
clone('.rectEdge.' + groupClass)
.classed('rectEdge-left', true)
.call(move.drag);
clone('.rectEdge.' + groupClass)
.classed('rectEdge-right', true)
.call(move.drag);
clone('.rectEdge.' + groupClass)
.classed('rectEdge-top', true)
.call(move.drag);
clone('.rectEdge.' + groupClass)
.classed('rectEdge-bottom', true)
.call(move.drag);
// Create Corners
clone('.nwse.' + groupClass)
.classed('rectCorner-topleft', true)
.call(resize.makeContainer('topleft'));
clone('.nesw.' + groupClass)
.classed('rectCorner-topright', true)
.call(resize.makeContainer('topright'));
clone('.nesw.' + groupClass)
.classed('rectCorner-botleft', true)
.call(resize.makeContainer('botleft'));
clone('.nwse.' + groupClass)
.classed('rectCorner-botright', true)
.call(resize.makeContainer('botright'));
// Remove prototype elements from DOM
proto.forEach(function (d, i) {
d.remove();
});
// Format size and shape of added objects.
setCoordsData(g.datum());
}
}
SVGCanvas.prototype.setActive = function (id, force_clear = false) {
// Sets class to active for selected groups.
var deactivate = false;
// When should all other groups be deactivated?
// 1.A If the ctrl key is not pressed
// 1.B If the present element isn't already active
// (Use De Morgan's Rules for this one.)
deactivate = deactivate || !(d3.event.sourceEvent.ctrlKey || d3.selectAll('g.' + id).classed('active'));
// 2. If we didn't force it to be.
deactivate = deactivate || force_clear;
// If any of these conditions met, clear the active elements.
if (deactivate) {
this.svg.selectAll('g.active').classed('active', false);
}
// Add 'active' class to any 'g' element with id = id passed.
d3.selectAll('g.' + id).classed('active', true);
}
SVGCanvas.prototype.keydownEventHandlers = function () {
// Event handler for keydown events
// Press 'Delete' to remove all active groups.
if (d3.event.key === 'Delete') {
d3.selectAll('g.active').remove();
}
}
/*******************
Overall Container
*******************/
SVGCanvas.prototype.addContainer = function () {
var container = d3.select(self.options.addTo)
.append('div')
.attr('id', this.canvasID);
//// Format the bounding containers
////// Containing Row
var row1 = container
.append('div')
.attr('class', 'row')
.style('padding', '0');
////// Column for the menu
row1.append('div')
.attr('class', 'col-10 SVGCanvas-menu')
.style('position', 'static')
.style('text-align', 'center')
.style('min-width', '77px')
.style('padding', 0)
.style('border', function () {
if (self.options.debug) {
return '2px solid grey';
} else {
return '0px none white';
}
});
////// Column for the canvas
var row2 = container
.append('div')
.attr('class', 'row')
.style('padding', '0');
row2.append('div')
.attr('class', 'col-auto SVGCanvas-canvas')
.style('position', 'static').style('border', function () {
if (self.options.debug) {
return '2px solid grey';
} else {
return '0px none white';
}
})
.style('padding', 0);
}
/***************
Menu Elements
***************/
SVGCanvas.prototype.addMenu = function () {
// Adding menu elements
var self = this;
// Add as a table
//// The table
var tab = d3.select(self.options.addTo + ' div.SVGCanvas-menu')
.append('ul')
.attr('class', 'nav nav-pills');
//// Button elements
////// The State Togglers
tab.append('li')
.attr('class', 'nav-item')
.attr('id', 'buttonToggle')
.style('margin', '0.15em');
addStateTogglers(self.options.addTo + ' div.SVGCanvas-menu li#buttonToggle');
////// The Data Submit Button
tab.append('li')
.attr('class', 'nav-item')
.attr('id', 'submitButton')
.style('margin', '0.15em');
addDataSubmitButton(self.options.addTo + ' div.SVGCanvas-menu li#submitButton');
function addStateTogglers(addTo) {
// Add the state toggler controls
// Create the callback function to be called after each click.
function callbackStateToggle(arg) {
// What to do when the state toggle buttons are pushed
// Update the old element
//// Get the index of old element
var idx = self.stateData.findIndex(function (element) {
return element.name == self.state.name;
});
//// Update stateData
self.stateData[idx] = self.state;
// Break off the new state
self.state = self.stateData.find(function (element) {
return element.name == arg;
});
}
//// Set the Options
var stateToggleOpt = {
type: 'dropdown',
addTo: addTo,
clickCall: callbackStateToggle,
};
//// Create the new object
self.stateTogglers = new ButtonToggle(self.stateData, stateToggleOpt);
}
function addDataSubmitButton(addTo) {
// Make a button that will submit the necessary data to the system.
// Add the button
d3.select(addTo)
.append('button')
.attr('class', 'btn btn-dark submit-button')
.style('font', 'caption')
.text('Submit')
.on('click', onclickCallback);
// Define the 'onclick' callback
function onclickCallback() {
var out = self.dataCompile();
console.log(out);
self.previewSelections(out);
}
}
}
SVGCanvas.prototype.dataCompile = function () {
// A function for compiling all of the data on the canvas into a
// json data structure.
//
// FUTURE:
// More metadata
// Accomodate more than one image.
var self = this;
var out = [];
// One file for each image (will have more in future).
//// Initialize object
var out_i = {
meta: {},
bb: {}
};
//// Get image bounding box.
var imgBB = self.img.node().getBBox();
// Get Metadata - Add more later.
//// File name w/ path
out_i.meta.href = self.img.attr('href');
//// File size
out_i.meta.height = imgBB.height;
out_i.meta.width = imgBB.width;
//// Common Name/Identifier
// Get bounding boxes
self.zoomG.selectAll('g.g-rect')
.each(function (d, i) {
// Retrieve rectangle bounding boxes _relative to image_.
// Follows convention from VOC2008:
// http://host.robots.ox.ac.uk/pascal/VOC/voc2008/htmldoc/voc.html#SECTION00092000000000000000
var d2 = {};
d2.xmin = Math.max(d.x - imgBB.x, 0) / self.img.data[0].scale;
d2.ymin = Math.max(d.y - imgBB.y, 0) / self.img.data[0].scale;
d2.xmax = Math.min(d.x - imgBB.x + d.w, imgBB.width) / self.img.data[0].scale;
d2.ymax = Math.min(d.y - imgBB.y + d.h, imgBB.height) / self.img.data[0].scale;
d2.type = d.type;
out_i.bb[d.id] = d2;
});
// Push onto full dataset.
out.push(out_i);
return out;
}
SVGCanvas.prototype.previewSelections = function (d) {
// Make a table to preview the selections made above.
var self = this;
// Load into a new window
//// Open
var w = window.open();
//// Add styling
w.document.write('<html><head><title>Data</title><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"></head><body>');
//// Get the body.
var b = d3.select(w.document.body);
// Create Table and headers
//// Table
var tab = b
.append('div')
.attr('id', 'bb-preview-table')
.append('table')
.attr('class', 'table table-hover table-responsive');
//// Headers
var row = tab.append('thead').append('tr');
row.append('th').text('Selection');
row.append('th').text('Document');
row.append('th').text('ID');
row.append('th').text('Type');
row.append('th').text('xmin');
row.append('th').text('ymin');
row.append('th').text('xmax');
row.append('th').text('ymax');
// Body
var tbody = tab.append('tbody');
// Cycle through output.
for (var ii = 0; ii < d.length; ii++) {
for (var bb in d[ii].bb) {
// Get the data
var d_i = d[ii].bb[bb];
// Set the height and width
var w = (d_i.xmax - d_i.xmin) * self.img.data[0].scale;
var h = (d_i.ymax - d_i.ymin) * self.img.data[0].scale;
// Make the row
var row = tbody.append('tr');
// Append the image
var canvas = row.append('td')
.append('canvas')
.attr('width', w)
.attr('height', h);
var ctx = canvas.node().getContext("2d");
// ~~!!! NOT WORKING ON FIREFOX !!!~~~
ctx.drawImage(self.img.node(),
d_i.xmin, d_i.ymin,
d_i.xmax - d_i.xmin, d_i.ymax - d_i.ymin,
0, 0, w, h);
// Append other info
row.append('td').text(d[ii].meta.href);
row.append('td').text(bb);
row.append('td').text(d_i.type);
row.append('td').text(d_i.xmin.toFixed(2));
row.append('td').text(d_i.ymin.toFixed(2));
row.append('td').text(d_i.xmax.toFixed(2));
row.append('td').text(d_i.ymax.toFixed(2));
}
}
}
/***************
Canvas Elements
***************/
SVGCanvas.prototype.addCanvas = function () {
// Adding the canvas elements
var self = this;
// Make the main container SVG
self.svg = d3.select(self.options.addTo + ' div.SVGCanvas-canvas')
.append('svg')
.attr('height', self.options.h)
.attr('width', self.options.w)
.attr('class', 'display-svg');
// Add border if requested (last to be on top)
if (self.options.addBorderRect) {
self.svg.append('rect')
.attr('height', '100%')
.attr('width', '100%')
.attr('stroke', 'black')
.attr('stroke-width', 4)
.attr('opacity', 0.25)
.attr('fill-opacity', 0.0)
.attr('class', 'border-rect')
.attr('fill', 'none');
}
// Add image to background
self.loadImage('./media/sample_table.png');
// Raise border to top
// (do this last because border enforces canvas size)
self.svg.select('rect.border-rect').raise();
}
SVGCanvas.prototype.loadImage = function (arg) {
// Load an image to the canvas.
var self = this;
//// Add zoom and pan group
self.zoomG = self.svg
.append('g')
.attr('class', 'zoom-group')
//// Adding
self.img = self.zoomG.append('image')
.attr('href', arg)
.attr('width', '98%')
.attr('height', '98%')
.attr('x', '1%')
.attr('y', '1%')
.call(function () {
// Call this function to get size attributes for the
// displayed and actual image.
var image = new Image();
image.onload = function () {
imgBB = self.img.node().getBBox();
var d = {};
d.height = image.naturalHeight;
d.width = image.naturalWidth;
// Get x/y coordinates and scaling:
if (d.height > d.width) {
d.scale = (imgBB.height / d.height);
d.x = (self.options.w - d.scale * d.width) / 2;
d.y = imgBB.y;
} else {
d.scale = (imgBB.width / d.width);
d.x = imgBB.x;
d.y = (self.options.h - d.scale * d.height) / 2;
}
// Reformat image attributes
self.img
.attr('width', d.scale * d.width)
.attr('height', d.scale * d.height)
.attr('x', d.x)
.attr('y', d.y);
// Assign as data
self.img.data = [d];
console.log(d);
}
image.src = arg;
});
};
SVGCanvas.prototype.loadStateData = function () {
return [{
name: 'Table',
color: '#d32f2f',
count: 0,
class: 'rect',
id: 'Table-0',
},
{
name: 'Row',
color: '#303f9f',
count: 0,
class: 'rect',
id: 'Row-0',
},
{
name: 'Column',
color: '#388e3c',
count: 0,
class: 'rect',
id: 'Column-0',
},
{
name: 'Title',
color: '#512da8',
count: 0,
class: 'rect',
id: 'Title-0',
},
{
name: 'Note',
color: '#fbc02d',
count: 0,
class: 'rect',
id: 'Note-0',
},
{
name: 'Number',
color: '#e64a19',
count: 0,
class: 'rect',
id: 'Number-0',
},
{
name: 'Word',
color: '#0288d1',
count: 0,
class: 'rect',
id: 'Word-0',
}];
}
/**********
SETUP
**********/
window.onload = function () {
options = {
h: 500,
w: 500,
addTo: '.sample-div',
addBorderRect: true,
debug: false,
}
var c = new SVGCanvas(options);
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Drawing: Part 4</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<link href="main.css" rel="stylesheet" type="text/css">
<style>
div.sample-div {
position: fixed;
top: 10%;
left: 10%;
width: 90%;
}
</style>
</head>
<body>
<div class="sample-div">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.js"></script>
<script src="button-toggle.js"></script>
<script src="drawing-4-main.js"></script>
</body>
</html>
rect.rect-main {
stroke: #d32f2f;
stroke-width: 2;
fill-opacity: 0;
stroke-opacity: 0.5;
vector-effect: non-scaling-stroke;
}
g.active rect {
stroke-opacity: 1;
}
div.sample-div {
position: block;
}
.rectEdge {
fill: #303f9f;
opacity: 0;
stroke-width: 4;
stroke: #303f9f;
vector-effect: non-scaling-stroke;
}
.rectCorner {
fill: #668d3c;
opacity: 0;
stroke-width: 4;
stroke: #668d3c;
vector-effect: non-scaling-stroke;
}
.rectEdge:hover {
cursor: move;
}
.rectCorner.nwse:hover {
cursor: nwse-resize;
}
.rectCorner.nesw:hover {
cursor: nesw-resize;
}
.cornerEdge.debug {
opacity: 0.5;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment