I have data in following format and want to use with D3.js Scatter Plot:
{
"0": [
{"X":"1", "Y":"1"},
{"X":"2", "Y":"2"},
{"X":"3", "Y":"3"},
{"X":"4", "Y":"4"}
],
"1": [
{"X":"1", "Y":"1"},
{"X":"2", "Y":"2"},
{"X":"3", "Y":"3"},
{"X":"4", "Y":"4"}
],
"2": [
{"X":"1", "Y":"1"},
{"X":"2", "Y":"2"},
{"X":"3", "Y":"3"},
{"X":"4", "Y":"4"}
],
...
}
Provided that, I want each 0, 1, 2 and N to be treated as a new series for the Scatter Plot while want to draw dots using X,Y provided in that N (0, 1, 2 or N).
My apparent confusions are around following points:
Is this data good for Scatter Plot? if not, then what should be the best graph (an existing example will be great in either case).
How to use this data with d3.min() and d3.max()?
How to use this data to define the scaling for X and Y axis, as well?
Any help is highly appreciated. Thanks in advance.
There is a fiddle to accompany this code: http://jsfiddle.net/GyWpN/13/
Crude, but the elements are there.
Is this data good for Scatter Plot? if not, then what should be the
best graph (an existing example will be great in either case).
This data will work in a scatter plot. The 'best' graph very much depends on what the data represents, and how your users will interpret it.
How to use this data with d3.min() and d3.max()?
See code below
How to use this data to define the scaling for X and Y axis, as
well?
See code below
var myData = {
"0": [ {"X":"1", "Y":"1"},
{"X":"2", "Y":"2"},
{"X":"3", "Y":"3"},
{"X":"4", "Y":"4"} ],
"1": [ {"X":"1", "Y":"2"},
{"X":"2", "Y":"3"},
{"X":"3", "Y":"4"},
{"X":"4", "Y":"5"} ],
"2": [ {"X":"1", "Y":"7"},
{"X":"2", "Y":"6"},
{"X":"3", "Y":"5"},
{"X":"4", "Y":"4"} ]};
var width = 625,
height = 350;
// A way to look more easily across all 'inner' arrays
var myDataDrill = d3.values( myData );
var x = d3.scale.linear()
.domain([0,
// max over all series'
d3.max( myDataDrill, function(d) {
// within a series, look at the X-value
return d3.max( d, function(v) { return v.X } )
} ) ])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max( myDataDrill, function(d) {
return d3.max( d, function(v) { return v.Y } ) } )])
.range([height, 0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var series = svg.selectAll( "g" )
// convert the object to an array of d3 entries
.data( d3.map( myData ).entries() )
.enter()
// create a container for each series
.append("g")
.attr( "id", function(d) { return "series-" + d.key } );
series.selectAll( "circle" )
// do a data join for each series' values
.data( function(d) { return d.value } )
.enter()
.append("circle")
.attr( "cx", function(d) { return x(d.X) } )
.attr( "r", "10" )
.attr( "cy", function(d) { return y(d.Y) } );
I'm new to D3 to but I can direct you to alignedleft his tutorials are really helpfull and your questions accept from key,value-objects are explained.
Related
I would like to create a graphic in D3 that consists of nodes connected to each other with curved lines. The lines should be curved differently depending on how far apart the start and end point of the line are.
For example (A) is a longer connection and therefore is less curved than (C).
Which D3 function is best used for this calculation and how is it output as SVG path
A code example (for example on observablehq.com) would help me a lot.
Here is a code example in obserbavlehq.com
https://observablehq.com/#garciaguillermoa/circles-and-links
I will try to explain it, let me know if there is something I am not clear enough:
Lets start with our circles, we use d3.pie() to position this circles, passing the data defined above, it will return us some arcs, but as we want circles instead of arcs, we use arc.centroid to get the coordinates of our circles
Value is required for the spacing in the pie layout that we use to calculate the position, if you want more circles, you will need to reduce the value, here is the related code:
pie = d3
.pie()
.sort(null)
.value((d) => {
return d.value;
});
arc = d3.arc().outerRadius(300).innerRadius(50);
data = [
{ id: 0, value: 10 },
{ id: 1, value: 10 },
{ id: 2, value: 10 },
{ id: 3, value: 10 },
{ id: 4, value: 10 },
{ id: 5, value: 10 },
{ id: 6, value: 10 },
{ id: 7, value: 10 },
{ id: 8, value: 10 },
{ id: 9, value: 10 },
];
const circles = [];
for(let item of pieData) {
const [x, y] = arc.centroid(item);
circles.push({x, y});
}
Now we can render the circles:
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const mainGroup = svg
.append("g")
.attr("id", "main")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Insert lines and circles groups, lines first so they are behind circles
const linesGroup = mainGroup.append("g").attr("id", "lines");
const circlesGroup = mainGroup.append("g").attr("id", "circles");
circlesGroup
.selectAll("circle")
.data(circles, (_, index) => index)
.join((enter) => {
enter
.append("circle")
.attr("id", (_, index) => {
return `circle-${index}`;
})
.attr("r", 20)
.attr("cx", (d) => {
return d.x;
})
.attr("cy", (d) => {
return d.y;
})
.style("stroke-width", "2px")
.style("stroke", "#000")
.style("fill", "#963cff");
});
Now we need to declare the links, we could do this with an array specifying the id of the source and destination (from and to). we use this to search each circle, get its coordinates (the source and destination of our links) and then create the links, in order to create them, we can use a path and the d3 method quadraticCurveTo, this function requires four parameters, the first two are "the control point" which defines our curve, we use 0, 0 as it is the center of our viz (it is the center because we used a translate in the parent group).
lines = [
{
from: 1,
to: 3,
},
{
from: 8,
to: 4,
},
];
for (let line of lines) {
const fromCircle = circles[line.from];
const toCircle = circles[line.to];
const fromP = { x: fromCircle.x, y: fromCircle.y };
const toP = { x: toCircle.x, y: toCircle.y };
const path = d3.path();
path.moveTo(fromP.x, fromP.y);
path.quadraticCurveTo(0, 0, toP.x, toP.y);
linesGroup
.append("path")
.style("fill", "none")
.style("stroke-width", "2px")
.style("stroke-dasharray", "10 10")
.style("stroke", "#000")
.attr("d", path);
}
Big thank you in advance, I am new to D3. I am trying to make a multiline chart that is animated, basically that acts as if the line is being "drawn" from left to right. I would like all the lines to be drawn at once.
Here is a link to the codepen project: https://codepen.io/amandaraen/project/editor/ZLLWBe
Basically, this is what I thought I should do based on Scott Murray's D3 book that I'm reading:
d3.transition().selectAll(".line")
.delay(function(d, i) { return i * 500; })
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d, i){
return colorScale(i);
});
But all this does is add the lines in their entirety one by one, instead, I would like for them to be unrolled. I have looked at lots of posts here and tried a lot of things but this is the closest I've come. Additionally, (and annoyingly) I had labels for this lines on the right hand side, but after I added the transition code the labels disappeared.
Edit, okay, I made a few changes and now, at least the labels have reappeared. So now it is only matter of how to make the lines appear like they are being drawn. Update:
var country = g.selectAll(".country")
.data(countries)
.enter().append("g")
.attr("class", "country");
// draw the lines
country.append("path")
.transition()
.duration(2000)
.delay(function(d, i) { return i * 500; } )
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d, i){
return colorScale(i);
});
EDIT:
Actually I would ignore my solution and try the stroke-dashed property described in this article
http://duspviz.mit.edu/d3-workshop/transitions-animation/
that I linked to through this question.
Animate line chart in d3
To make the line appear like it is drawn, use the attrTween along with the built in interpolator d3.interpolateNumber to set the x2, y2 values.
<!doctype html>
<html lang='en'>
<head>
<!-- Required meta tags -->
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<!-- Bootstrap CSS -->
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<title>Hello, world!</title>
</head>
<body>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src='https://code.jquery.com/jquery-3.4.1.min.js' integrity='sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=' crossorigin='anonymous'></script>
<script src='https://cdn.jsdelivr.net/npm/popper.js#1.16.0/dist/umd/popper.min.js' integrity='sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo' crossorigin='anonymous'></script>
<script src='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js' integrity='sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6' crossorigin='anonymous'></script>
<!-- d3 v5 -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.0/d3.js'></script>
<script>
let width = 500
let height = 300
//make up some lines
let lines = [
{
"x1": 0,
"y1": 0,
"x2": 400,
"y2": 250
},
{
"x1": 0,
"y1": 250,
"x2": 400,
"y2": 0
}
]
//add lines
let svg = d3.select( "body" ).style( "padding", "30px" ).append( "svg" ).attr( "height", height).attr( "width", width )
// Here you set the x1, y1, x2, y2 values of line to all be equal to x1, y1,
// Then in your transition, use interpolate the number between x1 and x2 and y1
// and y2 respectively.
svg.selectAll( ".line").data( lines )
.enter().append( "line" )
.attr( "class", "line" )
.attr( "stroke-width", 2 )
.attr( "stroke", "black" )
.attr( "x1", d => d.x1 )
.attr( "y1", d => d.y1 )
.attr( "x2", d => d.x1 )
.attr( "y2", d => d.y1 )
.transition().duration( 750 )
.attrTween( "x2", d => {
return d3.interpolateNumber( d.x1, d.x2 )
})
.attrTween( "y2", d => {
return d3.interpolateNumber( d.y1, d.y2 )
})
</script>
</body>
</html>
Since this is a line it was easy to use the build in interpolator, but if it was a path or something you can define your own. I found this to be a great reference.
https://www.andyshora.com/tweening-shapes-paths-d3-js.html
EDIT: I realized you were making paths. I'm sure there is a more elegant solution, but you can break the path up at each node to create list of segments. For example, if the path was ABCD you would get
A
AB
ABC
ABCD
Then you can each line with a delay so A is drawn, then AB, then ABC, etc.
let width = 500
let height = 300
//get some paths
let paths = [
getPath( .5, 0 ),
getPath( .25, 0 )
]
// for each path, take a length from 0-i
// So for path [(0,0), (1, 1), (2,2)] =>
// [ [(0,0)], [(0,0),(1,1)], [(0,0), (1,1), (2,2)] ]
let pathSegments = paths.map( p => segmentPath( p ))
//make the line generator
var lineGenerator = d3.line()
.x(function(d, i) {
return d.x;
})
.y(function(d) {
return d.y;
});
//add the svg
let svg = d3.select("body").style("padding", "30px").append( "svg" ).attr( "height", height).attr( "width", width)
//add the group elements which will contain the segments for each line - merge with update selection -
let lines = svg.selectAll( ".line" ).data( pathSegments )
lines = lines.enter().append( "g" )
.attr( "class", "line" )
.merge( lines )
//To add line to g, selectAll to add the segments - data bound to lines will be passed
// down to children if data is called - in this case .data( d => d ) - d is the segmented
// path [ [(0,0)], [(0,0),(1,1)], [(0,0), (1,1), (2,2)] ] for a single line
// add a delay on stroke-width to give the appearance of being drawn
lines.selectAll( ".segment" ).data( d => d )
.enter().append( "path" )
.attr( "class", "segment" )
.attr( "stroke", "black" )
.attr( "stroke-width", 0 )
.attr( "fill", "none" )
.attr( "d", lineGenerator )
.transition().duration(750)
.delay( (d,i) => i * 100 )
.attr( "stroke-width", 2 )
//helper functions
function getPath( m, b ) {
let line = [...Array(5).keys()].map( d => {
return {
"x": d * 100,
"y": m * d * 100 + b,
}
})
return line
}
function segmentPath( path ) {
let segments = []
for( let i=1; i < path.length; i++ ) {
segments.push( path.slice(0, i ))
}
return segments
}
Of course, you might want to remove the extra segments after adding. Also, it only works if the path segments are small enough to make a smooth transition. If not, segment the line further, or break the path into a set of lines and use the x1,y1,x2,y2 interpolation from above.
I am getting the following console error:
Error: <circle> attribute cy: Expected length, "NaN".
This is resulting in my circle elements being assigned a value of 0 and being rendered at the top of the screen.
My circles are being passed a Date value as the cy rather than a number, and I have tried using both scaleTime() and scaleLinear() but not had the desired effect.
<circle cx="41.9047619047619" cy="NaN" r="6" class="dot" data-xvalue="1995" data-yvalue="Mon Jan 01 1900 00:36:50 GMT+0000 (Greenwich Mean Time)"></circle>
Code
let width = 500;
let height = 500;
let margin = {left: 20, right: 20, top: 20, bottom: 20};
let timeFormat = d3.timeParse("%M:%S");
// select container and render svg canvas, set height and width.
const chart = d3.select('#container')
.append('svg')
.attr('height', height)
.attr('width', width);
// Fetch data
d3.json('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/cyclist-data.json').then(data => {
// Find highest and lowest years in dataset
const maxYear = d3.max(data, (d) => d.Year);
const minYear = d3.min(data, (d) => d.Year);
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d.Time = timeParser(d.Time);
});
// Define Scales
let xScale = d3.scaleLinear() // try scaleTime afterwards to check
.domain([minYear, maxYear])
.range([margin.left, width - margin.right]);
let yScale = d3.scaleTime()
.domain([d3.extent(data, (d) => d.Time)])
.range([0, height - margin.top])
// Render circles for each data point
chart.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', (d) => xScale(d.Year))
.attr('cy', (d) => yScale(d.Time))
.attr('r', 6)
.attr('class', 'dot')
.attr('data-xvalue', (d) => d.Year)
.attr('data-yvalue', (d) => d.Time)
})
I have tried the following alternatives when I am parsing the time data but still getting the same error:
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d["Time"] = timeParser(d.Time);
});
// Parse time data
data.forEach(d => {
let timeParser = d3.timeParse('%M:%S')
d.Time = new Date(timeParser(d.Time));
});
JSON data for reference:
{
"Time": "36:50",
"Place": 1,
"Seconds": 2210,
"Name": "Marco Pantani",
"Year": 1995,
"Nationality": "ITA",
"Doping": "Alleged drug use during 1995 due to high hematocrit levels",
"URL": "https://en.wikipedia.org/wiki/Marco_Pantani#Alleged_drug_use"
},
{
"Time": "36:55",
"Place": 2,
"Seconds": 2215,
"Name": "Marco Pantani",
"Year": 1997,
"Nationality": "ITA",
"Doping": "Alleged drug use during 1997 due to high hermatocrit levels",
"URL": "https://en.wikipedia.org/wiki/Marco_Pantani#Alleged_drug_use"
},
how can I force all xAxis labels to show up on my graph without defining each tick via .tickvalues ?
I'm totally new to nvd3 and this is my first try.
Maybe there is someone with a good heart who can have a look at my code and help me out.
I did my research but nothing worked for me.
Here is my code:
var data = [
{
"key": "www.WebsiteA.com",
"values": [
{date:"20151221",rank:1},
{date:"20151222",rank:3},
{date:"20151223",rank:2},
{date:"20151224",rank:4},
{date:"20151225",rank:2},
{date:"20151226",rank:5},
{date:"20151227",rank:3},
{date:"20151228",rank:2},
{date:"20151229",rank:2},
{date:"20151230",rank:1},
{date:"20151231",rank:2},
{date:"20160101",rank:4},
{date:"20160102",rank:5},
{date:"20160103",rank:3},
] },
{
"key": "www.WebsiteB.com",
"values": [
{date:"20151221",rank:2},
{date:"20151222",rank:1},
{date:"20151223",rank:3},
{date:"20151224",rank:5},
{date:"20151225",rank:1},
{date:"20151226",rank:4},
{date:"20151227",rank:1},
{date:"20151228",rank:5},
{date:"20151229",rank:3},
{date:"20151230",rank:4},
{date:"20151231",rank:2},
{date:"20160101",rank:1},
{date:"20160102",rank:3},
{date:"20160103",rank:2},
] },
]
nv.addGraph(function() {
var chart = nv.models.lineChart()
.x(function(d) {return d3.time.format("%Y%m%d").parse(d.date) })
.y(function(d) {return d.rank})
.yDomain([6, 1])
.color(d3.scale.category10().range())
.useInteractiveGuideline(true)
.margin({left: 100})
.margin({right: 50})
.margin({bottom: 100})
;
chart.legend.margin({top: 10, right:60, left:80, bottom: 100});
chart.xAxis
.tickFormat(function(d) {return d3.time.format('%Y-%m-%d')(new Date(d)) })
.rotateLabels(-45)
;
chart.xScale(d3.time.scale()); //fixes misalignment of timescale with line graph
chart.yAxis
.axisLabel('Rank')
.tickFormat(d3.format('d'))
;
d3.select('#chart svg')
.datum(data)
.transition().duration(500)
.call(chart)
;
nv.utils.windowResize(chart.update);
return chart;
});
You can find the fiddle here: http://jsfiddle.net/Marei/1azqmx1L/3/
Thank you!
I think (I have only worked directly with d3 before, not nvd3) that you need to specify a list of xValues that you want to display using tickValues().
So the first thing you need to do is to get a list of all your xValues (the order and/or duplicates do not matter):
//Map all xValues for each dataset to an array (tmp)
var tmp = data.map(function(e) {
return e.values.map(function(d) {
return d3.time.format('%Y%m%d').parse(d.date);
});
});
//And flatten out that array, so you have all your xValues in a 1D-array
var xValues = [].concat.apply([], tmp);
Then use this to set that you want to display all xValues:
chart.xAxis
.tickFormat(function(d) {return d3.time.format('%Y-%m-%d')(new Date(d)) })
.rotateLabels(-45)
.tickValues(xValues)
.showMaxMin(false)
;
showMaxMin needs is set to false because otherwise all the end-values must be displayed
I have string values for x,
so, for tickValues() i created an index based array with items length like,
axisLabel: graph.x_label,
tickValues: Array.from({ length: graph.values.length }, (x, i) => i),
I'm trying to use attrTween in d3 to animate a pie chart when the page is loaded but it's not working for me. I've used attrTween before to animate a change in data and it's worked fine but this time I want to 'grow' the pie chart when the page is loaded first but it's not behaving as expected and I'm not getting any information as to why this is.
If I remove the line .attrTween('d' arcTweenStart); then everything works fine except of course it does not animate. If the line is left in then nothing is displayed and the arcTweenStart function is never entered. Can anyone spot where I'm going wrong?
function drawCharts()
{
// Create the chart and bind the data to it and position it
var pieChart = d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter().append("svg")
.attr("id", "pie")
.attr("width", w) // Set th width
.attr("height", h) // Set the height
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")"); // Position the chart
// Create the pie chart layout
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null); // Sort is set to null to allow for better looking tweens
// Create "slices" for each data element
var arcs = pieChart.selectAll("g.slice")
.data(pie) // Bind the pie layout to the slices
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
// Create the graphics for each slice and colour them
arcs.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; })
.transition()
.duration(500)
.attrTween('d' arcTweenStart);
}
function arcTweenStart(b)
{
var start =
{
startAngle: b.startAngle,
endAngle: b.endAngle
};
var i = d3.interpolate(start, b);
return function(t)
{
return arc(i(t));
};
}
EDIT:
My data set looks like this:
var dataSet=
[
[
{ "label": "Green", "count": 40 },
{ "label": "Amber", "count": 50 },
{ "label": "Red", "count": 10 }
],
[
{ "label": "Green", "count": 20 },
{ "label": "Amber", "count": 30 },
{ "label": "Red", "count": 50 }
],
[
{ "label": "Green", "count": 50 },
{ "label": "Amber", "count": 20 },
{ "label": "Red", "count": 30 }
]
];
I have an array of data sets so I want to draw a chart for each one.
You don't show what your dataSet variable holds (that would have really helped answer the question!) but assuming your data looks like this:
var dataSet = [{
count: 4
}, {
count: 5
}, {
count: 6
}];
You don't need to do the first bind/enter:
d3.select("#groupRisk").selectAll("svg")
.data(dataSet) // Bind the data to the chart
.enter()
...
This would give you a pie chart for each entry in the data. Getting rid of that, your bind then becomes:
var arcs = pieChart.selectAll("g.slice")
.data(pie(dataSet)) //<-- call pie with the dataSet
.attr("id", "arcs")
.enter()
.append("g")
.attr("class", "slice");
But really to the heart of your question, your tween var start, has the same start/end angle as where you want to end. So, you animate the same thing over and over again. What I think you meant is:
function arcTweenStart(b) {
var start = {
startAngle: b.startAngle,
endAngle: b.startAngle //<-- set end to start and adjust on each call
};
var i = d3.interpolate(start, b);
return function(t) {
return arc(i(t));
};
}
Oh, and one typo in there too:
.attrTween('d' arcTweenStart); //<-- comma missing between 'd' and arcTweenStart
Example here.