writing a Javascript function outside of scope to fire inside the scope - javascript

I use the D3 visualization library for a lot of projects and find myself copying and pasting a lot of boilerplate code for each one. Most projects, for example, start out like this:
var margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
var svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
After this sort of code, every project diverges. Part of the joy of D3 is that you do some specialized, creative coding for each new project.
I want to write a lightweight wrapper for the boilerplate code so that I can skip to the fun part each time, and in so doing I realized I don't quite understand how to properly make a complex, reusable Javascript object. Here's what I started with:
var d3mill = function() {
var margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
var svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
return {
svg: function() { return svg; },
call: function(f) { f(); }
};
};
I think I want to be able to do this:
var d3m = d3mill();
var test = function() {
console.log(svg);
};
d3.call(test);
I thought (wishfully) that passing the function through call() would cause the function to fire inside the closure of the d3mill instance, thus making svg be defined.
It would be a huge waste of time to expose every variable in the closure to the outside world in the manner of the svg() function above. What's the right way to allow outside functions to operate here?

If you change you code to this:
return {
svg: function() { return svg; },
call: function(f) { f.call(this); }
};
then it should correctly set the context within test to be d3m.
Within that function you should then be able to access this.svg() to get the SVG object, but you will not be able to access the "private" lexically scoped variable svg directly, i.e.:
var d3m = d3mill();
var test = function() {
console.log(this.svg()); // OK
console.log(svg); // not OK - undefined variable
};
d3m.call(test);
You could also just pass the svg parameter to f when it's called:
return {
svg: function() { return svg; },
call: function(f) { return f.call(this, svg); } // also added "return", just in case
};
with usage:
var d3m = d3mill();
var test = function(svg) {
console.log(svg); // now OK - it's a parameter
};
d3m.call(test);

You could also use as a constructor function.
var D3Mill = (function() {
var defaults = {
margin: { top: 20, right: 10, bottom: 30, left: 60 },
width: 960,
height: 500
};
function num(i, def) {
return ("number" === typeof i) ? i : def;
}
function D3Mill(container_id, opts) {
opts = opts || {};
// Use opts.xxx or default.xxx if no opts provided
// Expose all values as this.xxx
var margin = this.margin = (opts.margin || defaults.margin);
var width = this.width = num(opts.width, defaults.width);
var height = this.height = num(opts.height, defaults.height);
this.svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
}
D3Mill.prototype.perform = function(f) { return f.call(this); };
return D3Mill;
}());
var d3m = new D3Mill("my_container_id");
// or
var opts = {
width: 1,
height: 1,
margin: { ... }
};
var d3m = new D3Mill("my_container_id", opts);
var test = function() {
console.log(this.svg, this.margin, this.width, this.height);
};
d3m.perform(test);

the following will also give you access to the variables you want inside test.
var d3mill = function() {
this.margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
this.svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
};
var d3m = new d3mill();
var test = function() {
console.log(this.svg);
};
test.call(d3m);

Related

Why does my class field appear as undefined when referencing a value set in the constructor

I am trying to set a field value using a basic calculation that uses values set in my constructor, though for some reason when i reference some of the values initialised in my constructor, i get TypeError: Cannot read property of undefined.
Though when i reference this without trying to access any of the values set in my constructor (margin or dimension) , i do not get this error and can see the initialised object.
class viz {
constructor(dimension, margin) {
this.dimension = dimension;
this.margin = margin;
}
width = this.dimension.width - this.margin.left - this.margin.right;
height = this.dimension.height - this.margin.top - this.margin.bottom;
}
const margin = { left: 40, right: 40, top: 40, bottom: 20 };
const dimension = { width: 1000, height: 600 };
let visual = new viz(dimension,margin)
Class fields, de-sugared, always run near the beginning of the constructor, before properties are assigned to this (though class fields are assigned after a super call, if present - and a super call must be done before references to this in the constructor too):
class Parent {
}
class viz extends Parent {
constructor(dimension, margin) {
console.log('before super in constructor');
super();
console.log('after super');
this.dimension = dimension;
this.margin = margin;
}
something = console.log('class field');
width = this.dimension.width - this.margin.left - this.margin.right;
height = this.dimension.height - this.margin.top - this.margin.bottom;
}
const margin = { left: 40, right: 40, top: 40, bottom: 20 };
const dimension = { width: 1000, height: 600 };
let visual = new viz(dimension,margin)
You can't use a class field if you want to reference properties created in the constructor. You'll have to put that functionality into the constructor instead.
class viz {
constructor(dimension, margin) {
this.dimension = dimension;
this.margin = margin;
this.width = this.dimension.width - this.margin.left - this.margin.right;
this.height = this.dimension.height - this.margin.top - this.margin.bottom;
}
}
const margin = { left: 40, right: 40, top: 40, bottom: 20 };
const dimension = { width: 1000, height: 600 };
let visual = new viz(dimension,margin)
console.log(visual);

Dropdown menu in plotly.js to switch between charts

I'm trying to create a way to automatically visualise many different types of data on an online dashboard. I'm using plotly.js (built on top of d3) to make these visualisations. For now, next to the default chart, I want to create a drop down box that allows you to switch to other chart types, but I can't get it to work and haven't found examples of something similar done in plotly. Any help would be great!
My code is below which queries the server for the data, and then plots it in a histogram. I can get individual charts to work, just not with the drop down box. I'm not even sure if what I've done by making the drop down object a function is allowed, and if I can use it to change the data to be plotted.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Simple Bar Chart</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script> </script>
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
</style>
</head>
<body>
<div id="chart" style="width:90%;height:600px;"></div>
<!-- For plotly code to bind to -->
<div id="drop-down"></div>
<script>
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 80, left: 40 };
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// sends asynchronous request to the url
var HttpClient = function() {
this.get = function(aUrl, aCallback) {
var anHttpRequest = new XMLHttpRequest();
anHttpRequest.onreadystatechange = function() {
if (anHttpRequest.readyState == 4 && anHttpRequest.status == 200) {
aCallback(anHttpRequest.responseText);
}
}
anHttpRequest.open("GET", aUrl, true);
anHttpRequest.send(null);
}
};
var client = new HttpClient();
//hard coded URL for now, will accept from UI later
myURL = "https://neel-dot-village-test.appspot.com/_ah/api/searchApi/v1/fetchChartData?chartSpecs=%7B%22axis1%22%3A+%22name%22%2C+%22axis2%22%3A%22cumulativeNumbers.totalBudget%22%7D&topicType=%2Ftype%2Ftata%2Fproject";
client.get(myURL, function(response) {
var jresp = JSON.parse(response); //get response as JS object
plotHist(JSON.parse(jresp.json));
});
var chooseChart = function(data){
buttons: [{
method: plotHist,
args: data,
label: 'Histogram'
}, {
method: plotBar,
args: data,
label: 'Bar Chart'
}]
};
var plotHist = function(data) {
var plotdata = [{
x: data.y.values,
type: 'histogram',
marker: {
//color: 'rgba(100,250,100,0.7)'
},
}];
var layout = {
xaxis: {
title: data.y.label,
rangeslider: {} }, //does not automatically adjust bin sizes though
yaxis: { title: "Count" },
updatemenus: chooseChart(data),
autobinx: true
};
Plotly.newPlot('chart', plotdata, layout);
};
var plotBar = function(data) { //using plotly (built on d3)
var plotdata = [{
x: data.x.values,
y: data.y.values,
type: 'bar'
}];
var layout = {
xaxis: { title: data.x.label },
yaxis: { title: data.y.label },
updatemenus: chooseChart(data)
};
Plotly.newPlot('chart', plotdata, layout);
};
</script>
</body>

Build dynamic rectangle with Angular directive

Recently I have written a project with D3, so I need a dynamic rectangle. I used Angular to create a dynamic visualization.I have two input type rang, the first one will change the 'width' of rectangle and the second will change the 'height'.However I don't know how to use angular to draw a dynamic rectangle.
This is my code:
<div ng-app="myApp">
<rect-designer/>
<div>
<input type="range" ng-model="rectWidth" min="0" max="400" value="0"/>
<input type="range" ng-model="rectHeight" min="0" max="700" value="0"/>
</div>
</div>
Here is my JavaScript code:
var App = angular.module('myApp', []);
App.directive('rectDesigner', function() {
function link(scope, el, attr) {
var svgwidth=1000, svgheight=600;
var svgContainer = d3.select(el[0]).append('svg').attr('id','svgcontainer')
.attr({ width: svgwidth, height: svgheight });
scope.$watchGroup(['rectWidth','rectHeight'], function () {
svgContainer.append("rect").attr("id", "Rect")
.attr({ width: rectWidth, height: rectHeigh })
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
},true);
}return {
link: link,
scope: {
rectHeigh: '=',
rectWidth: '=',
},
restrict: 'E'
};
});
I don't know if there is any way to make svgWidth and svgheight dynamic, I used this code but the result was undefined.
scope.$watch(function(){
svgWidth = el.clientWidth;
svgHeight = el.clientHeight;
});
You are missing some basics here:
You don't have a controller.
The variables you are watching are not part of your directive but they should be part of that missing controller.
Since these variables aren't part of the directive, there's no need to return them into it's scope (again, they will be in the controller).
$scope.watchGroup has a callback with a function of newValues. This is where the changed variables will be.
You want to append the rect to the svg and then manipulate it's width/height. You don't want to re-append it each time the width/height changes.
So putting all this together:
var App = angular.module('myApp', []);
var Ctrl = App.controller('myCtrl', ['$scope', function($scope) {
// I always like to give them defaults in code
$scope.rectWidth = 50;
$scope.rectHeight = 50;
}]);
Ctrl.directive('rectDesigner', function() {
function link(scope, el, attr) {
var svgwidth = 500,
svgheight = 600;
var svgContainer = d3.select(el[0])
.append('svg')
.attr('id', 'svgcontainer')
.attr({
width: svgwidth,
height: svgheight
});
// only append one rect
var rect = svgContainer
.append("rect")
.attr("id", "Rect")
.attr('transform', 'translate(' + svgwidth / 2 + ',' + svgheight / 2 + ')');
scope.$watchGroup(['rectWidth', 'rectHeight'], function(newValues) {
var width = newValues[0];
var height = newValues[1];
// now change it's width and height
rect.attr({
width: width,
height: height
});
}, true);
}
return {
link: link,
};
});
Example here.

Running an async method in a syncronized manner

I have a simple for loop, which basically checks if the images are stored in the file system, if not, then download it and render the UI:
for (var t = 0; t < toJSON.length; t++) {
if (t < 3) {
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
image_url = toJSON[t];
//start
if (utilities.CheckIfImageExist(utilities.ExtractImageName(image_url))) {
var parent = Titanium.Filesystem.getApplicationDataDirectory();
var picture = Titanium.Filesystem.getFile(parent, 'pictures');
var picturePath = parent + 'pictures/';
Ti.API.info('picturePath: ' + picturePath);
var f = Titanium.Filesystem.getFile(picturePath, utilities.ExtractImageName(image_url));
var blob = f.read();
// here is saved blog file
console.log('Image already downloaded');
var width = blob.width;
var height = blob.height;
//crop so it fits in image view
if (width > height) {
view.image = ImageFactory.imageAsCropped(blob, {
width: height,
height: height,
x: 60,
y: 0
});
} else {
view.image = ImageFactory.imageAsCropped(blob, {
width: (width - 1),
height: (width - 1),
x: 60,
y: 0
});
}
} else {
//do new loop - async causing problems
alert('not downloaded');
// if image is not downloaded we will download it here
utilities.APIGetRequestImage(image_url, function (e) {
alert('begin downloaded');
var status = this.status;
if (status == 200) {
Ti.API.info(this.responseData);
//save to directory
utilities.SaveImageToDirectory(this.responseData, image_url);
//create view
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
var width = this.responseData.width;
var height = this.responseData.height;
//crop so it fits in image view
if (width > height) {
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
view.image = ImageFactory.imageAsCropped(this.responseData, {
width: height,
height: height,
x: 60,
y: 0
});
// $.scrollableView.addView(view);
viewArr.push(view);
} else {
view.image = ImageFactory.imageAsCropped(this.responseData, {
width: (width - 1),
height: (width - 1),
x: 60,
y: 0
});
viewArr.push(view);
//when t = 3, all views are put inside array, set image view
//if(t==3){
//}
}
}
}, function (err) {
alert('error downloading image');
});
}
}
}
The code, where it says "begin download" only executes after the for loop executes the first half of the IF statement (where it says "not downloaded"), by that t=3.
The for loop then executes the else statement, the trouble I have is that I need it to do it in a synchronized manner, as I am reliant on the t value to know which image to download and place in the view.
utilities.APIGetRequestImage(image_url, function(e) {
is a callback method which gets the file from the server and downloads it.
How can I get both methods to run concurrently?
Check it out:
for (var t = 0; t < toJSON.length; t++) {
if (t < 3) {
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
image_url = toJSON[t];
//start
if (utilities.CheckIfImageExist(utilities.ExtractImageName(image_url))) {
var parent = Titanium.Filesystem.getApplicationDataDirectory();
var picture = Titanium.Filesystem.getFile(parent, 'pictures');
var picturePath = parent + 'pictures/';
Ti.API.info('picturePath: ' + picturePath);
var f = Titanium.Filesystem.getFile(picturePath, utilities.ExtractImageName(image_url));
var blob = f.read();
// here is saved blog file
console.log('Image already downloaded');
var width = blob.width;
var height = blob.height;
//crop so it fits in image view
if (width > height) {
view.image = ImageFactory.imageAsCropped(blob, {
width: height,
height: height,
x: 60,
y: 0
});
} else {
view.image = ImageFactory.imageAsCropped(blob, {
width: (width - 1),
height: (width - 1),
x: 60,
y: 0
});
}
} else {
//do new loop - async causing problems
alert('not downloaded');
// if image is not downloaded we will download it here
utilities.APIGetRequestImage(image_url, (function (t, image_url) {
return function (e) { // <----- wrap callback function
alert('begin downloaded');
var status = this.status;
if (status == 200) {
Ti.API.info(this.responseData);
//save to directory
utilities.SaveImageToDirectory(this.responseData, image_url);
//create view
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
var width = this.responseData.width;
var height = this.responseData.height;
//crop so it fits in image view
if (width > height) {
var view = Titanium.UI.createImageView({
width: 320,
height: 310,
//top: 10
});
view.image = ImageFactory.imageAsCropped(this.responseData, {
width: height,
height: height,
x: 60,
y: 0
});
// $.scrollableView.addView(view);
viewArr.push(view);
} else {
view.image = ImageFactory.imageAsCropped(this.responseData, {
width: (width - 1),
height: (width - 1),
x: 60,
y: 0
});
viewArr.push(view);
//when t = 3, all views are put inside array, set image view
//if(t==3){
//}
}
}
};
})(t, image_url), function (err) {
alert('error downloading image');
});
}
}
}
By wrapping utilities.APIGetRequestImage callback, t and image_url are passed correctly.
When saying that you "need it to do it in a synchronized manner", then you don't actually. You say that you want to "get both methods to run concurrently", which requires asynchrony.
Just swap the check if(t==3) (which is always 3, and even if you did put a closure around it like #wachme suggested then it would just be 3 for the download that started last - not the one that ended last) for a check of the number of items you've received: if(viewArr.length==3). It will be 3 when all three callbacks are executed, and you can continue executing your tasks.

Bubble tree in d3?

Is there an equivalent implementation of a Bubble Tree in D3? In the link I provided, the Bubble Tree was implemented in RaphaelJS and jQuery.
The straight answer to your question is no.
Using the resources at https://github.com/okfn/bubbletree/tree/master/build, the information you already know, and the information provided on http://d3js.org/ and through D3's documentation on GitHub, you should be able to conjure up your own bubble tree for D3!
This is a piece of JavaScript I used a long time ago to visualize binary tree data:
var updateVisual;
updateVisual = function() {
var drawTree, out;
drawTree = function(out, node) {
var col, gray, i, line, lineElt, lines, sub, _results, _results1;
if (node.lines) {
out.appendChild(document.createElement("div")).innerHTML = "<b>leaf</b>: " + node.lines.length + " lines, " + Math.round(node.height) + " px";
lines = out.appendChild(document.createElement("div"));
lines.style.lineHeight = "6px";
lines.style.marginLeft = "10px";
i = 0;
_results = [];
while (i < node.lines.length) {
line = node.lines[i];
lineElt = lines.appendChild(document.createElement("div"));
lineElt.className = "lineblock";
gray = Math.min(line.text.length * 3, 230);
col = gray.toString(16);
if (col.length === 1) col = "0" + col;
lineElt.style.background = "#" + col + col + col;
console.log(line.height, line);
lineElt.style.width = Math.max(Math.round(line.height / 3), 1) + "px";
_results.push(i++);
}
return _results;
} else {
out.appendChild(document.createElement("div")).innerHTML = "<b>node</b>: " + node.size + " lines, " + Math.round(node.height) + " px";
sub = out.appendChild(document.createElement("div"));
sub.style.paddingLeft = "20px";
i = 0;
_results1 = [];
while (i < node.children.length) {
drawTree(sub, node.children[i]);
_results1.push(++i);
}
return _results1;
}
};
out = document.getElementById("btree-view");
out.innerHTML = "";
return drawTree(out, editor.getDoc());
};
Just insert some circular elements and manipulate it a bit to style in a circular manor and you should have a good program set!
Here you go. I didn't add the text or decorations, but it's the meat and potatoes:
function bubbleChart(config) {
var aspectRatio = 1,
margin = { top: 0, right: 0, bottom: 0, left: 0 },
radiusScale = d3.scale.sqrt(),
scan = function(f, data, a) {
a = a === undefined ? 0 : a;
var results = [a];
data.forEach(function(d, i) {
a = f(a, d);
results.push(a);
});
return results;
},
colorScale = d3.scale.category20(),
result = function(selection) {
selection.each(function(data) {
var outerWidth = $(this).width(),
outerHeight = outerWidth / aspectRatio,
width = outerWidth - margin.left - margin.right,
height = outerHeight - margin.top - margin.bottom,
smallestDimension = Math.min(width, height),
sum = data[1].reduce(function(a, d) {
return a + d[1];
}, 0),
radiusFractions = data[1].map(function(d) {
return Math.sqrt(d[1] / sum);
}),
radiusNormalSum = radiusFractions.reduce(function(a, d) {
return a + d;
}, 0),
scanned = scan(function(a, d) {
return a + d;
}, radiusFractions.map(function(d) {
return d / radiusNormalSum;
}), 0);
radiusScale.domain([0, sum]).range([0, smallestDimension / 6]);
var svg = d3.select(this).selectAll('svg').data([data]),
svgEnter = svg.enter().append('svg');
svg.attr('width', outerWidth).attr('height', outerHeight);
var gEnter = svgEnter.append('g'),
g = svg.select('g').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')'),
circleRing = g.selectAll('circle.ring').data(data[1]),
circleRingEnter = circleRing.enter().append('circle').attr('class', 'ring');
circleRing.attr('cx', function(d, i) {
return smallestDimension / 3 * Math.cos(2 * Math.PI * (scanned[i] + scanned[i + 1]) / 2) + width / 2;
}).attr('cy', function(d, i) {
return smallestDimension / 3 * Math.sin(2 * Math.PI * (scanned[i] + scanned[i + 1]) / 2) + height / 2;
}).attr('r', function(d) {
return radiusScale(d[1]);
}).style('fill', function(d) {
return colorScale(d[0]);
});
var circleMain = g.selectAll('circle#main').data([data[0]]),
circleMainEnter = circleMain.enter().append('circle').attr('id', 'main');
circleMain.attr('cx', width / 2).attr('cy', height / 2).attr('r', radiusScale(sum)).style('fill', function(d) {
return colorScale(d);
});
});
};
result.aspectRatio = function(value) {
if(value === undefined) return aspectRatio;
aspectRatio = value;
return result;
};
result.margin = function(value) {
if(value === undefined) return margin;
margin = value;
return result;
};
return result;
}
var myBubbleChart = bubbleChart().margin({
top: 1,
right: 1,
bottom : 1,
left: 1
});
var data = ['Random Names, Random Amounts', [['Immanuel', .4], ['Pascal', 42.9], ['Marisa', 3.3], ['Hadumod', 4.5], ['Folker', 3.2], ['Theo', 4.7], ['Barnabas', 1.0], ['Lysann', 11.1], ['Julia', .7], ['Burgis', 28.2]]];
d3.select('#here').datum(data).call(myBubbleChart);
<div class="container">
<div class="row">
<div class="col-xs-12">
<div id="here"></div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You can use the pack layout , basically you can bind any data you want to the shapes in the graph and custom parameters for them to position well respect to each other. Another alternative would be the force layout.

Categories