Creating a d3.js scale with svg elements as range - javascript

I am trying to create a scale which contains the svg elements as range. E.g. a linear continous scale with a range containing circles with radius ranging from 0 to 100 and which can also be queried for those circles. The purpose of this is to pass the scale to a legend maker which uses the nice properties of d3 scales to construct a legend.
I am able to create circles that I see in the browser's page inspector but they are not displayed where it matters. Why is that? If append() accepts a dom element why isn't it displayed?
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let domFun = function(scale, range) {
scale.range(range);
return function(d) {
let template = document.createElement("template");
template.innerHTML = scale(d);
let dom = template.content.childNodes[0];
return dom;
}
};
let cScale = domFun(d3.scaleLinear(), ["<circle r = 0>", "<circle r = 100>"]);
let data = [0.2, 0.3, 0.6, 1];
canvas.selectAll("circle").data(data).enter()
.append(d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
Grateful for any help/input here.

While the other answer is the typical D3 approach to this sort of task, you can make this work with one modification.
You need to specify an SVG namespace when creating the elements. This might look like:
return function(d) {
let template = document.createElementNS(d3.namespaces.svg, "svg")
template.innerHTML = scale(d);
let dom = template.firstChild;
return dom;
}
Altogether that looks something like:
let svg = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let domFun = function(scale, range) {
scale.range(range);
return function(d) {
let template = document.createElementNS(d3.namespaces.svg, "svg")
template.innerHTML = scale(d);
let dom = template.firstChild;
return dom;
}
};
let cScale = domFun(d3.scaleLinear(), ["<circle r = 0>", "<circle r = 100>"]);
let data = [0.2, 0.3, 0.6, 1];
svg.selectAll("circle").data(data).enter()
.append(d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Why use the scale to construct some tags and use a function to extract it from inside another tag?
Just use the scale to calculate the radius and the circles are visible
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let cScale = d3.scaleLinear().range([0, 100]);
let data = [0.2, 0.3, 0.6, 1];
canvas.selectAll("circle").data(data).enter()
.append("circle")
.attr("r", d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
Edit
Instead of using the string interpolator and constructing a DOM element it can be done with the Object interpolator. This allows you to also interpolate colors.
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let icScale = d3.scaleLinear().range([{r:10, fill:"red", cx:100}, {r:100, fill:"yellow", cx:700, cy:100, shape:"circle"}]);
let data = icScale.ticks(5);
let shape = icScale(data[0]).shape;
canvas.selectAll(shape).data(data).enter()
.append(shape)
.attrs(d => icScale(d));
you need to add
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
this to your HTML page to use the .attrs().
Only the second object needs to have the shape attribute, and also because here cy does not change, it only needs to be in the second object.
If you need to keep the object returned by the scale be aware that you have to make a copy.

Related

Replacing colored circles with image circles using javascript d3 visualization

I am trying to replace the colored circles with image circles. The following plot is made using d3 in javascript.
I cannot add image due to my less than 10 rating.
enter image description here
// https://observablehq.com/#d3/smooth-zooming#58
function _chart(width,height,d3,data,radius)
{
let currentTransform = [width / 2, height / 2, height];
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
const g = svg.append("g");
g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", ([x]) => x)
.attr("cy", ([, y]) => y)
.attr("r", radius)
.attr("fill", (d, i) => d3.interpolateRainbow(i/360))
function transition() {
const d = data[Math.floor(Math.random() * data.length)];
const i = d3.interpolateZoom(currentTransform, [...d, radius * 2 + 1]);
g.transition()
.delay(250)
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)))
.on("end", transition);
}
function transform([x, y, r]) {
return `
translate(${width / 2}, ${height / 2})
scale(${height / r})
translate(${-x}, ${-y})
`;
}
return svg.call(transition).node();
}
function _height(){return(
500
)}
function _radius(){return(
2
)}
function _step(radius){return(
radius * 2
)}
function _data(step,theta,width,height){return(
Array.from({length: 2000}, (_, i) => {
const r = step * Math.sqrt(i += 0.5), a = theta * i;
return [
width / 2 + r * Math.cos(a),
height / 2 + r * Math.sin(a)
];
})
)}
function _theta(){return(
Math.PI * (3 - Math.sqrt(5))
)}
function _d3(require){return(
require("d3#6")
)}
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer("chart")).define("chart", ["width","height","d3","data","radius"], _chart);
main.variable().define("height", _height);
main.variable().define("radius", _radius);
main.variable().define("step", ["radius"], _step);
main.variable().define("data", ["step","theta","width","height"], _data);
main.variable().define("theta", _theta);
main.variable().define("d3", ["require"], _d3);
return main;
}
I think that we might need to alter the data function. I was not able to show it with images as I am a beginner in Javascript.
First off, your immediate question is:
I am trying to replace the colored circles with image circles.
This has been answered a number of times on StackOverflow, for example here.
Doing so in the context of d3 is a bit different, though, since you'll probably want some code along the lines of:
let url = ...
let defs = svg.append("defs");
defs
.append("pattern")
...
.attr("id", 'image')
.append("image")
...
.attr("href", url);
There are more details on this below.
Also, the code block that you've posted looks to me like it was returned by the Observalbe embedding API, i.e.:
https://api.observablehq.com/#d3/smooth-zooming.js?v=3
I've forked that notebook and made the changes necessary to include a few images. Here's the fork and here's the proper way to embed that code:
<div id="observablehq-chart-25b97cbc"></div>
<script type="module">
import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/#observablehq/runtime#5/dist/runtime.js";
import define from "https://api.observablehq.com/d/adfba0cfb4396df0.js?v=3";
new Runtime().module(define, name => {
if (name === "chart") return new Inspector(document.querySelector("#observablehq-chart-25b97cbc"));
});
</script>
If you examine the forked notebook, you should notice a code block near the top that looks something like so:
image_urls = [
"https://upload.wikimedia.org/wikipedia/commons/3/3d/Perspectiva_Corporum_Regularium_36a.jpg",
"https://upload.wikimedia.org/wikipedia/commons/7/7f/Hexaeder_NdFeB_5041.jpg",
"https://upload.wikimedia.org/wikipedia/commons/f/f6/Hexader_1.jpg"
]
I guess that could be just about any list of publicly accessible URLs pointing to images.
The next code block, that defines the chart, contains a few lines that look like:
let defs = svg.append("defs");
image_urls.forEach(function (url, i) {
defs
.append("pattern")
.attr("id", `image${i}`)
.attr("x", "0%")
.attr("y", "0%")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "50 50 200 100")
.append("image")
.attr("x", "0%")
.attr("y", "0%")
.attr("width", "100%")
.attr("height", "100%")
.attr("href", url);
});
That sets up the patterns that you can use as fills. Finally, that bit is used later when the circles are joined:
g.selectAll("circle")
.data(data)
.join("circle")
...
.attr("fill", (_, i) => `url(#image${i % image_urls.length}`);

How to build a compartmentalized heatmap

Using D3, I want to take the data visualization type of a classical heatmap...
.. onto a compartmentalized version of several heatmap groups drawing data from a single data source.
Technically this should be one heatmap element drawing its data from a single source - separation and thus clustering/grouping is supposed to happen through sorting the data in the *.csv file (group one, group two, group three..) and the D3 *.JS file handling the styling.
While generating a single map:
// Build X scales and axis:
const x = d3.scaleBand()
.range([0, width])
.domain(myGroups)
.padding(0.00);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Build Y scales and axis:
const y = d3.scaleBand()
.range([height, 0])
.domain(myVars)
.padding(0.00);
svg.append('g')
.call(d3.axisLeft(y));
assigning a color:
// Assign color scale
const myColor = d3.scaleLinear()
.range(['red', '#750606'])
.domain([1, 100]);
and fetching (sample) data:
// Read the data
d3.csv('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/heatmap_data.csv', (data) => {
data.sort(function(a, b) {
return myVars.indexOf(b.variable) - myVars.indexOf(a.variable) || myGroups.indexOf(a.group) - myGroups.indexOf(b.group)
});
Has been working like a charm:
CodePen
I'm struggling to expand this basic structure onto the generation of multiple groups as described above. Expanding the color scheme, trying to build several additional X and Y axis that cover different ranges result in a complete break of the D3 element rendering the map unable to be displayed at all.
Can someone point me in the right direction on how to generate multiple heatmap groups without breaking the heatmap?
I was able to solve the compartmentalization using a row and column based procedure to construct the compartments:
// Dimensions
const numCategoryCols = 4;
const numCategoryRows = Math.ceil(grouped.length / numCategoryCols);
const numEntryCols = 3;
const numEntryRows = Math.ceil(grouped[0].values.length / numEntryCols);
const gridSize = 20;
const width = gridSize * numCategoryCols * numEntryCols;
const height = gridSize * numCategoryRows * numEntryRows;
const tooltipArrowSize = 8;
// Containers
const container = d3
.select("#" + containerId)
.classed("heatmap-grid", true)
.style("position", "relative");
const svg = container
.append("svg")
.style("display", "block")
.style("width", "100%")
.attr("viewBox", [0, 0, width, height])
.style("opacity", 0);
svg.transition()
.duration(3000)
.delay((d,i) => i*200)
.style("opacity", 1)
// Heatmap
const gCategory = svg
.selectAll(".category-g")
.data(grouped, (d) => d.key)
.join("g")
.attr("class", "category-g")
.attr("fill", (d) => color(d.key))
.attr("transform", (_, i) => {
const y = Math.floor(i / numCategoryCols);
const x = i % numCategoryCols;
return `translate(${gridSize * numEntryCols * x},${
gridSize * numEntryRows * y
})`;
});
const gEntry = gCategory
.selectAll(".entry-g")
.data((d) => d.values)
.join("g")
.attr("class", "entry-g")
.attr("transform", (_, i) => {
const y = Math.floor(i / numEntryCols);
const x = i % numEntryCols;
return `translate(${gridSize * x},${gridSize * y})`;
});
const entry = gEntry
.append("rect")
.attr("width", gridSize)
.attr("height", gridSize)
.attr("fill-opacity", (d) => d.Severity / 100)
.on("mouseenter", showTooltip)
.on("mouseleave", hideTooltip);

Object Oriented D3 JS - How to select object?

Setting up my D3 project, which will have a number of shapes on the screen being animated (Org class). I am trying to maintain an object-oriented paradigm, and take advantage of the D3 animations (https://jrue.github.io/coding/2014/lesson07/).
Consider the code below:
function test() {
class Org {
constructor(_width, _height) {
this.width = _width;
this.height = _height;
}
}
var orgs = [];
var canvas = d3.select('body')
.append('svg')
.attr('width', screen.width)
.attr('height', screen.height);
for (var x = 0; x < 100; x++) {
var circle = new Org(Math.random()*screen.width, Math.random()*screen.height);
orgs.push(circle);
canvas.append('circle')
.attr('cx', circle.width)
.attr('cy', circle.height)
.attr('r', 5)
.attr('fill', 'pink');
}
for (var b = 0; b < orgs.length; b++) {
circle.transition().attr('cx', 0); //DOES NOT WORK
}
}
Obviously, the commented line throws an error because transition() belongs to D3, not my class. How can I select these objects and animate them?
Your code is not very D3-ish, which makes it cumbersome to achieve your goal. Keep in mind, that D3 by its nature is about data-driven documents and, thus, data binding is at its very core. It is essential to understand this concept to get the most out of this library. When refactoring your code accordingly, the solution becomes almost obvious.
That said, it always looks suspicious using for-loops when dealing with D3. Only rarely is there a need to put those loops to use as this is taken care of by D3's internal workings. Without breaking your OO-approach you can bind your orgs array to a selection and take advantage of D3 doing its magic:
var circles = canvas.selectAll("circle")
.data(orgs)
.enter().append('circle')
.attr('cx', d => d.width )
.attr('cy', d => d.height )
.attr('r', 5)
.attr('fill', 'pink');
This will append circles to your selection corresponding to all Org instances in your orgs array which was bound to the selection using .data(orgs). The above statement also keeps a reference to the selection containing all newly appended circles in the circles variable, which you can use for later manipulation.
This reference comes in handy, when doing the transition:
circles
.transition()
.attr('cx', 0);
Have a look at the following snippet which is equivalent to your approach, but does it the D3 way.
class Org {
constructor(_width, _height) {
this.width = _width;
this.height = _height;
}
}
var orgs = d3.range(100).map(function() {
return new Org(Math.random() * screen.width, Math.random() * screen.height);
});
var canvas = d3.select('body')
.append('svg')
.attr('width', screen.width)
.attr('height', screen.height);
var circles = canvas.selectAll("circle")
.data(orgs)
.enter().append('circle')
.attr('cx', d => d.width )
.attr('cy', d => d.height )
.attr('r', 5)
.attr('fill', 'pink');
circles
.transition()
.attr('cx', 0);
<script src="https://d3js.org/d3.v4.js"></script>
You might want to have a look at some tutorials about this concept for a more in-depth introduction:
Three Little Circles
Let’s Make a Bar Chart, Parts I, II & III
Thinking with Joins
How Selections Work (advanced)
Perhaps give them an id and select them with d3?
First give the circles an id:
for (var x = 0; x < 100; x++) {
var circle = new Org(Math.random()*screen.width, Math.random()*screen.height);
orgs.push(circle);
canvas.append('circle')
.attr('id', "myCircle_" + x)
.attr('cx', circle.width)
.attr('cy', circle.height)
.attr('r', 5)
.attr('fill', 'pink');
}
Then select them by id:
for (var b = 0; b < orgs.length; b++) {
d3.select('#myCircle_'+b).transition().attr('cx', 0);
}

Linear cx scaling with javascript objects in D3

I am attempting to plot a simple dataset consisting of an array of javascript objects. Here is the array in JSON format.
[{"key":"FITC","count":24},{"key":"PERCP","count":16},{"key":"PERCP-CY5.5","count":16},{"key":"APC-H7","count":1},{"key":"APC","count":23},{"key":"APC-CY7","count":15},{"key":"ALEXA700","count":4},{"key":"E660","count":1},{"key":"ALEXA647","count":17},{"key":"PE-CY5","count":4},{"key":"PE","count":38},{"key":"PE-CY7","count":18}]
Each object simply contains a String: "key", and a Integer: "count".
Now, I am plotting these in D3 as follows.
function key(d) {
return d.key;
}
function count(d) {
return parseInt(d.count);
}
var w = 1000,
h = 300,
//x = d3.scale.ordinal()
//.domain([count(lookup)]).rangePoints([0,w],1);
//y = d3.scale.ordinal()
//.domain(count(lookup)).rangePoints([0,h],2);
var svg = d3.select(".chart").append("svg")
.attr("width", w)
.attr("height", h);
var abs = svg.selectAll(".little")
.data(lookup)
.enter().append("circle")
.attr("cx", function(d,i){return ((i + 0.5)/lookup.length) * w;})
.attr("cy", h/2).attr("r", function(d){ return d.count * 1.5})
Here is what this looks like thus far.
What I am concerned about is how I am mapping my "cx" coordinates. Shouldn't the x() scaling function take care of this automatically, as opposed to scaling as I currently handle it? I've also tried .attr("cx", function(d,i){return x(i)}).
What I eventually want to do is label these circles with their appropriate "keys". Any help would be much appreciated.
Update:
I should mention that the following worked fine when I was dealing with an array of only the counts, as opposed to an array of objects:
x = d3.scale.ordinal().domain(nums).rangePoints([0, w], 1),
y = d3.scale.ordinal().domain(nums).rangePoints([0, h], 2);
Your code is doing what you want...I just added the text part. Here is the FIDDLE.
var txt = svg.selectAll(".txt")
.data(lookup)
.enter().append("text")
.attr("x", function (d, i) {
return ((i + 0.5) / lookup.length) * w;
})
.attr("y", h / 2)
.text(function(d) {return d.key;});
I commented out the scales, they were not being used...as already noted by you.

duplicating the whole svg element using d3.js

I am creating a rectangle using d3.js, inside that rectangle i am creating 10 smaller rectangles`.
i want to replicate whole thing into another svg element on mouse click.
jsfiddle link of the code : http://jsfiddle.net/nikunj2512/XK585/
Here is the code:
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//Draw the Rectangle
var rectangle = svgContainer.append("rect")
.attr("x", 10)
.attr("y", 10)
.attr("fill", "red")
.attr("width", 200)
.attr("height", 200);
var bigRectContainer = d3.select('#bigRectContainer').append('svg')
.attr('width', 200)
.attr('height', 200);
var dim = 20;
var x = 0;
var y = 0;
for (i = 1; i < 11; i++) {
x = 10 + (i-1)*dim;
//alert(x);
y = 10;
svgContainer.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", 20)
.attr("height", 20)
.style("fill", "black");
}
var bigRectContainer = svgContainer.append("g");
svgContainer.selectAll("rect").on("click", function () {
var littleRect = d3.select(this);
console.log(littleRect)
var bigRect = bigRectContainer.selectAll("rect")
.data(littleRect)
.enter()
.append("rect");
});
Please tell me where i made the mistake...
I'm not entirely certain what you're trying to do with the code you've posted, but I thought that duplicating an entire SVG node was interesting. It turns out it's quite easy to do with selection#html - this doesn't work on the SVG node, but it does work on its container, so it's easy to clone the whole node:
function addAnother() {
var content = d3.select(this.parentNode).html();
var div = d3.select('body').append('div')
.html(content);
div.selectAll('svg').on('click', addAnother);
}
svg.on('click', addAnother);
See working fiddle here. Note that this only works if the SVG node is the only child of its parent - otherwise, you might need to wrap it somehow.
D3 doesn't provide cloning functionality, probably because of the native cloneNode method that already exists on DOM elements, including SVG nodes.
This method includes a boolean parameter to deep copy (i.e. copy all descendants) instead of just cloning the node it is called on. You would probably want to do something like bigRectContainer.node().cloneNode(true) to copy the entire DOM branch of rectangles.

Categories

Resources