pgadmin4/web/pgadmin/misc/static/explain/js/explain.js
Murtuza Zabuawala 97760d65c2 Add keyboard navigation in Query tool module via Tab/Shift-Tab key. Fixes #2896
Note:
1) Once the keyboard shortcut infrastructure is ready we will add generic shortcut to focus out from CodeMirror editor and set foucs to next element, Right now there is no way of doing this, For testing purpose you can manually focus out from CodeMirror and click on data output panel to continue navigate using Tab key.
2) As of now inner panel's are not getting focused on Tab/Shift-Tab keys but once RM#2895 patch gets committed it will start working automatically as it's inherited code which will add tabindex tag automatically on each newly created wcDocker panel.
2018-01-25 12:27:13 +00:00

895 lines
25 KiB
JavaScript

define('pgadmin.misc.explain', [
'sources/url_for', 'jquery', 'underscore', 'underscore.string',
'sources/pgadmin', 'backbone', 'snapsvg',
], function(url_for, $, _, S, pgAdmin, Backbone, Snap) {
pgAdmin = pgAdmin || window.pgAdmin || {};
// Snap.svg plug-in to write multitext as image name
Snap.plugin(function(Snap, Element, Paper) {
Paper.prototype.multitext = function(x, y, txt, max_width, attributes) {
var svg = Snap(),
abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
temp = svg.text(0, 0, abc);
temp.attr(attributes);
/*
* Find letter width in pixels and
* index from where the text should be broken
*/
var letter_width = temp.getBBox().width / abc.length,
word_break_index = Math.round((max_width / letter_width)) - 1;
svg.remove();
var words = txt.split(' '),
width_so_far = 0,
lines = [],
curr_line = '',
/*
* Function to divide string into multiple lines
* and store them in an array if it size crosses
* the max-width boundary.
*/
splitTextInMultiLine = function(leading, so_far, line) {
var l = line.length,
res = [];
if (l == 0)
return res;
if (so_far && (so_far + (l * letter_width) > max_width)) {
res.push(leading);
res = res.concat(splitTextInMultiLine('', 0, line));
} else if (so_far) {
res.push(leading + ' ' + line);
} else {
if (leading)
res.push(leading);
if (line.length > word_break_index + 1)
res.push(line.slice(0, word_break_index) + '-');
else
res.push(line);
res = res.concat(splitTextInMultiLine('', 0, line.slice(word_break_index)));
}
return res;
};
for (var i = 0; i < words.length; i++) {
var tmpArr = splitTextInMultiLine(
curr_line, width_so_far, words[i]
);
if (curr_line) {
lines = lines.slice(0, lines.length - 1);
}
lines = lines.concat(tmpArr);
curr_line = lines[lines.length - 1];
width_so_far = (curr_line.length * letter_width);
}
// Create multiple tspan for each string in array
var t = this.text(x, y, lines).attr(attributes);
t.selectAll('tspan:nth-child(n+2)').attr({
dy: '1.2em',
x: x,
});
return t;
};
});
if (pgAdmin.Explain)
return pgAdmin.Explain;
var pgExplain = pgAdmin.Explain = {
// Prefix path where images are stored
prefix: url_for('misc.index') + 'static/explain/img/',
};
/*
* A map which is used to fetch the image to be drawn and
* text which will appear below it
*/
var imageMapper = {
'Aggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Aggregate',
},
'Append': {
'image': 'ex_append.svg',
'image_text': 'Append',
},
'Bitmap Index Scan': function(data) {
return {
'image': 'ex_bmp_index.svg',
'image_text': data['Index Name'],
};
},
'Bitmap Heap Scan': function(data) {
return {
'image': 'ex_bmp_heap.svg',
'image_text': data['Relation Name'],
};
},
'BitmapAnd': {
'image': 'ex_bmp_and.svg',
'image_text': 'Bitmap AND',
},
'BitmapOr': {
'image': 'ex_bmp_or.svg',
'image_text': 'Bitmap OR',
},
'CTE Scan': {
'image': 'ex_cte_scan.svg',
'image_text': 'CTE Scan',
},
'Function Scan': {
'image': 'ex_result.svg',
'image_text': 'Function Scan',
},
'Foreign Scan': {
'image': 'ex_foreign_scan.svg',
'image_text': 'Foreign Scan',
},
'Gather': {
'image': 'ex_gather_motion.svg',
'image_text': 'Gather',
},
'Group': {
'image': 'ex_group.svg',
'image_text': 'Group',
},
'GroupAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Group Aggregate',
},
'Hash': {
'image': 'ex_hash.svg',
'image_text': 'Hash',
},
'Hash Join': function(data) {
if (!data['Join Type']) return {
'image': 'ex_join.svg',
'image_text': 'Join',
};
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_hash_anti_join.svg',
'image_text': 'Hash Anti Join',
};
case 'Semi':
return {
'image': 'ex_hash_semi_join.svg',
'image_text': 'Hash Semi Join',
};
default:
return {
'image': 'ex_hash.svg',
'image_text': String('Hash ' + data['Join Type'] + ' Join'),
};
}
},
'HashAggregate': {
'image': 'ex_aggregate.svg',
'image_text': 'Hash Aggregate',
},
'Index Only Scan': function(data) {
return {
'image': 'ex_index_only_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan': function(data) {
return {
'image': 'ex_index_scan.svg',
'image_text': data['Index Name'],
};
},
'Index Scan Backword': {
'image': 'ex_index_scan.svg',
'image_text': 'Index Backward Scan',
},
'Limit': {
'image': 'ex_limit.svg',
'image_text': 'Limit',
},
'LockRows': {
'image': 'ex_lock_rows.svg',
'image_text': 'Lock Rows',
},
'Materialize': {
'image': 'ex_materialize.svg',
'image_text': 'Materialize',
},
'Merge Append': {
'image': 'ex_merge_append.svg',
'image_text': 'Merge Append',
},
'Merge Join': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_merge_anti_join.svg',
'image_text': 'Merge Anti Join',
};
case 'Semi':
return {
'image': 'ex_merge_semi_join.svg',
'image_text': 'Merge Semi Join',
};
default:
return {
'image': 'ex_merge.svg',
'image_text': String('Merge ' + data['Join Type'] + ' Join'),
};
}
},
'ModifyTable': function(data) {
switch (data['Operation']) {
case 'Insert':
return {
'image': 'ex_insert.svg',
'image_text': 'Insert',
};
case 'Update':
return {
'image': 'ex_update.svg',
'image_text': 'Update',
};
case 'Delete':
return {
'image': 'ex_delete.svg',
'image_text': 'Delete',
};
}
},
'Nested Loop': function(data) {
switch (data['Join Type']) {
case 'Anti':
return {
'image': 'ex_nested_loop_anti_join.svg',
'image_text': 'Nested Loop Anti Join',
};
case 'Semi':
return {
'image': 'ex_nested_loop_semi_join.svg',
'image_text': 'Nested Loop Semi Join',
};
default:
return {
'image': 'ex_nested.svg',
'image_text': 'Nested Loop ' + data['Join Type'] + ' Join',
};
}
},
'Recursive Union': {
'image': 'ex_recursive_union.svg',
'image_text': 'Recursive Union',
},
'Result': {
'image': 'ex_result.svg',
'image_text': 'Result',
},
'Sample Scan': {
'image': 'ex_scan.svg',
'image_text': 'Sample Scan',
},
'Scan': {
'image': 'ex_scan.svg',
'image_text': 'Scan',
},
'Seek': {
'image': 'ex_seek.svg',
'image_text': 'Seek',
},
'SetOp': function(data) {
var strategy = data['Strategy'],
command = data['Command'];
if (strategy == 'Hashed') {
if (S.startsWith(command, 'Intersect')) {
if (command == 'Intersect All')
return {
'image': 'ex_hash_setop_intersect_all.svg',
'image_text': 'Hashed Intersect All',
};
return {
'image': 'ex_hash_setop_intersect.svg',
'image_text': 'Hashed Intersect',
};
} else if (S.startsWith(command, 'Except')) {
if (command == 'Except All')
return {
'image': 'ex_hash_setop_except_all.svg',
'image_text': 'Hashed Except All',
};
return {
'image': 'ex_hash_setop_except.svg',
'image_text': 'Hash Except',
};
}
return {
'image': 'ex_hash_setop_unknown.svg',
'image_text': 'Hashed SetOp Unknown',
};
}
return {
'image': 'ex_setop.svg',
'image_text': 'SetOp',
};
},
'Seq Scan': function(data) {
return {
'image': 'ex_scan.svg',
'image_text': data['Relation Name'],
};
},
'Subquery Scan': {
'image': 'ex_subplan.svg',
'image_text': 'SubQuery Scan',
},
'Sort': {
'image': 'ex_sort.svg',
'image_text': 'Sort',
},
'Tid Scan': {
'image': 'ex_tid_scan.svg',
'image_text': 'Tid Scan',
},
'Unique': {
'image': 'ex_unique.svg',
'image_text': 'Unique',
},
'Values Scan': {
'image': 'ex_values_scan.svg',
'image_text': 'Values Scan',
},
'WindowAgg': {
'image': 'ex_window_aggregate.svg',
'image_text': 'Window Aggregate',
},
'WorkTable Scan': {
'image': 'ex_worktable_scan.svg',
'image_text': 'WorkTable Scan',
},
'Undefined': {
'image': 'ex_unknown.svg',
'image_text': 'Undefined',
},
};
// Some predefined constants used to calculate image location and its border
var pWIDTH = 100.;
var pHEIGHT = 100.;
var IMAGE_WIDTH = 50;
var IMAGE_HEIGHT = 50;
var offsetX = 200,
offsetY = 60;
var ARROW_WIDTH = 10,
ARROW_HEIGHT = 10,
DEFAULT_ARROW_SIZE = 2;
var TXT_ALIGN = 5,
TXT_SIZE = '15px';
var xMargin = 25,
yMargin = 25;
var MIN_ZOOM_FACTOR = 0.01,
MAX_ZOOM_FACTOR = 2,
INIT_ZOOM_FACTOR = 1;
var ZOOM_RATIO = 0.05;
// Backbone model for each plan property of input JSON object
var PlanModel = Backbone.Model.extend({
defaults: {
'Plans': [],
level: [],
'image': undefined,
'image_text': undefined,
xpos: undefined,
ypos: undefined,
width: pWIDTH,
height: pHEIGHT,
},
parse: function(data) {
var idx = 1,
lvl = data.level = data.level || [idx],
plans = [],
node_type = data['Node Type'],
// Calculating relative xpos of current node from top node
xpos = data.xpos = data.xpos - pWIDTH,
// Calculating relative ypos of current node from top node
ypos = data.ypos,
maxChildWidth = 0;
data['width'] = pWIDTH;
data['height'] = pHEIGHT;
/*
* calculating xpos, ypos, width and height if current node is a subplan
*/
if (data['Parent Relationship'] === 'SubPlan') {
data['width'] += (xMargin * 2) + (xMargin / 2);
data['height'] += (yMargin * 2);
data['ypos'] += yMargin;
xpos -= xMargin;
ypos += yMargin;
}
if (S.startsWith(node_type, '(slice'))
node_type = node_type.substring(0, 7);
// Get the image information for current node
var mappedImage = (_.isFunction(imageMapper[node_type]) &&
imageMapper[node_type].apply(undefined, [data])) ||
imageMapper[node_type] || {
'image': 'ex_unknown.svg',
'image_text': node_type,
};
data['image'] = mappedImage['image'];
data['image_text'] = mappedImage['image_text'];
// Start calculating xpos, ypos, width and height for child plans if any
if ('Plans' in data) {
data['width'] += offsetX;
_.each(data['Plans'], function(p) {
var level = _.clone(lvl),
plan = new PlanModel();
level.push(idx);
plan.set(plan.parse(_.extend(
p, {
'level': level,
xpos: xpos - offsetX,
ypos: ypos,
})));
if (maxChildWidth < plan.get('width')) {
maxChildWidth = plan.get('width');
}
var childHeight = plan.get('height');
if (idx !== 1) {
data['height'] = data['height'] + childHeight + offsetY;
} else if (childHeight > data['height']) {
data['height'] = childHeight;
}
ypos += childHeight + offsetY;
plans.push(plan);
idx++;
});
}
// Final Width and Height of current node
data['width'] += maxChildWidth;
data['Plans'] = plans;
return data;
},
/*
* Required to parse and include non-default params of
* plan into backbone model
*/
toJSON: function(non_recursive) {
var res = Backbone.Model.prototype.toJSON.apply(this, arguments);
if (non_recursive) {
delete res['Plans'];
} else {
var plans = [];
_.each(res['Plans'], function(p) {
plans.push(p.toJSON());
});
res['Plans'] = plans;
}
return res;
},
// Draw an arrow to parent node
drawPolyLine: function(g, startX, startY, endX, endY, opts, arrowOpts) {
// Calculate end point of first starting straight line (startx1, starty1)
// Calculate start point of 2nd straight line (endx1, endy1)
var midX1 = startX + ((endX - startX) / 3),
midX2 = startX + (2 * ((endX - startX) / 3));
//create arrow head
var arrow = g.polygon(
[0, ARROW_HEIGHT,
(ARROW_WIDTH / 2), ARROW_HEIGHT,
(ARROW_HEIGHT / 4), 0,
0, ARROW_WIDTH,
]
).transform('r90');
var marker = arrow.marker(
0, 0, ARROW_WIDTH, ARROW_HEIGHT, 0, (ARROW_WIDTH / 2)
).attr(arrowOpts);
// First straight line
g.line(
startX, startY, midX1, startY
).attr(opts);
// Diagonal line
g.line(
midX1 - 1, startY, midX2, endY
).attr(opts);
// Last straight line
var line = g.line(
midX2, endY, endX, endY
).attr(opts);
line.attr({
markerEnd: marker,
});
},
// Draw image, its name and its tooltip
draw: function(s, xpos, ypos, pXpos, pYpos, graphContainer, toolTipContainer) {
var g = s.g();
var currentXpos = xpos + this.get('xpos'),
currentYpos = ypos + this.get('ypos'),
isSubPlan = (this.get('Parent Relationship') === 'SubPlan');
// Draw the subplan rectangle
if (isSubPlan) {
g.rect(
currentXpos - this.get('width') + pWIDTH + xMargin,
currentYpos - this.get('height') + pHEIGHT + yMargin - TXT_ALIGN,
this.get('width') - xMargin,
this.get('height') + (currentYpos - yMargin),
5
).attr({
stroke: '#444444',
'strokeWidth': 1.2,
fill: 'gray',
fillOpacity: 0.2,
});
// Provide subplan name
g.text(
currentXpos + pWIDTH - (this.get('width') / 2) - xMargin,
currentYpos + pHEIGHT - (this.get('height') / 2) - yMargin,
this.get('Subplan Name')
).attr({
fontSize: TXT_SIZE,
'text-anchor': 'start',
fill: 'red',
});
}
// Draw the actual image for current node
var image = g.image(
pgExplain.prefix + this.get('image'),
currentXpos + (pWIDTH - IMAGE_WIDTH) / 2,
currentYpos + (pHEIGHT - IMAGE_HEIGHT) / 2,
IMAGE_WIDTH,
IMAGE_HEIGHT
);
// Draw tooltip
var image_data = this.toJSON();
image.mouseover(function() {
// Empty the tooltip content if it has any and add new data
toolTipContainer.empty();
var tooltip = $('<table></table>', {
class: 'pgadmin-tooltip-table',
}).appendTo(toolTipContainer);
_.each(image_data, function(value, key) {
if (key !== 'image' && key !== 'Plans' &&
key !== 'level' && key !== 'image' &&
key !== 'image_text' && key !== 'xpos' &&
key !== 'ypos' && key !== 'width' &&
key !== 'height') {
tooltip.append('<tr><td class="label explain-tooltip">' + key + '</td><td class="label explain-tooltip-val">' + value + '</td></tr>');
}
});
var zoomFactor = graphContainer.data('zoom-factor');
// Calculate co-ordinates for tooltip
var toolTipX = ((currentXpos + pWIDTH) * zoomFactor - graphContainer.scrollLeft());
var toolTipY = ((currentYpos + pHEIGHT) * zoomFactor - graphContainer.scrollTop());
// Recalculate x.y if tooltip is going out of screen
if (graphContainer.width() < (toolTipX + toolTipContainer[0].clientWidth))
toolTipX -= (toolTipContainer[0].clientWidth + (pWIDTH * zoomFactor));
//if(document.children[0].clientHeight < (toolTipY + toolTipContainer[0].clientHeight))
if (graphContainer.height() < (toolTipY + toolTipContainer[0].clientHeight))
toolTipY -= (toolTipContainer[0].clientHeight + ((pHEIGHT / 2) * zoomFactor));
toolTipX = toolTipX < 0 ? 0 : (toolTipX);
toolTipY = toolTipY < 0 ? 0 : (toolTipY);
// Show toolTip at respective x,y coordinates
toolTipContainer.css({
'opacity': '0.8',
});
toolTipContainer.css('left', toolTipX);
toolTipContainer.css('top', toolTipY);
});
// Remove tooltip when mouse is out from node's area
image.mouseout(function() {
toolTipContainer.empty();
toolTipContainer.css({
'opacity': '0',
});
toolTipContainer.css('left', 0);
toolTipContainer.css('top', 0);
});
// Draw text below the node
var node_label = this.get('Schema') == undefined ?
this.get('image_text') :
(this.get('Schema') + '.' + this.get('image_text'));
g.multitext(
currentXpos + (pWIDTH / 2) + TXT_ALIGN,
currentYpos + pHEIGHT - TXT_ALIGN,
node_label,
150, {
'font-size': TXT_SIZE,
'text-anchor': 'middle',
}
);
// Draw Arrow to parent only its not the first node
if (!_.isUndefined(pYpos)) {
var startx = currentXpos + pWIDTH;
var starty = currentYpos + (pHEIGHT / 2);
var endx = pXpos - ARROW_WIDTH;
var endy = pYpos + (pHEIGHT / 2);
var start_cost = this.get('Startup Cost'),
total_cost = this.get('Total Cost');
var arrow_size = DEFAULT_ARROW_SIZE;
// Calculate arrow width according to cost of a particular plan
if (start_cost != undefined && total_cost != undefined) {
arrow_size = Math.round(Math.log((start_cost + total_cost) / 2 + start_cost));
arrow_size = arrow_size < 1 ? 1 : arrow_size > 10 ? 10 : arrow_size;
}
var arrow_view_box = [0, 0, 2 * ARROW_WIDTH, 2 * ARROW_HEIGHT];
var opts = {
stroke: '#000000',
strokeWidth: arrow_size + 1,
},
subplanOpts = {
stroke: '#866486',
strokeWidth: arrow_size + 1,
},
arrowOpts = {
viewBox: arrow_view_box.join(' '),
};
// Draw an arrow from current node to its parent
this.drawPolyLine(
g, startx, starty, endx, endy,
isSubPlan ? subplanOpts : opts, arrowOpts
);
}
var plans = this.get('Plans');
// Draw nodes for current plan's children
_.each(plans, function(p) {
p.draw(s, xpos, ypos, currentXpos, currentYpos, graphContainer, toolTipContainer);
});
},
});
// Main backbone model to store JSON object
var MainPlanModel = Backbone.Model.extend({
defaults: {
'Plan': undefined,
xpos: 0,
ypos: 0,
},
initialize: function() {
this.set('Plan', new PlanModel());
},
// Parse the JSON data and fetch its children plans
parse: function(data) {
if (data && 'Plan' in data) {
var plan = this.get('Plan');
plan.set(
plan.parse(
_.extend(
data['Plan'], {
xpos: 0,
ypos: 0,
})));
data['xpos'] = 0;
data['ypos'] = 0;
data['width'] = plan.get('width') + (xMargin * 2);
data['height'] = plan.get('height') + (yMargin * 4);
delete data['Plan'];
}
return data;
},
toJSON: function() {
var res = Backbone.Model.prototype.toJSON.apply(this, arguments);
if (res.Plan) {
res.Plan = res.Plan.toJSON();
}
return res;
},
draw: function(s, xpos, ypos, graphContainer, toolTipContainer) {
var g = s.g();
//draw the border
g.rect(
0, 0, this.get('width') - 10, this.get('height') - 10, 5
).attr({
fill: '#FFF',
});
var plan = this.get('Plan');
// Draw explain graph
plan.draw(
g, xpos, ypos, undefined, undefined, graphContainer, toolTipContainer
);
},
});
// Parse and draw full graphical explain
_.extend(pgExplain, {
// Assumption container is a jQuery object
DrawJSONPlan: function(container, plan) {
container.empty();
var curr_zoom_factor = 1.0;
var zoomArea = $('<div></div>', {
class: 'pg-explain-zoom-area btn-group',
role: 'group',
}).appendTo(container),
zoomInBtn = $('<button></button>', {
class: 'btn pg-explain-zoom-btn badge',
title: 'Zoom in',
tabindex: 0,
}).appendTo(zoomArea).append(
$('<i></i>', {
class: 'fa fa-search-plus',
})),
zoomToNormal = $('<button></button>', {
class: 'btn pg-explain-zoom-btn badge',
title: 'Zoom to original',
tabindex: 0,
}).appendTo(zoomArea).append(
$('<i></i>', {
class: 'fa fa-arrows-alt',
})),
zoomOutBtn = $('<button></button>', {
class: 'btn pg-explain-zoom-btn badge',
title: 'Zoom out',
tabindex: 0,
}).appendTo(zoomArea).append(
$('<i></i>', {
class: 'fa fa-search-minus',
}));
// Main div to be drawn all images on
var planDiv = $('<div></div>', {
class: 'pgadmin-explain-container',
}).appendTo(container),
// Div to draw tool-tip on
toolTip = $('<div></div>', {
id: 'toolTip',
class: 'pgadmin-explain-tooltip',
}).appendTo(container);
toolTip.empty();
planDiv.data('zoom-factor', curr_zoom_factor);
var w = 0,
h = yMargin;
_.each(plan, function(p) {
var main_plan = new MainPlanModel();
// Parse JSON data to backbone model
main_plan.set(main_plan.parse(p));
w = main_plan.get('width');
h = main_plan.get('height');
var s = Snap(w, h),
$svg = $(s.node).detach();
planDiv.append($svg);
main_plan.draw(s, w - xMargin, yMargin, planDiv, toolTip);
var initPanelWidth = planDiv.width();
/*
* Scale graph in case its width is bigger than panel width
* in which the graph is displayed
*/
if (initPanelWidth < w) {
var width_ratio = initPanelWidth / w;
curr_zoom_factor = width_ratio;
curr_zoom_factor = curr_zoom_factor < MIN_ZOOM_FACTOR ? MIN_ZOOM_FACTOR : curr_zoom_factor;
curr_zoom_factor = curr_zoom_factor > INIT_ZOOM_FACTOR ? INIT_ZOOM_FACTOR : curr_zoom_factor;
var zoomInMatrix = new Snap.matrix();
zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
$svg.find('g').first().attr({
transform: zoomInMatrix,
});
$svg.attr({
'width': w * curr_zoom_factor,
'height': h * curr_zoom_factor,
});
planDiv.data('zoom-factor', curr_zoom_factor);
}
zoomInBtn.on('click', function() {
curr_zoom_factor = ((curr_zoom_factor + ZOOM_RATIO) > MAX_ZOOM_FACTOR) ? MAX_ZOOM_FACTOR : (curr_zoom_factor + ZOOM_RATIO);
var zoomInMatrix = new Snap.matrix();
zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
$svg.find('g').first().attr({
transform: zoomInMatrix,
});
$svg.attr({
'width': w * curr_zoom_factor,
'height': h * curr_zoom_factor,
});
planDiv.data('zoom-factor', curr_zoom_factor);
zoomInBtn.blur();
});
zoomOutBtn.on('click', function() {
curr_zoom_factor = ((curr_zoom_factor - ZOOM_RATIO) < MIN_ZOOM_FACTOR) ? MIN_ZOOM_FACTOR : (curr_zoom_factor - ZOOM_RATIO);
var zoomInMatrix = new Snap.matrix();
zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
$svg.find('g').first().attr({
transform: zoomInMatrix,
});
$svg.attr({
'width': w * curr_zoom_factor,
'height': h * curr_zoom_factor,
});
planDiv.data('zoom-factor', curr_zoom_factor);
zoomOutBtn.blur();
});
zoomToNormal.on('click', function() {
curr_zoom_factor = INIT_ZOOM_FACTOR;
var zoomInMatrix = new Snap.matrix();
zoomInMatrix.scale(curr_zoom_factor, curr_zoom_factor);
$svg.find('g').first().attr({
transform: zoomInMatrix,
});
$svg.attr({
'width': w * curr_zoom_factor,
'height': h * curr_zoom_factor,
});
planDiv.data('zoom-factor', curr_zoom_factor);
zoomToNormal.blur();
});
});
},
});
return pgExplain;
});