From 053bf1189cb1a052789822bb1f0d6a63f336c619 Mon Sep 17 00:00:00 2001 From: Fintan McGee Date: Wed, 5 Oct 2016 15:05:25 +0200 Subject: [PATCH] added fisheye zoom functionality per users request. Below each axis there is an eye icon. CLicking on this results in the applciation of a fishe eye lens focuses on the midpoint of the brush(es) on the axis --- .../inst/www/libs/parcoords/d3.parcoords.css | 6 + .../inst/www/libs/parcoords/d3.parcoords.js | 157 +++++++++++++++++- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.css b/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.css index 07880e9..f76a5a7 100644 --- a/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.css +++ b/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.css @@ -10,6 +10,12 @@ cursor: default; } +.parcoords text.staticlabel { + cursor: default; + font-family: FontAwesome; +} + + .parcoords rect.background { fill: transparent; } diff --git a/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.js b/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.js index 7b1a1b3..1ed16e7 100644 --- a/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.js +++ b/R.ICoVeR/inst/www/libs/parcoords/d3.parcoords.js @@ -255,7 +255,78 @@ pc.flip = function(d) { return this; }; +// add by fintan.mcgee@list.lu +// adds zoom funcitonality, , applies a log scale to axis and data +// centred around the current brush on the axis +pc.zoomScale = function(d, extents) { + + var lensHeight = 0; + var lensFocus = 0; + var currentRange = yscale[d].range(); + var lens = null, newScaleFunc; + if (!yscale[d].isZoomed) { + yscale[d].initialScale = yscale[d]; + } + + var previousAxisScale, preZoomScale; + + if (__.types[d] =="string") { + + // for the string axes the extents are in the ranges coordinate system + // we can take the numbers directly + lensHeight = (extents[1] - extents[0]) / 2; + lensFocus = extents[0] + lensHeight; + previousAxisScale = yscale[d]; + + lens = d3.fisheye.scale(d3.scale.linear).distortion(4).focus(lensFocus).domain(previousAxisScale.range()).range(previousAxisScale.range()) ; + newScaleFunc = function(val){ + // do original transform + var intermediateVal = previousAxisScale(val) + // then apply fish eye to it + return lens(intermediateVal) + } + newScaleFunc.domain = previousAxisScale.domain; + newScaleFunc.range = previousAxisScale.range; + newScaleFunc.initialScale = previousAxisScale.initialScale; + newScaleFunc.isZoomed = true; + newScaleFunc.copy = function() { + var newScaleFunc2 = newScaleFunc; + newScaleFunc2.domain = newScaleFunc.domain; + newScaleFunc2.range = newScaleFunc.range; + newScaleFunc2.copy = newScaleFunc.copy ; + newScaleFunc2.initialScale = newScaleFunc.initialScale; + newScaleFunc2.isZoomed = newScaleFunc.isZoomed + return newScaleFunc2; + }; + + yscale[d] = newScaleFunc; + } else if (__.types[d] =="number") { + // for the number axes the extents and in the domains coordinate system + // we need to map them the the original range + + previousAxisScale = yscale[d]; + lensHeight = (previousAxisScale(extents[1]) - previousAxisScale(extents[0])) / 2; + lensFocus = previousAxisScale(extents[0]) + lensHeight; + yscale[d] = d3.fisheye.scale(d3.scale.linear).distortion(4).focus(lensFocus).domain(previousAxisScale.domain()).range(previousAxisScale.range()); + yscale[d].initialScale = previousAxisScale.initialScale; + + } + yscale[d].isZoomed = true; +} +pc.checkZoomScale = function(dimension) { + // return true if zoomed in + if (yscale[dimension].isZoomed) { + return true; + } + return false; +} +pc.clearZoomScale = function(dimension) { + if (yscale[dimension].isZoomed) { + yscale[dimension] = yscale[dimension].initialScale; + yscale[dimension].isZoomed = false; + } +} pc.commonScale = function(global, type) { var t = type || "number"; if (typeof global === 'undefined') { @@ -588,6 +659,58 @@ pc.clear = function(layer) { }; d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat"); +// zoomAxis +// Functionality added by fintan.mcgee@list.lu +// when the user dbl click on the eye icon below and axis +// a linear fisheye lens effect, focused on the centre of the current brush, rescales the axis +// if there are multiple brushes overall max and min are used +// if click without brushes axis the effect is removed +function zoomAxis(dimension) { + + var g = pc.svg.selectAll(".dimension"); + // step 1 get the dimensions of the brush + var ext, + dExtents = [ Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY ]; + ext = brush.modes["1D-axes"].brushState(); + if(!ext[dimension]) { + ext = brush.modes["1D-axes-multi"].brushState(); + } + + if(ext[dimension]) { + + ext[dimension].forEach(function(e) { + if(dExtents[0] > e[0]) { + dExtents[0] = e[0]; + } + if(dExtents[1] < e[1]) { + dExtents[1] = e[1]; + } + }); + pc.brushReset(); + pc.zoomScale(dimension, dExtents); + } else { + // no brush on the data + // an undo zoom + if(pc.checkZoomScale(dimension)){ + pc.clearZoomScale(dimension); + } + } + if(__.types[dimension] =="number" && yscale[dimension].isZoomed) { + //include extra ticks if the axis is a number axis and zoomed + d3.select(this.parentElement) + .transition() + .duration(1100) + .call(axis.scale(yscale[dimension]).ticks(10)); + + } else { + d3.select(this.parentElement) + .transition() + .duration(1100) + .call(axis.scale(yscale[dimension]).ticks(5)); + } + pc.render(); + if (flags.shadows) paths(__.data, ctx.shadows); +} function flipAxisAndUpdatePCP(dimension) { var g = pc.svg.selectAll(".dimension"); @@ -628,21 +751,32 @@ pc.createAxes = function() { .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; }); // Add an axis and title. - g.append("svg:g") + var svgAxes = g.append("svg:g") .attr("class", "axis") .attr("transform", "translate(0,0)") - .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }) - .append("svg:text") + .each(function(d) { d3.select(this).call(axis.scale(yscale[d]));}); + svgAxes.append("svg:text") .attr({ "text-anchor": "middle", "y": 0, - "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")", + "transform": "translate(0, -5) rotate(" + __.dimensionTitleRotation + ")", "x": 0, "class": "label" }) .text(dimensionLabels) .on("dblclick", flipAxisAndUpdatePCP) .on("wheel", rotateLabels); + + svgAxes.append("svg:text") + .attr({ + "text-anchor": "middle", + "y": 0, + "x": 0, + "class": "staticlabel" + }) + .attr("transform",function(d) { return "translate(0," + (parseInt(yscale[d].range(), 10) * 2 + 11 ) + ")";}) + .text(function(d) { return '\uf06e';}) // eye icon in font awseome + .on("dblclick", zoomAxis); flags.axes= true; return this; @@ -657,7 +791,7 @@ pc.updateAxes = function() { var g_data = pc.svg.selectAll(".dimension").data(__.dimensions); // Enter - g_data.enter().append("svg:g") + var svgAxes = g_data.enter().append("svg:g") .attr("class", "dimension") .attr("transform", function(p) { return "translate(" + position(p) + ")"; }) .style("opacity", 0) @@ -665,7 +799,7 @@ pc.updateAxes = function() { .attr("class", "axis") .attr("transform", "translate(0,0)") .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }) - .append("svg:text") + svgAxes.append("svg:text") .attr({ "text-anchor": "middle", "y": 0, @@ -676,6 +810,17 @@ pc.updateAxes = function() { .text(dimensionLabels) .on("dblclick", flipAxisAndUpdatePCP) .on("wheel", rotateLabels); + svgAxes.append("svg:text") + .attr({ + "text-anchor": "middle", + "y": 0, + "x": 0, + "class": "staticlabel" + }) + .attr("transform",function(d) { + return "translate(0," + (parseInt(h(), 10) + 12) + ")"}) + .text(function(d) { return '\uf06e' }) // eye icon in font awseome + .on("dblclick", zoomAxis); // Update g_data.attr("opacity", 0); -- GitLab