Restart d3 simulation on user input from range slider - javascript

I am building a "spring" using the d3-force layout. I want to manipulate it's properties like "strength" and "distance" via user input. For that I am currently using an "input range slider". For better understanding I set up a working draft on codepen where this question is related to: http://codepen.io/bitHugger/pen/XNqGNE?editors=1010
The HTML:
<input id="strengthElem" step="0.1" type="range" min="0" max="2"/>
I wanted to do the event handling something along like this:
let strengthElem = window.document.getElementById('strengthElem');
let strength;
strengthElem.addEventListener('click', function(evt) {
strength = strengthElem.value;
console.log('strength', strength);
}, false);
Now I would like to restart or recalculate the d3.simulation object when some interaction happens with the range slider. This is my current simulation:
let simulation = d3.forceSimulation().nodes(nodes)
.force("link", d3.forceLink()
.id(function(d) { return d.index; })
.strength(function(d) { return 2; })
.distance(function(d) { return 2; }))
.force("charge", d3.forceManyBody());
For the strength and the distance the values are currently hard coded.I would like to change it to e.g.:
.strength(function(d) { return strength; })
.distance(function(d) { return distance; })
I tried to setup a d3.call().on() function but could not get it working. I wonder how I can manipulate the simulation based on unser input, that happens outside of the force() function / outside of the svg container.
Sadly I can't get something working and I don't know how to setup a proper d3 event listener that reacts on the input button and then recalculates the force layout based on the changed values. Any ideas?

Instead of creating a link force in-place without keeping a reference to the force, you should first create the force and just pass the reference to the simulation. That way, you are later on able to manipulate the force according to your sliders' values:
// Create as before, but keep a reference for later manipulations.
let linkForce = d3.forceLink()
.id(function(d) { return d.index; })
.strength(2)
.distance(2);
let simulation = d3.forceSimulation().nodes(nodes)
.force("link", linkForce)
.force("charge", d3.forceManyBody());
When registering the event handlers on the sliders you may also want to use d3.select() for ease of use, and assign the functions using selection.on().
d3.select('#strengthElem')
.on('click', function() {
// Set the slider's value. This will re-initialize the force's strenghts.
linkForce.strength(this.value);
simulation.alpha(0.5).restart(); // Re-heat the simulation
}, false);
d3.select('#distanceElem')
.on('click', function(evt) {
// Set the slider's value. This will re-initialize the force's strenghts
linkForce.distance(this.value);
simulation.alpha(0.5).restart(); // Re-heat the simulation
}, false);
Within the handler functions this points to the actual DOM element, whereby allowing to easily access the slider's value. The link force's parameters may now be updated using the previously kept reference. All that is left to do, is to re-heat the simulation to continue its calculations.
Have a look at this snippet for a working demo:
'use strict';
var route = [[30, 30],[192, 172],[194, 170],[197, 167],[199, 164],[199, 161],[199, 157],[199, 154],[199, 150],[199, 147],[199, 143],[199, 140],[200, 137],[202, 134],[204, 132],[207, 129],[207, 126],[200, 200]];
let distance = 1;
let createNode = function(id, coords) {
return {
radius: 4,
x: coords[0],
y: coords[1],
};
};
let getNodes = (route) => {
let d = [];
let i = 0;
route.forEach(function(coord) {
if(i === 0 || i === route.length-1) {
d.push(createNode(i, coord));
d[i].fx = coord[0];
d[i].fy = coord[1];
}
else {
d.push(createNode(i, coord));
}
++i;
});
return d;
};
let getLinks = (nodes) => {
let next = 1;
let prev = 0;
let obj = [];
while(next < nodes.length) {
obj.push({source: prev, target: next, value: 1});
prev = next;
++next;
}
return obj;
};
let force = function(route) {
let width = 900;
let height = 700;
let nodes = getNodes(route);
let links = getLinks(nodes);
d3.select('#strengthElem')
.on('click', function() {
linkForce.strength(this.value); // Set the slider's value. This will re-initialize the force's strenghts
simulation.alpha(0.5).restart(); // Re-heat the simulation
}, false);
d3.select('#distanceElem')
.on('click', function(evt) {
linkForce.distance(this.value); // Set the slider's value. This will re-initialize the force's strenghts
simulation.alpha(0.5).restart(); // Re-heat the simulation
}, false);
let linkForce = d3.forceLink()
.id(function(d) { return d.index; })
.strength(2)
.distance(2);
let simulation = d3.forceSimulation().nodes(nodes)
.force("link", linkForce)
.force("charge", d3.forceManyBody());
let svg = d3.select('svg').append('svg')
.attr('width', width)
.attr('height', height);
let link = svg.append("g")
.attr('class', 'link')
.selectAll('.link')
.data(links)
.enter().append('line')
.attr("stroke-width", 1);
let node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.attr("fill", function(d) { return '#fabfab'; });
simulation.nodes(nodes).on("tick", ticked);
simulation.force("link").links(links);
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
};
force(route);
.link {
stroke: #777;
stroke-width: 2px;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div>Strength <input id="strengthElem" step="0.1" type="range" min="0" max="2"/></div>
<div>Distance <input id="distanceElem" step="1" type="range" min="0" max="50"/></div>
<svg style="width: 900; height: 700;"></svg>
I have also updated the codepen accordingly.

One way to do is is removing the contents of the svg and re-drawing it with your desired constants.
I didn't understand where you got stuck because I only changed few lines like you said in your question.
Inside your click handlers I cleared the contents of the svg and called the "draw" function:
strengthElem.addEventListener('click', function(evt) {
strength = strengthElem.value;
console.log('strength', strength);
d3.select('svg').selectAll("*").remove();
force(route);
}, false);
Moved your config variables to a global scope:
var distance = 1;
let distElem = window.document.getElementById('distanceElem');
let strengthElem = window.document.getElementById('strengthElem');
var strength = strengthElem.value;
distance = distElem.value;
And just like you've said I've changed to return parameters:
.strength(function(d) { return strength; })
.distance(function(d) { return distance; }))
Full example: http://codepen.io/anon/pen/ObZYLo?editors=1010

Related

D3 only render to certain depth

For d3, given an array of nested objects.
Is it possible to only render a certain amount of depth?
I am basing off of some sunburst examples online like :
https://github.com/Nikhilkoneru/react-d3-zoomable-sunburst/blob/master/src/index.js
Using the .selectAll method from d3, can I only limit the sunburst to render 'X' depths, instead of rendering the entire array of nested objects?
I'm trying to render a really large array, and it causes a really laggy experience.
This is the selectAll that I'm using from the github example.
svg.selectAll('path')
.data(partition(root)
.descendants())
.enter()
.append('path')
.style('fill', (d) => {
let hue;
const current = d;
if (current.depth === 0) {
return '#33cccc';
}
if (current.depth <= 1) {
hue = hueDXScale(d.x0);
current.fill = d3.hsl(hue, 0.5, 0.6);
return current.fill;
}
if(current.depth <= 2){
console.log("depth > 2: ", d)
current.fill = current.parent.fill.brighter(0.5);
const hsl = d3.hsl(current.fill);
hue = hueDXScale(current.x0);
const colorshift = hsl.h + (hue / 4);
return d3.hsl(colorshift, hsl.s, hsl.l);
}
})
.attr('stroke', '#fff')
.attr('stroke-width', '1')
.on('click', d => click(d, node, svg, x, y, radius, arc))
.on('mouseover', function (d) {
if (props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => {
const name = utils.formatNameTooltip(d);
return name;
});
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
});
As you can see, from the current.depth, i'm able to filter out what depth is more than 2.
Is it possible to add display: none to anything more than depth of 2, or is there a better way to not render anything more than depth of 2 from current

Trying to get two circles to transition simultaneously

Trying to get two circles (one red, one blue) to move to center of screen from opposite directions. Can only get the second circle to do it - unsure as to why.
I tried everything from switching up the order of functions being called to switching variable names
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 200);
var circles = svg.append('circle')
.attr('cx',50).attr('cy',50).attr('r',10).style('fill','red');
var circlesTwo = svg.append('circle')
.attr('cx',50).attr('cy',50).attr('r',10).style('fill','blue');
animation();
animationTwo();
function animation() {
svg.transition()
.duration(750)
.tween("precision", function() {
var area = d3.interpolateRound(0, 300);
return function(t) {
minArea = area(t);
render();
};
})
}
function animationTwo() {
svg.transition()
.duration(750)
.tween("precision", function() {
var area = d3.interpolateRound(600, 300);
return function(t) {
minArea = area(t);
renderTwo();
};
})
}
function render() {
circles.attr('cx',minArea);
}
function renderTwo() {
circlesTwo.attr('cx',minArea);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Expected results are two circles coming to their respective positions (from off screen originally).
Actual results are I am only getting my blue circle to work.
Applying the transitions to the SVG selection is not a very idiomatic D3: you should apply them to the elements that are moving (i.e., the circles). That, by the way, is the cause of the problem you're facing: one transition is cancelling the other.
This happens because since your transitions have no name, null is used (link):
selection.transition([name]) <>
Returns a new transition on the given selection with the specified name. If a name is not specified, null is used.
Then, because all of them have the same name (null), the last one cancels the first one:
The starting transition also cancels any pending transitions of the same name on the same element that were created before the starting transition.
Therefore, to apply multiple transitions to the same element, you have to name them:
function animation() {
svg.transition("foo")
//etc...
}
function animationTwo() {
svg.transition("bar")
//etc...
}
Here is your code with that change:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 300);
var circles = svg.append('circle')
.attr('cx', 50).attr('cy', 50).attr('r', 10).style('fill', 'red');
var circlesTwo = svg.append('circle')
.attr('cx', 50).attr('cy', 50).attr('r', 10).style('fill', 'blue');
animation();
animationTwo();
function animation() {
svg.transition("foo")
.duration(750)
.tween("precision", function() {
var area = d3.interpolateRound(0, 300);
return function(t) {
minArea = area(t);
render();
};
})
}
function animationTwo() {
svg.transition("bar")
.duration(750)
.tween("precision", function() {
var area = d3.interpolateRound(600, 300);
return function(t) {
minArea = area(t);
renderTwo();
};
})
}
function render() {
circles.attr('cx', minArea);
}
function renderTwo() {
circlesTwo.attr('cx', minArea);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Again, have in mind that I'm simply answering your question here: my advice is that you should refactor that transition code completely.

d3js prevent forceSimulation from recalculating position all the time

I'm trying to build a force layout that will allow me to visualize the flow of objects in a system.
I want to show how many objects are on a specific state and when a state change I want to update my graph.
I've built a prototype, but I've noticed that D3.js is recalculating transform of each node even when they don't need to move:
can this be fixed? maybe there is an option to add minimum value for an update?
I've declared force layout this way:
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
After changing alphaTarget to alpha recalculation stopped, but I got another bug:
I've added drag functionality and it stopped working with above changes.
Here is the version with fixed recalculation but with drag problem.
The culprit is in your restart() function:
force.alphaTarget(0.3).restart();
The way you are reheating your simulation by setting .alphaTarget(0.3) is not correct. alphaTarget is a configuration parameter which controls the way alpha decreases. Figuratively speaking, alpha—for as long as it is greater than alphaMin— is headed towards alphaTarget. The heat in the system is measured by alpha which can be thought of as dynamic data; alphaTarget, on the other hand, resembles more or less static data.
Furthermore, it is important to have alphaTarget set to a value less than alphaMin or else your simulation is going to run indefinitely because alpha, while on its way towards alphaTarget so to speak, is never going to be less than alphaMin.
Thus, if you want to reheat your system, you have to manipulate alpha instead of alphaTarget. Changing above mentioned line to the following is all it takes to get the desired effect.
force.alpha(0.3).restart();
Have a look at the following snippet, which is basically a fork of your JSFiddle, to see it in action.
document.getElementById("a").addEventListener("click", function() {
AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
AddNewLink(4, 5);
});
function AddNewLink(from, to) {
var startNode;
var start = availableNodes.find(x => x.id === from);
if (start !== undefined) {
//check if this node is already added
var foundStart = nodes.find(x => x.id == start.id);
if (foundStart === undefined) {
nodes.push(start);
startNode = start;
} else {
foundStart.value--;
if (foundStart.value < 0) foundStart.value = 0;
startNode = foundStart;
}
}
var endNode;
var end = availableNodes.find(x => x.id === to);
if (end !== undefined) {
//check if this node is already added
var foundEnd = nodes.find(x => x.id == end.id);
if (foundEnd === undefined) {
nodes.push(end);
endNode = end;
end.value++;
} else {
foundEnd.value++;
endNode = foundEnd;
}
}
//console.log(startNode, endNode);
if (startNode !== undefined && endNode !== undefined) {
links.push({
source: startNode,
target: endNode
});
}
restart();
}
// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('svg')
.on('contextmenu', () => {
d3.event.preventDefault();
})
.attr('width', width)
.attr('height', height);
var availableNodes = [{
id: 1,
name: "Start",
value: 0,
reflexive: false
}, {
id: 2,
name: "Node 1",
value: 0,
reflexive: false
}, {
id: 3,
name: "Node 2",
value: 0,
reflexive: false
}, {
id: 4,
name: "Node 3",
value: 0,
reflexive: false
}, {
id: 5,
name: "Finish",
value: 0,
reflexive: false
}];
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[1],
target: nodes[2]
}
];
// init D3 force layout
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', (d) => {
const deltaX = d.target.x - d.source.x;
const deltaY = d.target.y - d.source.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const normX = deltaX / dist;
const normY = deltaY / dist;
const sourcePadding = d.left ? 17 : 12;
const targetPadding = d.right ? 17 : 12;
const sourceX = d.source.x + (sourcePadding * normX);
const sourceY = d.source.y + (sourcePadding * normY);
const targetX = d.target.x - (targetPadding * normX);
const targetY = d.target.y - (targetPadding * normY);
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
circle.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// remove old links
path.exit().remove();
// add new links
path = path.enter().append('svg:path')
.attr('class', 'link')
.style('marker-end', 'url(#end-arrow)')
.merge(path);
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, (d) => d.id);
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', (d) => colors(d.id))
.classed('reflexive', (d) => d.reflexive);
circle.selectAll('text.value').text((d) => d.value);
// remove old nodes
circle.exit().remove();
// add new nodes
const g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', (d) => colors(d.id))
.style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
.classed('reflexive', (d) => d.reflexive)
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'value')
.text((d) => d.value);
g.append('svg:text')
.attr('x', 20)
.attr('y', 4)
.attr('class', 'name')
.text((d) => d.name);
circle = g.merge(circle);
// set the graph in motion
force
.nodes(nodes)
.force('link').links(links);
force.alpha(0.3).restart();
}
restart();
svg {
background-color: #FFF;
cursor: default;
user-select: none;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 3px;
cursor: default;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.value {
text-anchor: middle;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<button id='a'>1</button>
<button id='b'>1>2</button>
<button id='c'>2>3</button>
<button id='d'>1>3</button>
<button id='e'>3>4</button>
<button id='f'>4>5</button>
<svg width="400" height="400"></svg>

D3.js Problem rendering barchart with object data

I have the following script for rendering a simple barchart in D3.js. I have been able to render charts ok up until a point.
I have this data below which I am struggling to insert into my chart, there is no specific key I can call upon and I'm really confused how I would insert all of these into my chart.
Object { "food-environmental-science": 0, "art-media-research": 0, .....}
I have a seperate file for the HTML (only a snippet):
var barchart1 = barchart("#otherchart");
function clickScatter(d){
var unitOfAssessment = d.UoAString;
click = d.environment.topicWeights
renderTopicWeights(click)
}
function renderTopicWeights(clickedPoint){
barchart1.loadAndRenderDataset(clickedPoint)
}
When I call upon the loadAndRenderDataset function, console gives me a data.map is not a function error.
function barchart(targetDOMelement) {
//=================== PUBLIC FUNCTIONS =========================
//
barchartObject.appendedMouseOverFunction = function (callbackFunction) {
console.log("appendedMouseOverFunction called", callbackFunction)
appendedMouseOverFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.appendedMouseOutFunction = function (callbackFunction) {
appendedMouseOutFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.loadAndRenderDataset = function (data) {
dataset=data.map(d=>d); //create local copy of references so that we can sort etc.
render();
return barchartObject;
}
barchartObject.overrideDataFieldFunction = function (dataFieldFunction) {
dataField = dataFieldFunction;
return barchartObject;
}
barchartObject.overrideKeyFunction = function (keyFunction) {
//The key function is used to obtain keys for GUP rendering and
//to provide the categories for the y-axis
//These valuse should be unique
GUPkeyField = yAxisCategoryFunction = keyFunction;
return barchartObject;
}
barchartObject.overrideMouseOverFunction = function (callbackFunction) {
mouseOverFunction = callbackFunction;
render();
return barchartObject;
}
barchartObject.overrideMouseOutFunction = function (callbackFunction) {
mouseOutFunction = callbackFunction;
render(); //Needed to update DOM
return barchartObject;
}
barchartObject.overrideTooltipFunction = function (toolTipFunction) {
tooltip = toolTipFunction;
return barchartObject;
}
barchartObject.overrideMouseClickFunction = function (fn) {
mouseClick2Function = fn;
render(); //Needed to update DOM if they exist
return barchartObject;
}
barchartObject.render = function (callbackFunction) {
render(); //Needed to update DOM
return barchartObject;
}
barchartObject.setTransform = function (t) {
//Set the transform on the svg
svg.attr("transform", t)
return barchartObject;
}
barchartObject.yAxisIndent = function (indent) {
yAxisIndent=indent;
return barchartObject;
}
//=================== PRIVATE VARIABLES ====================================
//Width and height of svg canvas
var svgWidth = 900;
var svgHeight = 450;
var dataset = [];
var xScale = d3.scaleLinear();
var yScale = d3.scaleBand(); //This is an ordinal (categorical) scale
var yAxisIndent = 400; //Space for labels
var maxValueOfDataset; //For manual setting of bar length scaling (only used if .maxValueOfDataset() public method called)
//=================== INITIALISATION CODE ====================================
//Declare and append SVG element
var svg = d3
.select(targetDOMelement)
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.classed("barchart",true);
//Declare and add group for y axis
var yAxis = svg
.append("g")
.classed("yAxis", true);
//Declare and add group for x axis
var xAxis = svg
.append("g")
.classed("xAxis", true);
//===================== ACCESSOR FUNCTIONS =========================================
var dataField = function(d){return d.datafield} //The length of the bars
var tooltip = function(d){return d.key + ": "+ d.datafield} //tooltip text for bars
var yAxisCategoryFunction = function(d){return d.key} //Categories for y-axis
var GUPkeyField = yAxisCategoryFunction; //For 'keyed' GUP rendering (set to y-axis category)
//=================== OTHER PRIVATE FUNCTIONS ====================================
var maxValueOfDataField = function(){
//Find the maximum value of the data field for the x scaling function using a handy d3 max() method
//This will be used to set (normally used )
return d3.max(dataset, dataField)
};
var appendedMouseOutFunction = function(){};
var appendedMouseOverFunction = function(){};
var mouseOverFunction = function (d,i){
d3.select(this).classed("highlight", true).classed("noHighlight", false);
appendedMouseOverFunction(d,i);
}
var mouseOutFunction = function (d,i){
d3.select(this).classed("highlight", false).classed("noHighlight", true);
appendedMouseOutFunction(d,i);
}
var mouseClick2Function = function (d,i){
console.log("barchart click function = nothing at the moment, d=",d)
};
function render () {
updateScalesAndRenderAxes();
GUP_bars();
}
function updateScalesAndRenderAxes(){
//Set scales to reflect any change in svgWidth, svgHeight or the dataset size or max value
xScale
.domain([0, maxValueOfDataField()])
.range([0, svgWidth-(yAxisIndent+10)]);
yScale
.domain(dataset.map(yAxisCategoryFunction)) //Load y-axis categories into yScale
.rangeRound([25, svgHeight-40])
.padding([.1]);
//Now render the y-axis using the new yScale
var yAxisGenerator = d3.axisLeft(yScale);
svg.select(".yAxis")
.transition().duration(1000).delay(1000)
.attr("transform", "translate(" + yAxisIndent + ",0)")
.call(yAxisGenerator);
//Now render the x-axis using the new xScale
var xAxisGenerator = d3.axisTop(xScale);
svg.select(".xAxis")
.transition().duration(1000).delay(1000)
.attr("transform", "translate(" + yAxisIndent + ",20)")
.call(xAxisGenerator);
};
function GUP_bars(){
//GUP = General Update Pattern to render bars
//GUP: BIND DATA to DOM placeholders
var selection = svg
.selectAll(".bars")
.data(dataset, GUPkeyField);
//GUP: ENTER SELECTION
var enterSel = selection //Create DOM rectangles, positioned # x=yAxisIndent
.enter()
.append("rect")
.attr("x", yAxisIndent)
enterSel //Add CSS classes
.attr("class", d=>("key--"+GUPkeyField(d)))
.classed("bars enterSelection", true)
.classed("highlight", d=>d.highlight)
enterSel //Size the bars
.transition()
.duration(1000)
.delay(2000)
.attr("width", function(d) {return xScale(dataField(d));})
.attr("y", function(d, i) {return yScale(yAxisCategoryFunction(d));})
.attr("height", function(){return yScale.bandwidth()});
enterSel //Add tooltip
.append("title")
.text(tooltip)
//GUP UPDATE (anything that is already on the page)
var updateSel = selection //update CSS classes
.classed("noHighlight updateSelection", true)
.classed("highlight enterSelection exitSelection", false)
.classed("highlight", d=>d.highlight)
updateSel //update bars
.transition()
.duration(1000)
.delay(1000)
.attr("width", function(d) {return xScale(dataField(d));})
.attr("y", function(d, i) {return yScale(yAxisCategoryFunction(d));})
.attr("height", function(){return yScale.bandwidth()});
updateSel //update tool tip
.select("title") //Note that we already created a <title></title> in the Enter selection
.text(tooltip)
//GUP: Merged Enter & Update selections (so we don't write these twice)
var mergedSel = enterSel.merge(selection)
.on("mouseover", mouseOverFunction)
.on("mouseout", mouseOutFunction)
.on("click", mouseClick2Function)
//GUP EXIT selection
var exitSel = selection.exit()
.classed("highlight updateSelection enterSelection", false)
.classed("exitSelection", true)
.transition()
.duration(1000)
.attr("width",0)
.remove()
};
return barchartObject;'
}
Any help would be much appreciated, and I appolguise if what I'm asking is not clear. Thanks
format your input data into object like {key:"",value:""} and pass this into d3 so that they can understand and render chart.
var input = {'a':1,"b":2};
function parseData(input){
return Object.keys(input).reduce(function(output, key){
output.push({"key":key,"value":input[key]});
return output;
},[])
}
console.log(parseData(input));
// [{"key":"a","value":1},{"key":"b","value":2}]
jsFiddle demo - https://jsfiddle.net/1kdsoyg2/1/

d3: Adding and removing force.nodes based on slider values

I had some difficulty a while back adding and removing nodes based on user input which was solved by updating the entire set of objects pushed into force.nodes() each time the user selected which they wanted to view from a list of checkboxes.
Updating from a slider however I think requires a more finesse touch - I don't want to update the entire set every time the slider is moved. I want to push and pop nodes in and out of force.nodes().
With my current code the nodes are coming out just fine - they're just not going back in. jsfiddle here - https://jsfiddle.net/hiwilson1/ancmtxux/3/
This is the part causing problems;
function brushed() {
var exists;
//var newd = new Date(2013, 05, 01)
data.forEach(function(d, i) {
//if data point in range (between extent 0 and 1)
if (d.date >= brush.extent()[0] && d.date <= brush.extent()[1]) {
exists = force.nodes().some(function(node, i) {
//check if data point already exists in force.nodes()
return (node.mIndex == d.mIndex)
})
console.log(exists)
if (!exists) {
force.nodes().push(d)
}
}
else {
force.nodes().splice(i, 1)
}
})
d3.select("#nodeCount").text(force.nodes().length)
}
For each data point, I'm checking whether or not the point lies between extent()[0] and [1]. If it does, then check force.nodes() to see if it's currently a member there. If it isn't, then push it into force.nodes().
If the data point doesn't lie between the extents then splice it from force.nodes(). This last bit works just fine.
UPDATE: Fixed. I actually also worked out how to filter links attached to nodes as well. jsfiddle here - https://jsfiddle.net/hiwilson1/7oumeat5/2/. Never try and do this with indexes/hard coded indexes, compare the nodes/links as objects.
Incidentally I'm seeing links over the top of nodes. If there's some way to fix this I'd be happy to hear it.
FURTHER UPDATE: To ensure links behind nodes use .insert("line", ":first-child") in place of .append("line")
There are a few problems with your current code. The fundamental problem is that you're giving data to force.nodes(), which means that the two data structures are actually the same. That is, when you're removing elements from force.nodes(), you're modifying the underlying data as well. Hence you can't add them back -- they're gone.
This is easily fixed by passing a copy of data to force.nodes():
var force = d3.layout.force()
.nodes(JSON.parse(JSON.stringify(data)))
Then you're removing the wrong nodes from force.nodes() -- the index you're using is for data, not force.nodes(). You can compute the index of the data element in force.nodes() and use it like this:
data.forEach(function(d, i) {
var idx = -1;
force.nodes().forEach(function(node, j) {
if(node.mIndex == d.mIndex) {
idx = j;
}
});
//if data point in range (between extent 0 and 1)
if (d.date >= brush.extent()[0] && d.date <= brush.extent()[1]) {
if (idx == -1) {
force.nodes().push(d)
}
}
else if(idx > -1) {
force.nodes().splice(idx, 1)
}
Finally, you need to call force.start() at the end of brushed for the changes to become visible after the layout has settled down.
Complete example here.
Building on the answer from the mighty #Lars Kotthoff, who fixed your technical problem, I will focus on the architecture. Here is an architecture that is simpler and more in keeping with d3 idiom.
The main principles are:
Use Array.prototype.filter() to manage the data
Use standard, data driven patterns.
Drive the visualisation by managing the data and then use the standard, general update pattern to drive the changes into the vis.
Seperate data events and animation events.
Respond to the data changes only when need... do it in the brushed event, not on every tick. The brushed event is a data event and the tick routine is an animation event. It's not optimal to be managing data on animation events on the off chance that it is required.
Make the entry and exit of the nodes a bit smoother.
The benefit of point 1 is that the filtered array is actually an array of reference to the original data elements, so when the extra state is added on the copied array, it is actually added on the original data array. Thus, the previous state is available when it is filtered back in, hence the smooth exit and entry behaviour. Meanwhile, no elements are deleted in the original data when the brush filters down: only the references to them are deleted in the cloned array. I have to admit I had not expected that but it is a nice discovery, even if by accident!
Of course this only works because the array elements are objects.
working example here...
var width = 700,
height = 600,
padding = 20;
var start = new Date(2013, 0, 1),
end = new Date(2013, 11, 31)
var data = []
for (i = 0; i < 100; i++) {
var point = {}
var year = 2013;
var month = Math.floor(Math.random() * 12)
var day = Math.floor(Math.random() * 28)
point.date = new Date(year, month, day)
point.mIndex = i
data.push(point)
}
var force = d3.layout.force()
.size([width - padding, height - 100])
.on("tick", tick)
.start()
var svg = d3.select("body").append("svg")
.attr({
"width": width,
"height": height
})
//build stuff
var x = d3.time.scale()
.domain([start, end])
.range([padding, width - 6 * padding])
.clamp(true)
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(20)
//.tickFormat(d3.time.format("%x"))
var brush = d3.svg.brush()
.x(x)
.extent([start, end])
.on("brush", brushed1)
//append stuff
var slidercontainer = svg.append("g")
.attr("transform", "translate(100, 500)")
var axis = slidercontainer.append("g")
.call(xAxis)
var slider = slidercontainer.append("g")
.call(brush)
.classed("slider", true)
//manipulate stuff
d3.selectAll(".resize").append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 10)
.attr("fill", "Red")
.classed("handle", true)
d3.select(".domain")
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)) })
.classed("halo", true)
function brushed1(e) {
var nodes = includedNodes(data, brush);
nodes.enter().append("circle")
.attr("r", 10)
.attr("fill", "red")
.call(force.drag)
.attr("class", "node")
.attr("cx", function (d) { return d.x })
.attr("cy", function (d) { return d.y })
nodes
.exit()
.remove()
force
.nodes(includedData(data, brush))
.start()
}
function includedData(data, brush) {
return data.filter(function (d, i, a) {
return d.date >= brush.extent()[0] && d.date <= brush.extent()[1]
})
}
function includedNodes(data, brush) {
return svg.selectAll(".node")
.data(includedData(data, brush), function (d, i) {
return d.mIndex
})
}
function tick() {
includedNodes(data, brush)
.attr("cx", function (d) { return d.x })
.attr("cy", function (d) { return d.y })
}
brushed1()
.domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 10px;
stroke-linecap: round;
}
.halo {
fill: none;
stroke: #ddd;
stroke-width: 8px;
stroke-linecap: round;
}
.tick {
font-size: 10px;
}
.selecting circle {
fill-opacity: .2;
}
.selecting circle.selected {
stroke: #f00;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: crosshair;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<p id="nodeCount"></p>

Categories