What I got:
I got a D3 forced graph, with two different link types. I do visualize those two types differently. need as a simple line and uses dashed. To do so I got two different CSS classes for the links and simply switch the type if clicked. To finally visualize this changes I call the main initialize() function again.
Whats the problem:
As soon as I click on one of those links and switch the type, the outdated lines are still visible. I miss the point how to avoid such behavior? How can I make sure, that the outdated lines are gone? I appreciate any hint.
Update:
I added svg.selectAll("line").remove() before the re-init. But I doubt its best practice, further sometimes the lines just disappear completely.
var graph = {
"nodes": [
{
"id": 1
},
{
"id": 2
},
{
"id": 3
}
],
"links": [
{
"source": 1,
"target": 2,
"type": "uses"
},
{
"source": 2,
"target": 3,
"type": "needs"
},
{
"source": 3,
"target": 1,
"type": "needs"
}
]
}
var svg = d3.select("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
var force = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(80))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("collision", d3.forceCollide().radius(90))
initialize()
function initialize() {
link = svg.selectAll(".link")
.data(graph.links)
.join("line")
//.attr("class", "link")
.attr("class", function (d) {
if (d.type === "uses") {
return "uses"
} else {
return "needs"
}
})
.on("dblclick", function (event, d) {
if (d.type === "uses") {
d.type = "needs"
} else if (d.type === "needs") {
d.type = "uses"
}
svg.selectAll("line").remove()
initialize()
})
node = svg.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(graph.nodes)
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
force
.nodes(graph.nodes)
.on("tick", ticked);
force
.force("link")
.links(graph.links)
}
function ticked() {
// update link positions
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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
function dragStarted(event, d) {
if (!event.active) force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
PosX = d.x
PosY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) force.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
body {
height: 100%;
background: #e6e7ee;
overflow: hidden;
margin: 0px;
}
line {
stroke-width: 6px;
}
line.uses {
stroke: grey;
stroke-dasharray: 5;
}
line.needs {
stroke: black;
}
line:hover {
stroke: goldenrod;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<svg id="svg"></svg>
</body>
</html>
In the segment:
link = svg.selectAll(".link")
.data(graph.links)
.join("line")
//.attr("class", "link")
.attr("class", function (d) {
if (d.type === "uses") {
return "uses"
} else {
return "needs"
}
})
The selection is selecting the link class, but the lines actually have uses or needs as classes. You can instead select the previous uses and needs classes:
link = svg.selectAll("line.uses,line.needs")
This will make the .join() remove the unused lines of those classes.
I'm trying to get tooltips working with Timeline | Groups ordering.
I'm getting the error:
Uncaught Error: No container provided
at Timeline.Core._create (vis.js:9748)
at new Timeline (vis.js:40685)
Are there any examples of Timeline | Groups ordering with tooltips?
Looking at http://visjs.org/examples/timeline/items/tooltip.html
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Groups ordering</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
position: relative;
width: 100%;
height: 300px;
}
</style>
<script src="./dist/moment.min.js"></script>
<script src="./dist/vis.js"></script>
<link href="./dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1> Oracle Database </h1>
<div id="tooltips-follow">
<div class="menu">
<input type="image" src="dist/img/network/plus.png" id="zoomIn" value="Zoom in"/>
<input type="image" src="dist/img/network/minus.png" id="zoomOut" value="Zoom out"/>
<input type="image" src="dist/img/network/leftArrow.png" id="moveLeft" value="Move left"/>
<input type="image" src="dist/img/network/rightArrow.png" id="moveRight" value="Move right"/>
</div>
</div>
<script>
var groups = new vis.DataSet([
{id:0, content: "Database Roadmap", value: 0},
...
]);
// create a dataset with items
// note that months are zero-based in the JavaScript Date object, so month 3 is April
var items = new vis.DataSet([
{id:1, group:0, content: "Full", className: "Full", start: new Date(2018, 01, 01), end: new Date(2022, 06, 30), title: 'Start Date2010, 07, 01<br> End Date2010, 07, 01'},
...
]);
// create visualization
var container = document.getElementById('tooltips-follow');
var options = {
// option groupOrder can be a property name or a sort function
// the sort function must compare two groups and return a value
// > 0 when a > b
// < 0 when a < b
// 0 when a == b
groupOrder: function (a, b) {
return a.value - b.value;
},
start: new Date(),
end: new Date(1000*60*60*36864 + (new Date()).valueOf()),
stack: false,
editable: false,
clickToUse: true
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
function move (percentage) {
var range = timeline.getWindow();
var interval = range.end - range.start;
timeline.setWindow({
start: range.start.valueOf() - interval * percentage,
end: range.end.valueOf() - interval * percentage
});
}
// attach events to the navigation buttons
document.getElementById('zoomIn').onclick = function () { timeline.zoomIn( 0.2); };
document.getElementById('zoomOut').onclick = function () { timeline.zoomOut( 0.2); };
document.getElementById('moveLeft').onclick = function () { move( 0.2); };
document.getElementById('moveRight').onclick = function () { move(-0.2); };
// Follow options
var follow_options = {
tooltip: {
followMouse: true
}
};
var timelineFollow = new vis.Timeline(document.getElementById('tooltips-follow'),
items, follow_options);
</script>
</body>
</html>
So it works by just adding the title tag and using the follow option does not work. Thanks
the recent v0.3.0 blog post mentions WebVR 1.0 support allowing "us to have different content on the desktop display than the headset, opening the door for asynchronous gameplay and spectator modes." This is precisely what I'm trying to get working. I'm looking to have one camera in the scene represent the viewpoint of the HMD and a secondary camera represent a spectator of the same scene and render that view to a canvas on the same webpage. 0.3.0 removes the ability to render a-scene to a specific canvas in favor of embedded component. Any thoughts on how to accomplish two cameras rendering a single scene simultaneously?
My intention is to have a the desktop display show what a user is doing from a different perspective. My end goal is to be able to build a mixed reality green screen component.
While there may be a better or cleaner way to do this in the future, I was able to get a second camera rendering by looking at examples of how this is done in the THREE.js world.
I add a component to a non-active camera called spectator. in the init function I set up a new renderer and attach to div outside the scene to create a new canvas. I then call the render method inside the tick() part of the lifecycle.
I have not worked out how to isolate the movement of this camera yet. The default look controls of the 0.3.0 aframe scene still control both camera
Source code:
https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa
I've created a set of components that can help with this. https://github.com/diarmidmackenzie/aframe-multi-camera
Here's an example showing usage with A-Frame 1.2.0 to display the main camera on the left half of the screen, and a secondary camera on the right half.
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/aframe-multi-camera#latest/src/multi-camera.min.js"></script>
</head>
<body>
<div>
<a-scene>
<a-entity camera look-controls wasd-controls position="0 1.6 0">
<!-- first secondary camera is a child of the main camera, so that it always has the same position / rotation -->
<!-- replace main camera (since main camera is rendered across the whole screen, which we don't want) -->
<a-entity
id="camera1"
secondary-camera="outputElement:#viewport1;sequence: replace"
>
</a-entity>
</a-entity>
<!-- PUT YOUR SCENE CONTENT HERE-->
<!-- position of 2nd secondary camera-->
<a-entity
id="camera2"
secondary-camera="outputElement:#viewport2"
position="8 1.6 -6"
rotation="0 90 0"
>
</a-entity>
</a-scene>
</div>
<!-- standard HTML to contrl layout of the two viewports-->
<div style="width: 100%; height:100%; display: flex">
<div id="viewport1" style="width: 50%; height:100%"></div>
<div id="viewport2" style="width: 50%; height:100%"></div>
</div>
</body>
</html>
Also here as a glitch: https://glitch.com/edit/#!/recondite-polar-hyssop
It's also been suggested that I post the entire source code for the multi-camera component here.
Here it is...
/* System that supports capture of the the main A-Frame render() call
by add-render-call */
AFRAME.registerSystem('add-render-call', {
init() {
this.render = this.render.bind(this);
this.originalRender = this.el.sceneEl.renderer.render;
this.el.sceneEl.renderer.render = this.render;
this.el.sceneEl.renderer.autoClear = false;
this.preRenderCalls = [];
this.postRenderCalls = [];
this.suppresssDefaultRenderCount = 0;
},
addPreRenderCall(render) {
this.preRenderCalls.push(render)
},
removePreRenderCall(render) {
const index = this.preRenderCalls.indexOf(render);
if (index > -1) {
this.preRenderCalls.splice(index, 1);
}
},
addPostRenderCall(render) {
this.postRenderCalls.push(render)
},
removePostRenderCall(render) {
const index = this.postRenderCalls.indexOf(render);
if (index > -1) {
this.postRenderCalls.splice(index, 1);
}
else {
console.warn("Unexpected failure to remove render call")
}
},
suppressOriginalRender() {
this.suppresssDefaultRenderCount++;
},
unsuppressOriginalRender() {
this.suppresssDefaultRenderCount--;
if (this.suppresssDefaultRenderCount < 0) {
console.warn("Unexpected unsuppression of original render")
this.suppresssDefaultRenderCount = 0;
}
},
render(scene, camera) {
renderer = this.el.sceneEl.renderer
// set up THREE.js stats to correctly count across all render calls.
renderer.info.autoReset = false;
renderer.info.reset();
this.preRenderCalls.forEach((f) => f());
if (this.suppresssDefaultRenderCount <= 0) {
this.originalRender.call(renderer, scene, camera)
}
this.postRenderCalls.forEach((f) => f());
}
});
/* Component that captures the main A-Frame render() call
and adds an additional render call.
Must specify an entity and component that expose a function call render(). */
AFRAME.registerComponent('add-render-call', {
multiple: true,
schema: {
entity: {type: 'selector'},
componentName: {type: 'string'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}
},
init() {
this.invokeRender = this.invokeRender.bind(this);
},
update(oldData) {
// first clean up any old settings.
this.removeSettings(oldData)
// now add new settings.
if (this.data.sequence === "before") {
this.system.addPreRenderCall(this.invokeRender)
}
if (this.data.sequence === "replace") {
this.system.suppressOriginalRender()
}
if (this.data.sequence === "after" ||
this.data.sequence === "replace")
{
this.system.addPostRenderCall(this.invokeRender)
}
},
remove() {
this.removeSettings(this.data)
},
removeSettings(data) {
if (data.sequence === "before") {
this.system.removePreRenderCall(this.invokeRender)
}
if (data.sequence === "replace") {
this.system.unsuppressOriginalRender()
}
if (data.sequence === "after" ||
data.sequence === "replace")
{
this.system.removePostRenderCall(this.invokeRender)
}
},
invokeRender()
{
const componentName = this.data.componentName;
if ((this.data.entity) &&
(this.data.entity.components[componentName])) {
this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender);
}
}
});
/* Component to set layers via HTML attribute. */
AFRAME.registerComponent('layers', {
schema : {type: 'number', default: 0},
init: function() {
setObjectLayer = function(object, layer) {
if (!object.el ||
!object.el.hasAttribute('keep-default-layer')) {
object.layers.set(layer);
}
object.children.forEach(o => setObjectLayer(o, layer));
}
this.el.addEventListener("loaded", () => {
setObjectLayer(this.el.object3D, this.data);
});
if (this.el.hasAttribute('text')) {
this.el.addEventListener("textfontset", () => {
setObjectLayer(this.el.object3D, this.data);
});
}
}
});
/* This component has code in common with viewpoint-selector-renderer
However it's a completely generic stripped-down version, which
just delivers the 2nd camera function.
i.e. it is missing:
- The positioning of the viewpoint-selector entity.
- The cursor / raycaster elements.
*/
AFRAME.registerComponent('secondary-camera', {
schema: {
output: {type: 'string', oneOf: ['screen', 'plane'], default: 'screen'},
outputElement: {type: 'selector'},
cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'},
quality: {type: 'string', oneOf: ['high, low'], default: 'high'}
},
init() {
if (!this.el.id) {
console.error("No id specified on entity. secondary-camera only works on entities with an id")
}
this.savedViewport = new THREE.Vector4();
this.sceneInfo = this.prepareScene();
this.activeRenderTarget = 0;
// add the render call to the scene
this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`,
{entity: `#${this.el.id}`,
componentName: "secondary-camera",
sequence: this.data.sequence});
// if there is a cursor on this entity, set it up to read this camera.
if (this.el.hasAttribute('cursor')) {
this.el.setAttribute("cursor", "canvas: user; camera: user");
this.el.addEventListener('loaded', () => {
this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask;
const cursor = this.el.components['cursor'];
cursor.removeEventListeners();
cursor.camera = this.camera;
cursor.canvas = this.data.outputElement;
cursor.canvasBounds = cursor.canvas.getBoundingClientRect();
cursor.addEventListeners();
cursor.updateMouseEventListeners();
});
}
if (this.data.output === 'plane') {
if (!this.data.outputElement.hasLoaded) {
this.data.outputElement.addEventListener("loaded", () => {
this.configureCameraToPlane()
});
} else {
this.configureCameraToPlane()
}
}
},
configureCameraToPlane() {
const object = this.data.outputElement.getObject3D('mesh');
function nearestPowerOf2(n) {
return 1 << 31 - Math.clz32(n);
}
// 2 * nearest power of 2 gives a nice look, but at a perf cost.
const factor = (this.data.quality === 'high') ? 2 : 1;
const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio);
const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio);
function newRenderTarget() {
const target = new THREE.WebGLRenderTarget(width,
height,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
stencilBuffer: false,
generateMipmaps: false
});
return target;
}
// We use 2 render targets, and alternate each frame, so that we are
// never rendering to a target that is actually in front of the camera.
this.renderTargets = [newRenderTarget(),
newRenderTarget()]
this.camera.aspect = object.geometry.parameters.width /
object.geometry.parameters.height;
},
remove() {
this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`);
if (this.renderTargets) {
this.renderTargets[0].dispose();
this.renderTargets[1].dispose();
}
// "Remove" code does not tidy up adjustments made to cursor component.
// rarely necessary as cursor is typically put in place at the same time
// as the secondary camera, and so will be disposed of at the same time.
},
prepareScene() {
this.scene = this.el.sceneEl.object3D;
const width = 2;
const height = 2;
if (this.data.cameraType === "orthographic") {
this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
}
else {
this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000);
}
this.scene.add(this.camera);
return;
},
render(renderer, renderFunction) {
// don't bother rendering to screen in VR mode.
if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return;
var elemRect;
if (this.data.output === "screen") {
const elem = this.data.outputElement;
// get the viewport relative position of this element
elemRect = elem.getBoundingClientRect();
this.camera.aspect = elemRect.width / elemRect.height;
}
// Camera position & layers match this entity.
this.el.object3D.getWorldPosition(this.camera.position);
this.el.object3D.getWorldQuaternion(this.camera.quaternion);
this.camera.layers.mask = this.el.object3D.layers.mask;
this.camera.updateProjectionMatrix();
if (this.data.output === "screen") {
// "bottom" position is relative to the whole viewport, not just the canvas.
// We need to turn this into a distance from the bottom of the canvas.
// We need to consider the header bar above the canvas, and the size of the canvas.
const mainRect = renderer.domElement.getBoundingClientRect();
renderer.getViewport(this.savedViewport);
renderer.setViewport(elemRect.left - mainRect.left,
mainRect.bottom - elemRect.bottom,
elemRect.width,
elemRect.height);
renderFunction.call(renderer, this.scene, this.camera);
renderer.setViewport(this.savedViewport);
}
else {
// target === "plane"
// store off current renderer properties so that they can be restored.
const currentRenderTarget = renderer.getRenderTarget();
const currentXrEnabled = renderer.xr.enabled;
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
// temporarily override renderer proeperties for rendering to a texture.
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
const renderTarget = this.renderTargets[this.activeRenderTarget];
renderTarget.texture.encoding = renderer.outputEncoding;
renderer.setRenderTarget(renderTarget);
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
renderer.clear();
renderFunction.call(renderer, this.scene, this.camera);
this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture;
// restore original renderer settings.
renderer.setRenderTarget(currentRenderTarget);
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
this.activeRenderTarget = 1 - this.activeRenderTarget;
}
}
});
I have been toiling away at this one particular graph that is a modified bullet chart from one of Mike Bostock's templates. I wanted to enhance it with some button events, specifically: when you click a button it will call a transition animation to change the graph markers to new values. Both new and old values are provided in the code in raw data form. For clarity and context I have included the code in it's entirety. Search for my comment "//confusion below" to get to the part where I defined my button functions.
<!DOCTYPE html>
<html>
<head>
<title>Bullet Chart</title>
<meta charset="utf-8">
</head>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
padding-top: 40px;
position: relative;
/*width: 100%;*/
}
table{
width:60%;
margin-left:auto;
margin-right:auto;
}
td{width:50%;}
.bulleT { font: 10px sans-serif; margin-left:auto;margin-right:auto;}
.bulleT .marker { stroke: #4D4D4D; stroke-width: 2px;}
.bulleT .marker.s0 { fill-opacity:0; stroke: #999999; stroke-width: 2px; }
.bulleT .marker.s1 { fill-opacity:0; stroke: #000; stroke-width: 2px; }
.bulleT .tick line { stroke: #666; stroke-width: .5px; }
.bulleT .range.s0 { fill: #005C7A; }
.bulleT .range.s1 { fill: #29A3CC; }
.bulleT .range.s2 { fill: #c6dbef; }
.bulleT .range.s3 { fill: #29A3CC; }
.bulleT .range.s4 { fill: #005C7A; }
.bulleT .measure.s0 { fill: #4D4D4D; }
.bulleT .measure.s1 { fill: #999999; }
.bulleT .measure.s2 { fill: #eeeeee; }
.bulleT .measure.s3 { fill: #999999; }
.bulleT .measure.s4 { fill: #4D4D4D; }
.bulleT .title { font-size: 12px; font-weight: bold; }
.bulleT .subtitle.s04 { fill: #000000; font-size: 16px; font-weight: bold;}
.bulleT .subtitle.s13 { fill: #999999; font-size: 12px; font-weight: bold;}
.bulleT .subtitle.s2 { fill: #999999; font-size: 10px;}
.option {
font-family: Play;
color: #ffffff;
font-size: 12px;
width: 6%;
background: #303030;
padding: 10px 20px 10px 20px;
}
.option:hover {
background: #4c4d4d;
}
div#option2{
position:relative;
top: 10px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="canvas-svg">
<div id="canvas-svg">
<table>
<tr>
<td>
<div id="BulleT_horizontal"></div>
</td>
</tr>
</table>
</div>
<div id="option1" class="option"> Outcome 1</div>
<div id="option2" class="option"> Outcome 2</div>
<p>The Bullet Chart is here.</p>
<script>
(function() {
// Simple modification based on mbostock's Bullet Charts.
d3.bulleT = function() {
var orient = "left",
reverse = false,
vertical = false,
terjedelem = bulleTTerjedelem,
ranges = bulleTRanges,
markers = bulleTMarkers,
measures = bulleTMeasures,
width = 380,
height = 30,
tickFormat = null;
// For each small multiple
function bulleT(g) {
g.each(function(d, i) {
var terjedelemz = terjedelem.call(this, d, i),
rangez = ranges.call(this, d, i).slice().sort(d3.descending),
markerz = markers.call(this, d, i),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
var wrap = g.select("g.wrap");
if (wrap.empty()) wrap = g.append("g").attr("class", "wrap");
// Compute the x-scale.
var x0 = d3.scale.linear()
.domain([terjedelemz[0], terjedelemz[1]])
.range(reverse ? [width, 0] : [0, width]);
// Stash the new scale.
this.__chart__ = x0;
// Derive width-scales from the x-scales.
var w = bulleTWidth(x0,terjedelemz[0]);
// Update the range rects.
rangez.unshift(terjedelemz[1]);
var range = wrap.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.filter( function(d, i){ if(i != 3){ return d} })
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w)
.attr("y", 0)
.attr("height",height)
.attr("x", reverse ? x0 : 0);
range.enter().append("line")
.filter( function(d, i){ if(i == 3){ return d} })
.attr("class", "marker")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", 0)
.attr("y2", height);
// Append the measure rects.
measurez.unshift(terjedelemz[1]);
var measure = wrap.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w)
.attr("height", height / 2)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 4);
// Append rect and line marker.
var marker = wrap.selectAll("rect.marker")
.data(markerz);
marker.enter().append("rect")
.filter( function(d, i){ if(i == 1){ return d} })
.attr("class", "marker s1")
.attr("width", 6)
.attr("y", -(height/10))
.attr("height",function(d) {return height+(height/5);})
.attr("x", x0)
.attr("transform", "translate(-3,0)");
//confusion below
var option1 = d3.select("#option1");
option1.on('click.outcome', outcome1);
//option1.on('click.val', val=[100,200,1000,20]);
var option2 = d3.select("#option2");
option2.on('click.outcome', outcome2);
function outcome1(val) {
d.markerz = [1000, 2000, 10, 20]
d3.selectAll('rect.marker')
.transition()
.duration(3000)
.attr("x", function(d) {return d})
}
function outcome2(val) {
d.markerz = [-1000, -2000, -10, -20]
d3.selectAll('rect.marker')
.transition()
.duration(3000)
.attr("x", function(d) {return d})
};
marker.enter().append("line")
.filter( function(d, i){ if(i == 0){ return d} })
.attr("class", "marker s0")
.attr("x1", x0)
.attr("x2", x0)
.attr("y1", height / 4)
.attr("y2", height-(height / 4) );
// Compute the tick format.
var format = tickFormat || x0.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("tick")
.data(x0.ticks(8), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("g")
.attr("class", "tick")
.attr("transform", bulleTTranslate(x0))
.style("opacity", 1);
tickEnter.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("text")
.attr("text-anchor", "middle")
.attr("transform", function(d){
if (vertical) {
return "rotate(90)";
}
})
.attr("dy", function(d){
if(vertical){return width/60; }else{ return height+15 }
})
.attr("dx", function(d){
if(vertical){return height+15 ;}
})
.text(format);
});
}
// left, right, top, bottom
bulleT.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bulleT;
};
// terjedelem
bulleT.terjedelem = function(x) {
if (!arguments.length) return terjedelem;
terjedelem = x;
return bulleT;
};
// ranges (bad, satisfactory, good)
bulleT.ranges = function(x) {
if (!arguments.length) return ranges;
ranges = x;
return bulleT;
};
//*
// markers (previous, goal)
bulleT.markers = function(x) {
if (!arguments.length) return markers;
markers = x;
return bulleT;
};
// measures (actual, forecast)
bulleT.measures = function(x) {
if (!arguments.length) return measures;
measures = x;
return bulleT;
};
//*/
bulleT.vertical = function(x) {
if (!arguments.length) return vertical;
vertical = x;
return bulleT;
};
bulleT.width = function(x) {
if (!arguments.length) return width;
width = x;
return bulleT;
};
bulleT.height = function(x) {
if (!arguments.length) return height;
height = x;
return bulleT;
};
bulleT.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return bulleT;
};
return bulleT;
};
function bulleTTerjedelem(d) {
return d.terjedelem;
}
function bulleTRanges(d) {
return d.ranges;
}
function bulleTMarkers(d) {
return d.markers;
}
function bulleTMeasures(d) {
return d.measures;
}
function bulleTTranslate(x) {
return function(d) {
return "translate(" + x(d) + ",0)";
};
}
function bulleTWidth(x,y) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d-y) - x0);
};
}
})();
var Tscore_Man_Height = -1019;
var Tscore_Woman_Height = -261;
var Tscore_Man_Weight = -4.64;
var Tscore_Woman_Weight = -1.6;
var Tscore2 = 1300
// terjedelem is the hungarian translation of the statistical term of range
var data = [
{"title":"Exper","dimension":"(diff)","subtitle":Tscore_Man_Height,"terjedelem":[-3000,3000],"ranges":[ -2000, -1000, 0, 1000, 2000],"measures":[-1366,-676,605,1108.81],"markers":[-23,Tscore_Man_Height]},
{"title":"Gold","dimension":"(diff)","subtitle":Tscore_Woman_Height,"terjedelem":[-8000,8000],"ranges":[-5250,-2750,0,2750,5250],"measures":[-901,110,845.5,2107.5],"markers":[412,Tscore_Woman_Height]},
{"title":"Tech","dimension":"(diff)","subtitle":Tscore_Man_Weight,"terjedelem":[-20,20],"ranges":[-13.33,-6.66,0,6.66,13.33],"measures":[-8.7,-.9,4.9,10.7],"markers":[1,Tscore_Man_Weight]},
{"title":"Units","dimension":"(diff)","subtitle":Tscore_Woman_Weight,"terjedelem":[-80,80],"ranges":[-53.33,-26.66,0,26.66,53.33],"measures":[-18,-7.8,3.8,23],"markers":[-3,Tscore_Woman_Weight]}
]
var Width = 400, Height = 50;
var margin = {top: 5, right: 20, bottom: 20, left: 60},
width = Width - margin.left - margin.right,
height = Height - margin.top - margin.bottom;
var chart = d3.bulleT()
.width(width)
.height(height);
function bulleT(whichData,whereToPut,direction) {
var a=Width, b=Height;
if( direction == "vertical"){
Height=a;Width=b+30;
vertical = true;
}else{
Height=a-20;Width=b;
vertical = false;
}
var svg = d3.select(whereToPut).selectAll("svg")
.data(whichData)
.enter().append("svg")
.attr("class", "bulleT")
.attr("width", Width)
.attr("height", Height)
.append("g")
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(-90)translate("+ -(Height-margin.left) +",10)";
}else{
return "translate("+ margin.left +","+ margin.top +")";
}
})
.call(chart.vertical(vertical));
var title = svg.append("g")
.style("text-anchor", function(){
if( direction == "vertical"){
return "middle";
}else{
return "end";
}
})
.attr("transform", function(){
if( direction == "vertical"){
return "rotate(90)translate("+ Width/4 +",20)";
}else{
return "translate(-16," + height / 3 + ")";
}
});
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("dy", "1.2em")
.text(function(d) { return d.dimension; })
title.append("text")
.attr("class",function(d) {
switch (true)
{
case ( (d.markers[1] < 30) || (70 < d.markers[1]) ):
return "subtitle s04";
break;
break;
case ( (30 <= d.markers[1]) && (d.markers[1] < 40) ):
return "subtitle s13";
break;
case ( (40 <= d.markers[1]) && (d.markers[1] <= 60) ):
return "subtitle s2";
break;
case ( (60 < d.markers[1]) && (d.markers[1] <= 70) ):
return "subtitle s13";
break;
}
}
)
.attr("dy", function(){
return "2.4em";
})
.text(function(d) { return d.subtitle; });
};
bulleT(data,"#BulleT_vertical","vertical"); // "horizontal" or "vertical"
bulleT(data,"#BulleT_horizontal","horizontal");
</script>
</body>
</html>
When I click the buttons, they both do the same thing, even though I changed markerz to different values. Moreover, they don't even do the right thing, the values seem to be wrong as some of the markers go flying off the graph boundary. So whatever I'm doing doesn't seem to change the way the data is parsed. Also, I'm not entirely sure that this raw value data would be scaled to go with the x scale of the graph, or if I need to scale it again as it goes through my button functions. The scaling seems to have something to do with x0 in the code above. I couldn't get it to work with my button for some reason.
There is a similar updated block here that has functional button functionality:
http://bl.ocks.org/CodeXmonk/6187523
My graph is different in that I don't change the measures or anything other than where the markers are positioned. And I'm not randomizing the data, I have pre-coded raw data that I want to switch back and forth from using my css buttons.
So what I'm looking for is how I can code the buttons to work with pre-existing scales and load raw data and put it into the markerz, or some other variable for the .transition() animation. That way the buttons will have the desired effect of moving the markers across the graph.
Sorry I couldn't make this shorter. I hope this will be worth everyone's while though because d3.js + css interface is a really powerful combination. I hope we can all learn from this example.
Thank you for reading
After further toiling, I have good news. I got it working by pure trial and error. I will post a partial answer here. I managed to get a functional chart in terms of using .transition() with precoded raw data in pixel form. Note: the only way I got this to work was by putting in the pixel value of x for the transition. Ideally, I'd like to just put in the raw data (in data form) and have the existing scale handle it to automatically compute the pixel values. I'm not going to be too picky for now. Here is what I have going so far. I took the trouble to post my own gist, because I want it to be easier to view than the lengthy code above.
https://bl.ocks.org/diggetybo/83188e161c39f2d0f7025087598ec075
I still need an answer on the following:
how to leverage existing scales alongside my raw data that is coupled with my css button functions
How do I update the subtitle text? It doesn't know I updated the values so they always read "1153, 1506, 8.63, 5.86". Ideally when I click Outcome 1, I want them to reflect the new marker positions: "-1019,-261,-4.64,-1.6". Then still be able to revert back to "1153, 1506, 8.63, 5.86" upon clicking the Outcome 2 button.
With regards to 2., I tried copying and pasting the title.append snippet for the marker values and using another mouse event for the cue, but each way I did it returned an error. Any experienced d3 people know how to do that?
Lastly, I'm not sure why, but every time I click a button, the dev tool error count increases by 1. The error is, "something something is not a function". I'm guessing that's not a good sign. However, the graph does work for the most part. I don't know how much I should be freaking out about these errors.
Thanks again
I think you have to use document.ready function for each variable and then give the .on click function.