# Angular Client
The original Breadboard 2 client used AngularJS to create the user interface and this older client can still be used if desired.
# Upgrade existing 2.3 experiment
To use an existing 2.3 experiment add this code at the top of the client-graph.js file.
async function init() {
const config = await Breadboard.loadConfig()
await Breadboard.loadAngularClient()
}
init()
# Use the old UI with a new experiment
Simply replace the client-graph and client-html content with the content below.
async function init() {
const config = await Breadboard.loadConfig()
await Breadboard.loadAngularClient()
}
init()
function Graph(clientId, parentElement) {
var width = parentElement ? parentElement.clientWidth : 600;
var height = parentElement ? parentElement.clientHeight : 600;
var egoNodeR = 50;
var alterNodeR = 30;
var arrowPadding = 7;
var graphPadding = 10;
var linkDistance = (Math.min(width, height) / 2) - alterNodeR - (2 * graphPadding);
var ignoreProps = ["$$hashKey", "text", "choices", "x", "y", "px", "py"];
var div = parentElement ? d3.select(parentElement) : d3.select("#graph");
var vis = div.append("svg:svg")
.attr("viewBox", "0 0 600 600")
// set up arrow markers for graph links
// Thanks to rkirsling for the example here: http://bl.ocks.org/rkirsling/5001347
vis.append('svg:defs').append('svg:marker')
.attr('id', 'end')
.attr('viewBox', '0 -5 10 10') //'0 -5 10 10'
.attr('refX', 6)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#333');
vis.append('svg:defs').append('svg:marker')
.attr('id', 'start')
.attr('viewBox', '0 -5 10 10') //'0 -5 10 10'
.attr('refX', 4)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#333');
vis.append('svg:defs').append('svg:marker')
.attr('id', 'end-green')
.attr('viewBox', '0 -5 10 10')//'0 -5 10 10'
.attr('refX', 6)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'green');
vis.append('svg:defs').append('svg:marker')
.attr('id', 'end-red')
.attr('viewBox', '0 -5 10 10')//'0 -5 10 10'
.attr('refX', 6)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'red');
vis.append('svg:defs').append('svg:marker')
.attr('id', 'start-green')
.attr('viewBox', '0 -5 10 10')//'0 -5 10 10'
.attr('refX', 4)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', 'green');
vis.append('svg:defs').append('svg:marker')
.attr('id', 'start-red')
.attr('viewBox', '0 -5 10 10')//'0 -5 10 10'
.attr('refX', 4)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', 'red');
var force = d3.layout.force()
.gravity(.05)
.friction(0.8)
.charge(-10000) //-500
.linkStrength(10) //2
.linkDistance(linkDistance * 0.9)
.size([width, height]);
var nodes = force.nodes(),
links = force.links();
force.on("tick", function () {
vis.selectAll("line.link")
.attr("x1", function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
sourcePadding = (d.source.id == clientId) ? egoNodeR : alterNodeR;
if (d.arrow && d.arrow.length > 0) {
sourcePadding += arrowPadding;
}
return d.source.x + (sourcePadding * normX);
})
.attr("y1", function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normY = deltaY / dist,
sourcePadding = (d.source.id == clientId) ? egoNodeR : alterNodeR;
if (d.arrow && d.arrow.length > 0) {
sourcePadding += arrowPadding;
}
return d.source.y + (sourcePadding * normY);
})
.attr("x2", function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
targetPadding = (d.target.id == clientId) ? egoNodeR : alterNodeR;
if (d.arrow && d.arrow.length > 0) {
targetPadding += arrowPadding;
}
return targetX = d.target.x - (targetPadding * normX);
})
.attr("y2", function (d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normY = deltaY / dist,
targetPadding = (d.target.id == clientId) ? egoNodeR : alterNodeR;
if (d.arrow && d.arrow.length > 0) {
targetPadding += arrowPadding;
}
return targetY = d.target.y - (targetPadding * normY);
});
vis.selectAll("g.node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")"
});
});
var removeNode = function (nid) {
var nodeIndex = findNode(nid);
if (nodeIndex > -1) {
nodes.splice(i, 1);
}
}
var findNode = function (nid) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].id == nid) {
return nodes[i];
}
}
return null;
}
var addLink = function (link, sourceId, targetId) {
link.source = findNode(sourceId);
link.target = findNode(targetId);
links.push(link);
}
var updateLink = function (oldLink, newLink, sourceId, targetId) {
_.extend(oldLink, newLink);
oldLink.source = findNode(sourceId);
oldLink.target = findNode(targetId);
}
this.updateGraph = function (newGraph) {
if (newGraph == undefined)
return;
if (newGraph.nodes == undefined || newGraph.nodes.length == 0) {
// Remove all nodes
nodes.length = 0;
} else {
// If there is anything in the old array that isn't in the new, it needs to be removed
for (var i = nodes.length - 1; i >= 0; i--) {
if (_.find(newGraph.nodes, function (n) {
return n.id === nodes[i].id;
}) === undefined) {
nodes.splice(i, 1);
}
}
// Finally, anything in the new array that isn't in the old needs to be added
for (var i = 0; i < newGraph.nodes.length; i++) {
var oldNode = _.find(nodes, function (n) {
return n.id === newGraph.nodes[i].id;
});
if (oldNode === null || oldNode === undefined) {
nodes.push(newGraph.nodes[i]);
} else {
// Update the old node
_.extend(oldNode, newGraph.nodes[i]);
}
}
}
if (newGraph.links == undefined || newGraph.links.length == 0) {
// Remove all links
links.length = 0;
} else {
// If there is anything in the old array that isn't in the new, it needs to be removed
for (var i = links.length - 1; i >= 0; i--) {
// source or target could have been removed at this point
var sourceId = (links[i].source == undefined) ? null : links[i].source.id;
var targetId = (links[i].target == undefined) ? null : links[i].target.id;
try {
if (_.find(newGraph.links, function (l) {
return ((newGraph.nodes[l.source].id === sourceId) && (newGraph.nodes[l.target].id === targetId));
}) === undefined) {
links.splice(i, 1);
}
} catch (e) {
}
}
// Finally, anything in the new array that isn't in the old needs to be added
for (var i = 0; i < newGraph.links.length; i++) {
var sourceIdx, targetIdx, source, target, sourceId, targetId = undefined;
var link = newGraph.links[i];
if (link != undefined) {
sourceIdx = link.source;
targetIdx = link.target;
}
if (sourceIdx != undefined && targetIdx != undefined) {
source = newGraph.nodes[sourceIdx];
target = newGraph.nodes[targetIdx];
}
if (source != undefined && target != undefined) {
sourceId = source.id;
targetId = target.id;
}
if (sourceId != undefined && targetId != undefined) {
var oldLink = _.find(links, function (l) {
return ((l.target.id === targetId) && (l.source.id === sourceId));
});
if (oldLink === null || oldLink === undefined) {
addLink(link, sourceId, targetId);
} else {
// Update the old link
updateLink(oldLink, link, sourceId, targetId);
}
}
}
}
update();
};
var animateScore = function (amount, start, end, endNodeId) {
//console.log("animateScore(" + amount + ", " + start + ", " + end + ", " + endNodeId + ")");
var animText = vis.append("svg:text")
.attr("class", "anim")
.style("text-anchor", "middle")
.style("fill", "#1EFF1E")
.style("stroke", "#000")
.style("font-family", "Lucida Sans")
.style("font-weight", "bold")
.style("font-size", 18)
.text(((amount < 0) ? "" : "+") + amount)
.attr("x", start.x)
.attr("y", start.y)
.transition()
.duration(1500)
.attr("x", end.x)
.attr("y", end.y)
.remove();
}
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function (d) {
return d.id;
});
link.enter().insert("svg:line", "g.node")
.attr("class", "link client");
link.exit().remove();
var g = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
});
var gEnter = g.enter().append("svg:g")
.attr("class", "node client");
//.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
gEnter.append("svg:text")
.attr("class", "node client")
.attr("r", 50)
.style("text-anchor", "middle")
.style("font-size", function (d) {
return (d.id == clientId) ? "18pt" : "14pt"
})
.text(function (d) {
return (d.score == undefined) ? "" : d.score;
});
gEnter.insert("svg:circle", "text.node")
.attr("class", "node client")
.attr("r", function (d) {
return (d.id == clientId) ? egoNodeR : alterNodeR;
})
.each(function (d) {
if (d.id == clientId) {
d.fixed = true;
d.x = width / 2;
d.y = height / 2;
}
});
g.exit().remove();
var scoreText = vis.selectAll("text.node");
scoreText.text(function (d) {
return (d.score == undefined) ? "" : d.score;
});
force
.nodes(nodes)
.links(links)
.start();
var node = g.selectAll("circle.node");
d3.selectAll("circle.node").each(function (d, i) {
for (var propertyName in d) {
if (_.indexOf(ignoreProps, propertyName) == -1 && isNaN(propertyName)) {
d3.select(this).attr(propertyName, d[propertyName]);
}
}
});
d3.selectAll("line.link").each(function (d, i) {
d3.select(this).attr("marker-start", null);
d3.select(this).attr("marker-end", null);
for (var propertyName in d) {
if (propertyName == "arrow") {
var arrowParams = d.arrow.split(",");
if (arrowParams.length < 1) {
return;
}
if (d.source.id == arrowParams[0] || arrowParams[0] == "both") {
if (arrowParams.length > 1 && arrowParams[1] != "grey") {
d3.select(this).attr("marker-start", "url(#start-" + arrowParams[1] + ")")
} else {
d3.select(this).attr("marker-start", "url(#start)")
}
} else {
d3.select(this).attr("marker-start", null);
}
if (d.target.id == arrowParams[0] || arrowParams[0] == "both") {
if (arrowParams.length > 1 && arrowParams[1] != "grey") {
d3.select(this).attr("marker-end", "url(#end-" + arrowParams[1] + ")")
} else {
d3.select(this).attr("marker-end", "url(#end)")
}
} else {
d3.select(this).attr("marker-end", null);
}
} else if (_.indexOf(ignoreProps, propertyName) == -1) {
d3.select(this).attr(propertyName, d[propertyName]);
}
}
});
var setupAnim = function () {
link.each(function (d) {
var animate = d3.select(this).attr("animate");
if (animate != undefined) {
var params = animate.split(",");
if (params.length > 3) {
var round = params[0];
var amount = params[1];
var startNodeId = params[2];
var endNodeId = params[3];
var startNode = findNode(startNodeId);
var endNode = findNode(endNodeId);
var start = { "x": startNode.x, "y": startNode.y };
var end = { "x": endNode.x, "y": endNode.y };
if ((d.animated != animate) && start != undefined && end != undefined && endNodeId != undefined) {
d.animated = animate;
animateScore(amount, start, end, endNodeId);
}
}
}
});
};
var t = setTimeout(setupAnim, 1000);
};
}
# Replace the client-html content
<div class="main" id="mainDiv">
<div id="dropped" ng-if="client.player.dropped == true">
<p><span>You have been dropped for being idle.</span></p>
<p><strong>Please return this HIT.</strong></p>
</div>
<div id="status" class="timers" ng-hide="client.player.dropped == true">
<bb-timer ng-repeat="timer in client.player.timers | orderBy:'order'"
timer="timer"
></bb-timer>
</div>
<div id="game" class="game">
<div id="graph" ng-hide="client.player.dropped == true"></div>
<div id="rightDiv" ng-hide="client.player.dropped == true">
<div id="text" ng-bind-html="client.player.text | to_trusted"></div>
<div id="choices" ng-controller="ChoicesCtrl">
<ng-form name="choicesForm" ng-hide="client.player.choices === undefined">
<div ng-if="custom" bind-html-compile="custom"></div>
<button ng-repeat="choice in childChoices |filter: {class: '!drop'}" class="{{choice.class}}" ng-click="makeChoice(choice.uid)" ng-disabled="choicesForm.$invalid || hasSelectedChoice">
{{choice.name}} <span ng-if="choice.wasSelected" class="fa fa-spinner fa-spin"></span>
</button>
</ng-form>
</div>
</div>
</div>
</div>