text align center for tspan text block - d3.js

Below sample generate text block but current test is align left, how to align text by center?
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width','100')
.attr('height','100')
.attr('transform','translate(50,50)')
var text = ["12","123456789","1234"]
var fontsize = 40
var txt = svg.append('text')
.attr("text-anchor", 'start')
.attr("dominant-baseline","middle")//text-before-edge
.attr("font-size",fontsize)
.attr('alignment-baseline','middle')
var tspan = txt.selectAll('tspan')
.data(text)
.join('tspan')
.attr('class','tspan')
.attr("x", 0)
.attr('y',function(d,i) {
return i*fontsize
})
.text(d => d)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
Update
After apply #herrstrietzel's answer, I create below demo to show the usecase to put text box at specific position, currently it always at the center of svg, I wish it deployed at specific location (x,y).
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width', 300)
.attr('height', 200)
.style('background','#ececec')
add_grid(svg)
var data = [
{
text:["B", "BB", "BBB",'BBBB'],
x:50,
y:50
},
{
text:["AAAAA", "AAA", "A"],
x:150,
y:100
}
]
var fontsize = 20
var txt = svg.selectAll('.box')
.data(data)
.join('text')
.attr("text-anchor", 'middle')
.attr("x", '50%')
.attr("y", '0%')
.attr("dominant-baseline", "central")
.attr("font-size", fontsize)
.style('border','1px solid red')
.attr('x',d => d.x)
.attr('y',d => d.y)
var tspan = txt.selectAll('tspan')
.data(d => d.text)
.join('tspan')
.attr('class', 'tspan')
.attr("x", '50%')
.attr('dy', function(d) {
return fontsize * 1.2
})
.text(d => d)
function add_grid(svg) {
var w = +svg.attr('width')
var step = 10
var mygrid = function(d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`
}
var grid = []
for(var i = 0; i < w; i+=step) {
grid.push(i);
}
svg.append('g')
.selectAll(null)
.data(grid).enter()
.append('path')
.attr('d',d => mygrid(d))
.attr('fill','none')
.attr('stroke','green')
.attr('stroke-width',.5)
}
}
<script src="https://d3js.org/d3.v7.min.js"></script>

Your text is aligned left since your text-anchor value is "start" instead of "middle".
Working example
demo()
function demo() {
var svg = d3.select('body').append('svg')
.attr('width', '100')
.attr('height', '100')
//.attr('viewBox','0 0 100 100')
var text = ["12", "123456789", "1234"]
var fontsize = 20
var txt = svg.append('text')
.attr("text-anchor", 'middle')
.attr("x", '50%')
.attr("y", '0%')
.attr("dominant-baseline", "central")
.attr("font-size", fontsize)
var tspan = txt.selectAll('tspan')
.data(text)
.join('tspan')
.attr('class', 'tspan')
.attr("x", '50%')
.attr('dy', function(d) {
return fontsize * 1.2
})
.text(d => d)
}
svg {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
dy can actually be better suited to mimic line breaks.
Worth mentioning:
there are no block-like elements in svg:
the parent svg won't automatically grow or shrink according to it's content/child node dimensions ( so you need to calculate an appropriate height and with depending on your text length)
<text> elements need a x value of "50%" or "viewBox width/2" to be centered
I strongly recommend to test the best <text> properties "statically" (e.g. in a codepen or a static html)
Second example:
You need to set the same x value for both <text> and <tspan> elements to mimic a center aligned text block.
Red circles illustrate the desired horizontal/vertical center points.
demo();
function demo() {
var svg = d3
.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200)
.style("background", "#ececec");
add_grid(svg);
var data = [
{
text: ["B", "BB", "BBB", "BBBB"],
x: 50,
y: 50
},
{
text: ["AAAAA", "AAA", "A"],
x: 150,
y: 100
}
];
var fontsize = 20;
var txt = svg
.selectAll(".box")
.data(data)
.join("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "text-bottom")
.attr("font-size", fontsize)
.attr("x", (d) => d.x)
.attr("y", (d) =>{
let textL = d.text.length;
let yOffset = d.y - (fontsize * 1.2 * textL/2);
return (yOffset);
});
var tspan = txt
.selectAll("tspan")
.data((d) => d.text)
.join("tspan")
.attr("class", "tspan")
.attr("x", function (d) {
let currentX = d3.select(this.parentNode).attr("x");
return currentX;
})
.attr("dy", function (d) {
return fontsize * 1.2;
})
.text((d) => d);
function add_grid(svg) {
var w = +svg.attr("width");
var step = 10;
var mygrid = function (d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`;
};
var grid = [];
for (var i = 0; i < w; i += step) {
grid.push(i);
}
svg
.append("g")
.selectAll(null)
.data(grid)
.enter()
.append("path")
.attr("d", (d) => mygrid(d))
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 0.5);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
<svg width="300" height="200" style="background: rgb(236, 236, 236);">
<g>
<path fill="none" stroke="green" stroke-width="0.5" d="M0 0l300 0m-300 0l0 300m0-290l300 0m-290-10l0 300m-10-280l300 0m-280-20l0 300m-20-270l300 0m-270-30l0 300m-30-260l300 0m-260-40l0 300m-40-250l300 0m-250-50l0 300m-50-240l300 0m-240-60l0 300m-60-230l300 0m-230-70l0 300m-70-220l300 0m-220-80l0 300m-80-210l300 0m-210-90l0 300m-90-200l300 0m-200-100l0 300m-100-190l300 0m-190-110l0 300m-110-180l300 0m-180-120l0 300m-120-170l300 0m-170-130l0 300m-130-160l300 0m-160-140l0 300m-140-150l300 0m-150-150l0 300m-150-140l300 0m-140-160l0 300m-160-130l300 0m-130-170l0 300m-170-120l300 0m-120-180l0 300m-180-110l300 0m-110-190l0 300m-190-100l300 0m-100-200l0 300m-200-90l300 0m-90-210l0 300m-210-80l300 0m-80-220l0 300m-220-70l300 0m-70-230l0 300m-230-60l300 0m-60-240l0 300m-240-50l300 0m-50-250l0 300m-250-40l300 0m-40-260l0 300m-260-30l300 0m-30-270l0 300m-270-20l300 0m-20-280l0 300m-280-10l300 0m-10-290l0 300" />
</g>
<circle cx="50" cy="50" r="2%" fill="red"/>
<text text-anchor="middle" dominant-baseline="text-bottom" font-size="20" x="50" y="2" style="border: 1px solid red;">
<tspan class="tspan" x="50" dy="24">B</tspan>
<tspan class="tspan" x="50" dy="24">BB</tspan>
<tspan class="tspan" x="50" dy="24">BBB</tspan>
<tspan class="tspan" x="50" dy="24">BBBB</tspan>
</text>
<circle cx="150" cy="100" r="2%" fill="red"/>
<text text-anchor="middle" dominant-baseline="text-bottom" font-size="20" x="150" y="64" style="border: 1px solid red;">
<tspan class="tspan" x="150" dy="24">AAAAA</tspan>
<tspan class="tspan" x="150" dy="24">AAA</tspan>
<tspan class="tspan" x="150" dy="24">A</tspan>
</text>
</svg>

Related

How can i group elements that are bound to different data (to be used with forcesimulatoin) in D3 so that I can sort them

I try to create a Scatter-Plot that uses forcesimulation to place labels around my data points. This works fine so far (thanks to good help on stackoverflow and some blogs :) ). Here is what it looks like so far ...
Scatter-Plot so far
However, I am now stuck trying to reorder my elements so that the circle, line and text elements of each data point are next to each other on the z-axis.
Meaning from what I have at the moment:
<g class="circles">
<circle dp-1></circle>
<circle dp-2></circle>
...
</g>
<g class="labels">
<text dp-1></text>
<text dp-2></text>
...
</g>
<g class="links">
<line dp-1></line>
<line dp-2></line>
...
</g>
I want to go to ...
<g id="dp-1">
<circle dp-1></circle>
<text dp-1></text>
<line dp-1></line>
</g>
<g id="dp-2">
<circle dp-2></circle>
<text dp-2></text>
<line dp-2></line>
</g>
<g>
...
I know how I could do this in a "static" case, without using forcesimulation. However, I am out of ideas how to do this in my case, where I run a force simulation on the labels (nodes) and lines (links) but not on the circles.
How can I achieve this properly in D3? Below are the most important snippets of my code.
The main point where I am stuck is that I use different data arrays for my circles (data) and nodes (forceData). The latter basically is an array twice as long as the data (2 nodes per data point).
And I don't know how, to either
get D3 to draw a "g" bound to two different data, or
do the force simulation based on the data array (which would only be half the length the node array has now)
Of course other ideas to solve my problem are also welcome.
Thanks for any ideas and help.
/**
* Updates the chart. To be used when the data stayed the same, but is sliced differently (filter, ...)
*/
public update() {
this.svg.select('.dataPoints')
.selectAll("circle")
.data(this.data,
function (d: any) { return d.category }
)
.join(
function (enter) {
// what is to be done with new items ...
return enter
.append("circle")
.style("opacity", 0)
},
// function (update) { return update },
)
.attr("cx", d => this.xScale()(d.x))
.attr("cy", d => this.yScale()(d.y))
.style("fill", d => this.color(d.color))
.style("stroke-width", this.settings.dataPoints.stroke.width)
.style("stroke-opacity", this.settings.dataPoints.stroke.opacity)
.style("stroke", this.settings.dataPoints.stroke.color)
.transition()
.duration(this.settings.dataPoints.duration)
.style('opacity', 1)
.attr("r", d => this.rScale()(d.r))
if (this.settings.labels.show) {
this.svg.select(".labels")
.call(this.labelPlacement)
}
private labelPlacement = (g) => {
// we need to create our node and link array. We need two nodes per datapoint. One for the point
// itself which has a fixed x and y (fx/fy) and one for the label, which will be floating ...
var forceData = {
'nodes': [],
'links': [],
};
var myXscale = this.xScale()
var myYscale = this.yScale()
this.data.forEach(function (d, i) {
// doing the two nodes per datapoint ...
forceData.nodes.push({
id: d.category,
label: d.label,
fx: myXscale(d.x),
fy: myYscale(d.y)
});
forceData.nodes.push({
id: d.category,
label: d.label,
x: myXscale(d.x),
y: myYscale(d.y),
dataX: myXscale(d.x),
dataY: myYscale(d.y)
});
// and also adding a link between the datapoint and its label ...
forceData.links.push({
source: i * 2,
target: i * 2 + 1,
});
});
// now drawing them labels and links ...
if (this.settings.labels.showLinks) {
var labelLink = this.svg.select('.label-links')
.selectAll("line")
.data(forceData.links, (d: any) => { return (d.source + "-" + d.target) })
.join("line")
.attr("stroke", this.settings.labels.linkStroke.color)
.attr("stroke-width", this.settings.labels.linkStroke.width)
.attr("opacity", this.settings.labels.linkStroke.opacity)
}
var labelNode = this.svg.select('.labels')
.selectAll("text")
.data(forceData.nodes, (d: any) => { return d.id })
.join("text")
.text((d, i) => { return i % 2 == 0 ? "" : TextService.textLimit(d.label, this.settings.labels.maxTextLength) })
.style("fill", this.settings.labels.label.fill)
.style("font-family", this.settings.labels.label.fontFamily)
.style("font-size", this.settings.labels.label.fontSize)
.call(d3.drag()
.on("drag", dragged)
.on("end", dragended)
)
// adding and doing the force simulation ...
if (this.settings.labels.force) {
d3.forceSimulation(forceData.nodes)
.alphaTarget(this.settings.labels.alphaTarget)
.alphaDecay(this.settings.labels.alphaDecay)
.force("charge", d3.forceManyBody().strength(this.settings.labels.chargeStrength))
.force("link", d3.forceLink(forceData.links)
.distance(this.settings.labels.linkDistance)
.strength(this.settings.labels.linkStrength))
.on("tick", ticked);
}
Thanks all for reading. I have been able to solve the problem after 1 more sleepless night :)
I did skip the "g" elements, as they were not really necessary, trying to get a structure like this:
<line dp-1></line>
<circle dp-1></circle>
<line dp-2></line>
<circle dp-2></line>
...
Doing this I could use two separate data bindings and data arrays (which is needed to use the force simulation).
To achieve the correcting sorting (line - circle - line - circle - ...) i used "d3.insert" instead of append with a dynamically computed "before" element.
Below are the most important parts of the code. Hope this helps someone eventually.
Regards
// Drawing the data ...
public update() {
this.dataPoints
.selectAll("circle")
.data(this.data,
function (d: any) { return d.category }
)
.join(
enter => enter.append("circle")
.style('opacity', 0)
)
.call(this.drawData)
if (this.settings.labels.showLinks && this.settings.labels.showLinks) {
this.dataPoints
.selectAll("line")
.data(this.forceData.links)
.join(
enter => enter.insert('line', (d, i) => {
console.log("JOIN", d)
return document.getElementById('dP_' + d.id)
})
// .style('opacity', 0)
)
.call(this.drawLabelLine)
}
}
/**
* Draws the data circles ...
* #param circle
*/
private drawData = (circle) => {
circle
.attr("id", (d, i) => { return 'dP_' + d.category })
.attr("class", "dataPoint")
.style("fill", d => this.color(d.color))
.style("stroke-width", this.settings.dataPoints.stroke.width)
.style("stroke-opacity", this.settings.dataPoints.stroke.opacity)
.style("stroke", this.settings.dataPoints.stroke.color)
.transition()
.duration(this.settings.dataPoints.duration)
.style('opacity', 1)
.attr("r", d => this.rScale()(d.r))
.attr("cx", d => this.xScale()(d.x))
.attr("cy", d => this.yScale()(d.y))
}
/**
* draws the lines to connect labels to data points
* #param g
*/
private drawLabelLine = (line) => {
line
.attr("class", "label-link")
.attr("stroke", this.settings.labels.linkStroke.color)
.attr("stroke-width", this.settings.labels.linkStroke.width)
.attr("opacity", this.settings.labels.linkStroke.opacity)
}
// adding and doing the force simulation ...
if (this.settings.labels.force) {
d3.forceSimulation(forceData.nodes)
.alphaTarget(this.settings.labels.alphaTarget)
.alphaDecay(this.settings.labels.alphaDecay)
.force("charge", d3.forceManyBody().strength(this.settings.labels.chargeStrength))
.force("link", d3.forceLink(forceData.links)
.distance(this.settings.labels.linkDistance)
.strength(this.settings.labels.linkStrength))
.on("tick", ticked);
}
Since you didn't include your data, I can give you a high-level conceptual way of solving it on the data side:
Essentially, merging the 3 arrays into one object grouped by the 'color' property (could be any property) using reduce. Then append each circle, line and text into each 'g' element we create for each color.
Note: the links array is pointless to have x1 and x2 and y1 and y2 values as we can get them from the circles and labels arrays. Also, if possible, you could just define your data like my combinedData from the start.
const circles = [
{shape: "circle", color: "green", x: 2, y: 2, r: 0.5},
{shape: "circle", color: "blue", x: 4, y: 4, r: 1},
{shape: "circle", color: "red", x: 8, y: 8, r: 1.5},
];
const links = [
{shape: "line", color: "green", x1: 2, y1: 2, x2: 1, y2: 1},
{shape: "line", color: "blue", x1: 4, y1: 4, x2: 2, y2: 6},
{shape: "line", color: "red", x1: 8, y1: 8, x2: 9, y2: 4},
];
const labels = [
{shape: "text", color: "green", x: 1, y: 1, text: "A"},
{shape: "text", color: "blue", x: 2, y: 6, text: "B"},
{shape: "text", color: "red", x: 9, y: 4, text: "C"},
];
const combinedData = [...circles, ...links, ...labels].reduce((aggObj, item) => {
if (!aggObj[item.color]) aggObj[item.color] = {};
aggObj[item.color][item.shape] = item;
return aggObj;
}, {});
//console.log(combinedData);
const groups = d3.select('svg').selectAll('g')
.data(Object.entries(combinedData))
.enter()
.append('g')
.attr('class', ([k,v]) => k);
groups
.append('circle')
.attr('fill', ([k,v]) => v.circle.color)
.attr('r', ([k,v]) => v.circle.r)
.attr('cx', ([k,v]) => v.circle.x)
.attr('cy', ([k,v]) => v.circle.y)
groups
.append('line')
.attr('stroke', ([k,v]) => v.line.color)
.attr('stroke-width', 0.1)
.attr('x1', ([k,v]) => v.line.x1)
.attr('y1', ([k,v]) => v.line.y1)
.attr('x2', ([k,v]) => v.line.x2)
.attr('y2', ([k,v]) => v.line.y2)
groups
.append('rect')
.attr('fill', "#cfcfcf")
.attr('x', ([k,v]) => v.text.x - 0.6)
.attr('y', ([k,v]) => v.text.y - 0.6)
.attr('width', 1.1)
.attr('height', 1.1)
groups
.append('text')
.attr('alignment-baseline', "middle")
.attr('text-anchor', "middle")
.attr('fill', ([k,v]) => v.text.color)
.attr('font-size', 1)
.attr('x', ([k,v]) => v.text.x)
.attr('y', ([k,v]) => v.text.y)
.text(([k,v]) => v.text.text)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="100%" viewbox="0 0 12 12">
</svg>
Elements in groups:
Expanded:
<svg width="100%" viewBox="0 0 12 12">
<g class="green">
<circle fill="green" r="0.5" cx="2" cy="2"></circle>
<line stroke="green" stroke-width="0.1" x1="2" y1="2" x2="1" y2="1"></line>
<rect fill="#cfcfcf" x="0.4" y="0.4" width="1.1" height="1.1"></rect>
<text alignment-baseline="middle" text-anchor="middle" fill="green" font-size="1" x="1" y="1">A</text>
</g>
<g class="blue">
<circle fill="blue" r="1" cx="4" cy="4"></circle>
<line stroke="blue" stroke-width="0.1" x1="4" y1="4" x2="2" y2="6"></line>
<rect fill="#cfcfcf" x="1.4" y="5.4" width="1.1" height="1.1"></rect>
<text alignment-baseline="middle" text-anchor="middle" fill="blue" font-size="1" x="2" y="6">B</text>
</g>
<g class="red">
<circle fill="red" r="1.5" cx="8" cy="8"></circle>
<line stroke="red" stroke-width="0.1" x1="8" y1="8" x2="9" y2="4"></line>
<rect fill="#cfcfcf" x="8.4" y="3.4" width="1.1" height="1.1"></rect>
<text alignment-baseline="middle" text-anchor="middle" fill="red" font-size="1" x="9" y="4">C</text>
</g>
</svg>
Output (crude example):

D3.JS bar graph in Firefox cshtml

I am trying to put together an interactive bar chart in cshtml.
The good news is it works on every browser except for Firefox.
That being said I'd very much like to know why it is failing on Firefox when it even works on Internet Explorer.. I mean come on, the internet doesn't even work on Internet Explorer.
I have added in what I believe to be the relevant patch of code here:
function buildVisualization(dataSet) {
var barWidth = (chartWidth / dataSet.Items.length - 1) - 1;
var bars = svg.selectAll("rect")
.data(dataSet.Items);
// Build bars for each item
// Example "rect" element: <rect x="200" y="400" width="300" height="100" style="" class="" />
bars.enter()
.append("rect")
.attr("x", function (item, i) { return xScale(new Date(item.DateAsked)) } )
.attr("y", function (item, i) { return chartHeight - yScale(item.Rate)})
.attr("width", function (item) { return barWidth})
.attr("height", function (item) { return yScale(item.Rate)})
.attr("fill", "teal");
bars.exit().remove();
bars.transition()
.attr("x", function (item, i) { return xScale(new Date(item.DateAsked))} )
.attr("y", function (item, i) { return chartHeight - yScale(item.Rate)})
.attr("width", function (item) { return barWidth})
.attr("height", function (item) { return yScale(item.Rate)})
.attr("fill", "teal");
}
That being said I can provide any information required if requested.
I should point out that when run the chart itself is put into the right place however the bars (an important bit of a bar chart) are all pushed off to the left and stacked on top of each other though they do change height when different options are selected so it seems to be something wrong with the positioning rather than with how they are created. Any advice would be quite welcome.
Entire Snippet:
#{
ViewBag.Title = "Bar Chart";
var choices = new List<SelectListItem>
(){
new SelectListItem(){Text= "C#", Value="c#", Selected=true },
new SelectListItem(){Text= ".Net", Value=".net" },
new SelectListItem(){Text= "ASP.Net", Value="asp.net" },
new SelectListItem(){Text= "ASP.Net MVC", Value="asp.net-mvc" },
new SelectListItem(){Text= "C", Value="c" },
new SelectListItem(){Text= "C++", Value="c++" },
new SelectListItem(){Text= "JavaScript", Value="javascript" },
new SelectListItem(){Text= "Objective C", Value="objective-c" },
new SelectListItem(){Text= "PHP", Value="php" },
new SelectListItem(){Text= "Ruby", Value="ruby" },
new SelectListItem(){Text= "Python", Value="python" }
};
}
<style type="text/css">
svg g.axis {
font-size: .75em;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
svg g.axis text.label {
font-size: 2em;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
svg g.axis path,
svg g.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
</style>
<h2>#ViewBag.Title</h2>
<div class="row">
<div class="col-md-8">
<p>This demo takes tag information from data.stackexchange.com and projects it below.</p>
</div>
</div>
<div class="row">
<div class="col-md-4">
#Html.Label("TagChoice", "Tag")
#Html.DropDownList("TagChoice", choices)
</div>
</div>
<div class="row">
<div id="chartContainer">
</div>
</div>
#Scripts.Render("~/bundles/d3")
#section Scripts{
<script type="text/javascript">
$(document).ready(function () {
$("#TagChoice").on("change", function () {
var tag = $(this).val();
var url = "/api/tags?tag=";
url += encodeURIComponent(tag);
$.getJSON(url, function (data) {
buildVisualization(data);
});
});
$("#TagChoice").change();
});
// Overall dimensions of the SVG
var height = 400;
var width = 900;
// Padding...
var leftPadding = 75;
var bottomPadding = 50;
// Actual space for the bars
var chartWidth = width - leftPadding;
var chartHeight = height - bottomPadding;
//Building the scale for the heights
var yScale = d3.scale
.linear()
.range([0, chartHeight])
.domain([0, 21000]);
var yAxisScale = d3.scale
.linear()
.range([chartHeight, 0])
.domain([0, 21000]);
//Building the scale for the bar locations
var xScale = d3.time.scale()
.domain([new Date("5-1-2008"), new Date("2-1-2014")])
.range([leftPadding, width - 10]);
//Building a Y axis
var yAxis = d3.svg.axis()
.scale(yAxisScale)
.orient("left");
// Building an X Axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(d3.time.format("%m/%d/%Y"));
// Build the overall SVG container
var svg = d3.select("#chartContainer")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "chart");
// Adding the Axes
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + leftPadding + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("dy", "-55px")
.attr("dx", "-50px")
.attr("class", "label")
.style("text-anchor", "end")
.text("Number of Questions Asked");
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + chartHeight + ")")
.call(xAxis)
.append("text")
.attr("dy", "40px")
.attr("dx", "475px")
.attr("class", "label")
.style("text-anchor", "end")
.text("Month Asked");
function buildVisualization(dataSet) {
var barWidth = (chartWidth / dataSet.Items.length - 1) - 1;
var bars = svg.selectAll("rect")
.data(dataSet.Items);
// Build bars for each item
// Example "rect" element: <rect x="200" y="400" width="300" height="100" style="" class="" />
bars.enter()
.append("rect")
.attr("x", function (item, i) { return xScale(new Date(item.DateAsked)) })
.attr("y", function (item, i) { return chartHeight - yScale(item.Rate) })
.attr("width", function (item) { return barWidth })
.attr("height", function (item) { return yScale(item.Rate) })
.attr("fill", "teal");
bars.exit().remove();
bars.transition()
.attr("x", function (item, i) { return xScale(new Date(item.DateAsked)) })
.attr("y", function (item, i) { return chartHeight - yScale(item.Rate) })
.attr("width", function (item) { return barWidth })
.attr("height", function (item) { return yScale(item.Rate) })
.attr("fill", "teal");
}
</script>
}
Perhaps the reason of your problem is that in Chrome
>> new Date("5-1-2008")
Thu May 01 2008 ...
while in Firefox:
>> new Date("5-1-2008")
Invalid Date
(this is relevant to lines, where you construct xScale)

shapes overlay instead of transition in d3.js

I tried to have a beeswarm plot shift datapoint location to no avail. First, I made a simple scatter plot which can transition except x axis overlays on toggling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3: Transitioning points to randomized values, plus rescaled axes!</title>
<script src="https://d3js.org/d3.v4.min.js"></script><style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<p>Click on this text to update the chart with new data values as many times as you like!</p>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 30;
//Dynamic, random dataset
dataset = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":10,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":40,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":54,
"group":"b"}
]
//Create scale functions
var xScale = d3.scaleLinear()
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.range([h - padding, padding]);
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
draw(dataset.filter(d=> d.group=="a"));
//////////////////
//toggle//
//////////////////
d3.selectAll("input")
.on("change", function() {
var data_new = dataset.filter(d => (d.group == this.value));
draw(data_new);
});
//////////////////
//Create circles
//////////////////
function draw(dataset) {
//////////////////
//Create axis
xScale.domain([0, d3.max(dataset, function(d) { return d.value; })])
yScale.domain([0, 2])
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Create X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.transition(2000)
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
//////////////////
//draw circle
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d.value);
})
.attr("cy", function(d) {
return yScale(d.id);
})
.attr("r", 3)
.attr("opacity",0.2);
//update
svg.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d.value);
})
}
</script>
</body>
</html>
However when apply a similar code for beeswarm plot, the points don't shift location on selection, they just layer on, like the x axis in the first example:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<label>
<input type="radio" name='market' value="a" checked/>a
<input type="radio" name='market' value="b"/>b
</label>
<svg width="400" height="200"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var formatValue = d3.format(",d");
var x = d3.scaleLog()
.rangeRound([0, width]);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data = [
{"id":1,
"value":20,
"group":"a"},
{"id":1,
"value":21,
"group":"a"},
{"id":1,
"value":30,
"group":"a"},
{"id":1,
"value":32,
"group":"a"},
{"id":1,
"value":42,
"group":"a"},
{"id":1,
"value":10,
"group":"b"},
{"id":1,
"value":12,
"group":"b"},
{"id":1,
"value":15,
"group":"b"},
{"id":1,
"value":23,
"group":"b"},
{"id":1,
"value":22,
"group":"b"},
{"id":1,
"value":24,
"group":"b"}
]
//default
draw(data.filter(d=> d.group=="a"));
d3.selectAll("input")
.on("change", function()
{
var newdata = draw(data.filter(d=> d.group==this.value));
draw(newdata)
} )
/////////////////
//draw swarmplot
/////////////////
function draw(data) {
// transition
var t = d3.transition()
.duration(750);
x.domain(d3.extent(data, d=> d.value));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) { return x(d.value); }).strength(1))
.force("y", d3.forceY(height / 3))
.force("collide", d3.forceCollide(18))
.stop();
for (var i = 0; i < 120; ++i) simulation.tick();
//axis
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20, ".0s"));
//for mouse-over
var cell = g.append("g")
.attr("class", "cells")
.selectAll("g").data(d3.voronoi()
.extent([[-margin.left, -margin.top], [width + margin.right, height + margin.top]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(data)).enter().append("g");
//circle
cell.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.data.x; })
.attr("cy", function(d) { return d.data.y; })
.attr("fill", d => (d.data.Food_Sub_Group))
.attr("opacity", 0.4);
//update circle
cell.selectAll("circle")
.transition()
.duration(1000)
.attr("cx", function(d) { return d.data.x; })
}
</script>
Anything amiss here? Thanks.
Here is the difference:
In draw function of the first code, you bind data to 'circle' elements. If you have 5 circle elements before and new data size is 5, then no element is added, what changed is only data. If you have 5 circles before and new data size is 6, then with 'enter()' and 'append' function, after execution, you will have 6 elements.
In draw function of the second code, you add new elements. Every call to draw function adds new points. If you have 5 circle elements before and new data size is 5, then 5 elements are added, you will have 10 elements.
What you say 'no shift location' is actually multiple points be plotted in same place(the point seems to be darker) or different places(more points).
See data binding in d3 which may help.

Text tag not displaying properly in D3 SVG

I have the following code to generate a simple graph in D3...
var width = 800, height = 800;
// force layout setup
var force = d3.layout.force()
.charge(-200).linkDistance(30).size([width, height]);
// setup svg div
var svg = d3.select("#graph").append("svg")
.attr("width", "100%").attr("height", "100%")
.attr("pointer-events", "all");
// load graph (nodes,links) json from /graph endpoint
d3.json("/graph", function(error, graph) {
if (error) return;
force.nodes(graph.nodes).links(graph.links).start();
// render relationships as lines
var link = svg.selectAll(".link")
.data(graph.links).enter()
.append("line")
.attr("class", "link")
.attr("stroke", "black");
// render nodes as circles, css-class from label
var node = svg.selectAll(".node")
.data(graph.nodes).enter()
.append("circle")
.attr("r", 10)
.call(force.drag);
// html title attribute for title node-attribute
node.append("title")
.text(function (d) { return d.title; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "black");
// force feed algo ticks for coordinate computation
force.on("tick", function() {
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; });
});
});
Everything looks great except I cannot see the title. If I look at the DOM I see the following....
<circle r="10" cx="384.5368115283669" cy="390.4516626058579"><title font-family="sans-serif" font-size="20px" fill="black">My NAme</title></circle>
However no matter what I try I cannot seem to see the title. What am I missing here?
This is nothing to blame on d3. As per the SVG 1.1 spec the title is a description string which will not be rendered when the graphic gets rendered. Most browsers, however, will display the title as a tooltip when the mouse pointer is over the element.
I have set up an updated snippet based on your code. Placing the mouse over the red circle will bring up the tooltip "My Name".
<svg width="200" height="200">
<circle r="10" cx="38" cy="39" fill="red">
<title>My Name</title>
</circle>
</svg>
To add text to your svg which will get rendered, use the <text> element instead.
<svg width="200" height="200">
<text x="50" y="100">My Text</text>
</svg>

d3js : How to select nth element of a group?

I create a group with 9 elements (circles) such as:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
//xml
<svg>
<g id="groupOfCircles">
<circle cx="20" cy="10" r="1" fill="green"></circle>
<circle cx="40" cy="20" r="2" fill="green"></circle>
<circle cx="60" cy="30" r="3" fill="green"></circle>
<circle cx="80" cy="40" r="4" fill="green"></circle>
<circle cx="100" cy="50" r="5" fill="green"></circle>
<circle cx="120" cy="60" r="6" fill="green"></circle>
<circle cx="140" cy="70" r="7" fill="green"></circle>
<circle cx="160" cy="80" r="8" fill="green"></circle>
<circle cx="180" cy="90" r="9" fill="green"></circle>
</g>
</svg>
But How to select the nth element (i.e : the 3rd circle) of the group groupOfCircles while not knowing the circles' id or attributes values ?
I will later on loop over all elements via a for loop, and color each for one second.
Note: I tried things such as :
circles[3].attr("fill","red") // not working
d3.select("#groupOfCircles:nth-child(3)").attr("fill","red") // not working
..
The selector needs to be circle:nth-child(3) -- the child means that the element is the nth child, not to select the nth child of the element (see here).
You could also use:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
d3.select("circle:nth-child(3)").attr("fill","red"); // <== CSS selector (DOM)
d3.select(circles[0][4]).attr("fill","blue"); // <== D3 selector (node)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
d3 v4 now supports using selection.nodes() to return an array of all elements of this selection. Then, we can change the attributes of nth element by d3.select(selection.nodes()[n]).attr(something)
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
circleElements = circles.nodes(); // <== Get all circle elements
d3.select(circleElements[6]).attr("fill","red");
<script src="https://d3js.org/d3.v4.min.js"></script>
You can also use your circles array to set the element's attribute:
d3.select(circles[3]).attr("fill","red");
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
var group = document.querySelector('#groupOfCircles');
var circleNodes = group.getElementsByTagName('circle');
d3.select(circleNodes[3]).attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you want to do it in d3 logic, the anonymous function always has an index parameter aside the data:
my_selection.attr("fill",function(d,i) {return i%3==0?"red":"green";});
http://jsfiddle.net/risto/os5fj9m6/
Here is another option by using a function as the selector.
circles.select(function (d,i) { return (i==3) ? this : null;})
.attr("fill", "red");
If the selector is a function it gets the datum (d) and the iterator (i) as parameter. It then returns either the object (this) if selected or null if not selected.

Resources