I drew a multi-line chart that changes based on some user input. I want to add a reference line to always appear at the value y=100. I was able to manually place a line, but it is not always exactly at y=100.
Formatting issues aside, this is what I have for one possible user input. As you can see, the reference line is slightly below 100:
And my code:
const svg = d3.select("svg");
const width = +svg2.attr("width");
const height = +svg2.attr("height");
const render = data =>{
const xValue = d => +d.week;
const yValue = d => +d.power_score;
const margin = {top:50, right:70, bottom:60, left:20};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const colorValue = d => d.team;
// define scales
const xScale = d3.scaleLinear()
.domain([1, d3.max(data, xValue)])
.range([0, innerWidth-250])
.nice();
const yScale = d3.scaleLinear()
.domain([d3.min(data, yValue)-10, d3.max(data, yValue)+10])
.range([innerHeight, 0])
.nice();
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
const g = svg2.append("g")
.attr('transform', 'translate(75, 50)');
// create axes
const xAxis = d3.axisBottom(xScale)
.tickSize(-innerHeight-10);
const yAxis = d3.axisLeft(yScale)
.tickSize(-innerWidth+240);
const xAxisG = g.append("g").call(xAxis)
.attr("transform", "translate(0, 400)");
xAxisG.select(".domain")
.remove();
xAxisG.append("text")
.attr("class", "axis-label")
.attr("y", 40)
.attr("x", (innerWidth-250)/2)
.attr("fill", "black")
.text("Week");
const yAxisG = g.append("g").call(yAxis)
.attr("transform", "translate(-10, 0)")
.select(".domain")
.remove();
yAxisG.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -35)
.attr("x", -innerHeight/4)
.attr("fill", "black")
.text("Power Score");
// generate line
const lineGenerator = d3.line()
.x(d => xScale(xValue(d)))
.y(d => yScale(yValue(d)));
// sort data for legend
const lastYValue = d =>
yValue(d.values[d.values.length - 1]);
// group data
const nested = d3.nest()
.key(colorValue)
.entries(data)
.sort((a, b) =>
d3.descending(lastYValue(a), lastYValue(b)));
colorScale.domain(nested.map(d => d.key));
// manually add horizonal line here
svg2.append("g")
.attr("transform", "translate(75, 267)")
.append("line")
.attr("x2", innerWidth-250)
.style("stroke", "black")
.style("stroke-width", "2px")
.style("stroke-dasharray", "3, 3");
// add lines with mouseover effect
g.selectAll(".line-path").data(nested)
.enter().append("path")
.attr("class", "line-path")
.attr("d", d => lineGenerator(d.values))
.attr("stroke", d => colorScale(d.key))
.attr("stroke-width", "3")
.attr("opacity", "0.5")
.on("mouseover", function(d, i) {
d3.select(this).transition()
.duration("50")
.attr("stroke-width", "5")
.attr("opacity", "1")})
.on("mouseout", function(d, i) {
d3.select(this).transition()
.duration("50")
.attr("stroke-width", "3")
.attr("opacity", "0.5")});
d3.line()
.x(d => xScale(xValue(d)))
.y(d => yScale(yValue(d)));
// draw legend
const colorLegend = (selection, props) => {
const {
colorScale,
circleRadius,
spacing,
textOffset
} = props;
const groups = selection.selectAll('g')
.data(colorScale.domain());
const groupsEnter = groups
.enter().append('g')
.attr('class', 'tick');
groupsEnter
.merge(groups)
.attr('transform', (d, i) =>
`translate(0, ${i * spacing})`
);
groups.exit().remove();
groupsEnter.append('circle')
.merge(groups.select('circle'))
.attr('r', circleRadius)
.attr('fill', colorScale);
groupsEnter.append('text')
.merge(groups.select('text'))
.text(d => d)
.attr('dy', '0.32em')
.attr('x', textOffset);
}
svg2.append("g")
.attr("transform", "translate(710, 60)")
.call(colorLegend, {
colorScale,
circleRadius: 4,
spacing: 15,
textOffset: 8
});
// Title
g.append("text")
.attr("class", "title")
.attr("x", (innerWidth-250)/4)
.attr("y", -10)
.text("Weekly Power Ranks");
};
d3.csv('data.csv').then(data => {
data.forEach(d => {
d.week = +d.week;
d.power_score = +d.power_score;
});
render(data);
});
Instead of using magic numbers...
svg2.append("g")
.attr("transform", "translate(75, 267)")
//magic numbers---------------^----^
...use the same y scale you're using to paint the paths:
g.append("g")
.attr("transform", `translate(75, ${yScale(100)})`)
Also, append it to the already translated <g>, or apply the same translation (again, more magic numbers in your code...).
Related
I am trying to load a CSV data set into d3 by assigning it to a variable, but it seems that I keep receiving an error saying that enter() is not a function. I think the issue lies in the way I'm loading the CSV data.
For reference, I'm following this tutorial: http://duspviz.mit.edu/d3-workshop/scatterplots-and-more/
Here is my code for reference.
var ratData = [];
d3.csv("rat-data.csv", function(d) {
return {
city : d.city, // city name
rats : +d.rats // force value of rats to be number (+)
};
}, function(error, rows) { // catch error if error, read rows
ratData = rows; // set ratData equal to rows
console.log(ratData);
createVisualization(); // call function to create chart
});
function createVisualization(){
// Width and height of SVG
var w = 150;
var h = 175;
// Get length of dataset
var arrayLength = ratData.length; // length of dataset
var maxValue = d3.max(ratData, function(d) { return +d.rats;} ); // get maximum
var x_axisLength = 100; // length of x-axis in our layout
var y_axisLength = 100; // length of y-axis in our layout
// Use a scale for the height of the visualization
var yScale = d3.scaleLinear()
.domain([0, maxValue])
.range([0, y_axisLength]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Select and generate rectangle elements
svg.selectAll( "rect" )
.data( ratData )
.enter()
.append("rect")
.attr( "x", function(d,i){
return i * (x_axisLength/arrayLength) + 30; // Set x coordinate of rectangle to index of data value (i) *25
})
.attr( "y", function(d){
return h - yScale(d.rats); // Set y coordinate of rect using the y scale
})
.attr( "width", (x_axisLength/arrayLength) - 1)
.attr( "height", function(d){
return yScale(d.rats); // Set height of using the scale
})
.attr( "fill", "steelblue");
// Create y-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 75)
.attr("x2", 30)
.attr("y2", 175)
.attr("stroke-width", 2)
.attr("stroke", "black");
// Create x-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 175)
.attr("x2", 130)
.attr("y2", 175)
.attr("stroke-width", 2)
.attr("stroke", "black");
// y-axis label
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.text("No. of Rats")
.attr("transform", "translate(20, 20) rotate(-90)")
.attr("font-size", "14")
.attr("font-family", "'Open Sans', sans-serif");
}; // end of function
I have generated a Sankey diagram as shown above using d3 code (.js file) mentioned below [the .html and .css files are not quoted here].
Now I want the Sankey diagram to look like below with node "Technology" and "Strategy" appearing apart as a fourth level:
What are the necessary changes to be done in the D3 code?
var svg = d3.select("svg").attr("style", "outline: thin solid grey;"),
width = +svg.attr("width"),
height = +svg.attr("height");
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scaleOrdinal(d3.schemeCategory10);
var school = {"nodes": [
{"name":"High School"}, // 0
{"name":"Community College"}, // 1
{"name":"Finance"}, // 2
{"name":"Accounting"}, // 3
{"name":"ITS"}, // 4
{"name":"Marketing"}, // 5
{"name":"Analytics"}, // 6
{"name":"Security"}, // 7
{"name":"Consulting"}, // 8
{"name":"Banking"}, // 9
{"name":"Internal"}, // 10
{"name":"Securities"}, // 11
{"name":"Public"}, // 12
{"name":"Audting"}, // 13
{"name":"Internal"}, // 14
{"name":"Retail"}, // 15
{"name":"Technology"}, // 16
{"name":"Strategy"} // 17
],
"links":[
// FirstYear
{"source":0,"target":2,"value":33},
{"source":0,"target":3,"value":42},
{"source":0,"target":4,"value":74},
{"source":0,"target":5,"value":60},
// Community College
{"source":1,"target":2,"value":7},
{"source":1,"target":3,"value":13},
{"source":1,"target":4,"value":11},
{"source":1,"target":5,"value":9},
// Finance
{"source":2,"target":9,"value":16},
{"source":2,"target":10,"value":14},
{"source":2,"target":11,"value":10},
// Accounting
{"source":3,"target":12,"value":20},
{"source":3,"target":13,"value":12},
{"source":3,"target":7,"value":8},
{"source":3,"target":14,"value":15},
// Marketing
{"source":5,"target":6,"value":30},
{"source":5,"target":15,"value":39},
// ITS
{"source":4,"target":6,"value":40},
{"source":4,"target":7,"value":20},
{"source":4,"target":12,"value":6},
{"source":4,"target":8,"value":19},
// ITS Consulting
{"source":8,"target":16,"value":10},
{"source":8,"target":17,"value":9},
]};
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 1], [width - 1, height - 6]]);
var link = svg.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll("path");
var node = svg.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
sankey(school);
link = link
.data(school.links)
.enter().append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke-width", function(d) { return Math.max(1, d.width); });
// link hover values
link.append("title")
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
node = node
.data(school.nodes)
.enter().append("g");
node.append("rect")
.attr("x", function(d) { return d.x0; })
.attr("y", function(d) { return d.y0; })
.attr("height", function(d) { return d.y1 - d.y0; })
.attr("width", function(d) { return d.x1 - d.x0; })
.attr("fill", function(d) { return color(d.name.replace(/ .*/, "")); })
.attr("stroke", "#000");
node.append("text")
.attr("x", function(d) { return d.x0 - 6; })
.attr("y", function(d) { return (d.y1 + d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < width / 2; })
.attr("x", function(d) { return d.x1 + 6; })
.attr("text-anchor", "start");
svg.append("text")
.attr("x", 10)
.attr("y", 30)
.attr("class", "graphTitle")
.text("STUDENT CHOICES");
svg.append("text")
.attr("x", width - 80)
.attr("y", height - 10)
.attr("class", "footnote")
.text("data is fictitious");
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-sankey#0"></script>
<svg width="600" height="500"></svg>
The alignment of d3.sankey can be configured using .nodeAlign(), and for your requirement, you will need .nodeAlign(d3.sankeyLeft)
If it is not specified, the alignment defaults to d3.sankeyJustify, which is what you are currently seeing.
https://github.com/d3/d3-sankey#alignments
For those who are looking for a quick ans. Have a nice day!
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.nodeAlign(function (node) {
// you may specify the horizatonal location here
// i.e. if your data structure contain node.horizontalPosition (an integer)
// you can return node.horizontalPosition
return node.depth; //align left
})
.extent([[1, 1], [width - 1, height - 6]]);
I am new to D3 and have been working along with examples and changing correcting my code using those examples,
Below is my D3 Code and it works well except the bar chart is not scaled, I seem to not understand the error on my part.
The data is coming from a TSV file and has 2 columns, 1- Categorical 2- Numerical.
<script type = "text/javascript">
var margin = { top:80, bottom:80, right:80, left:80},
width = 960 - margin.left-margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin. bottom)
.append("g")
.attr("transform", "translate("+margin.left+"," + margin.bottom+")");
d3.tsv("ticket.tsv",function(error, data) {
dataset = data.map(function(d) { return [d["ticket"],+d["num"] ] ;})
var xScale = d3.scale.ordinal()
.domain(data.map(function(d){ return [d.ticket];}))
.rangeRoundBands([0,width],0.1);
var yScale = d3.scale.linear()
.domain([0,d3.max(data,function(d) { return Math.max([d.num]);})])
.range([height, 0]);
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x",function(d,i) { return xScale(d.ticket);})
.attr("y",function (d) { return height - (yScale (d.num)) ;})
.attr("height",function (d) { return d.num;})
.attr("width",xScale.rangeBand())
.attr("fill","orange");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0"+","+(height)+")")
.call(xAxis);
svg.append("g")
.attr("class", "axis")
.call(yAxis);
;});
</script>
Here is the Solved Code and Picture, I have commented the code I have corrected.
<script type = "text/javascript">
var margin = { top:80, bottom:80, right:80, left:80},
width = 960 - margin.left-margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin. bottom)
.append("g")
.attr("transform", "translate("+margin.left+"," + margin.bottom+")");
d3.tsv("tickets.tsv",function(error, data) {
dataset = data.map(function(d) { return [d["ticket"],+d["num"] ] ;})
var xScale = d3.scale.ordinal()
.domain(data.map(function(d){ return [d.ticket];}))
.rangeRoundBands([0,width],0.1);
var yScale = d3.scale.linear()
.domain([0,d3.max(data,function(d) { return Math.max([d.num]);})])
.range([height,0]);
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x",function(d) { return xScale(d.ticket);})
.attr("y",function (d) { return yScale (d.num) ;})**//I made the change HERE**
.attr("height",function (d) { return height - yScale(d.num);})**// and HERE**
.attr("width",xScale.rangeBand())
.attr("fill","orange");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0"+","+(height)+")")
.call(xAxis);
svg.append("g")
.attr("class", "axis")
.call(yAxis);
;});
</script>
I am using foreignObject to use HTML inside a d3 circle.
What I want:
use HTML inside d3 objects
reach the normal scopes outside of HTML code
Now I have problems to get the right scope for using functions.
If I use "this" inside the
.html(".....")
I get the "g" element of d3. So is there a way to get to my normal scopes? Or is there even a more elegant way to solve this problem?
var foreign = that._foreignObject = d3.select(this).append("foreignObject")
.attr("width", 30)
.attr("height", 30)
.append("xhtml:div")
.style("font", "15px 'Arial'")
.html("<button name=Klickmich type=button value=play onclick=>");
EDIT: 16.12.2013:
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
//click once
.on("click", function (d) {
if (that._foreignObject) {
that._foreignObject.remove();
}
that.toggle(d);
that.update(d);
that.onNodeClick(d);
var circle = d3.select(this).select("circle")
.transition()
.duration(750)
.attr("r", 17)
.attr("width", 40)
.attr("heigth", 40)
.style("stroke-dasharray", ("5,2"))
.style("stroke-width", 2)
.style("stroke", "black");
var button = "<button name=Klickmich type=button value=play onclick=>";
var foreign = that._foreignObject = d3.select(this).append("foreignObject")
.attr("width", 30)
.attr("height", 30)
.append("xhtml:div")
.style("font", "15px 'Arial'")
.html(button)
.on("click", function (d) {
console.log("heyho");
});
d3.select(this).select("text").transition()
.duration(750)
.style("font-size", 15)
.style("font-weight", "bold")
.attr("x", function (d) {
return d.children || d._children ? -20 : -20;
});
})
If i Click on my html button i set
this._clicked = 1;
And the onClick function of the node is build as follows:
.on("click", function (d) {
if (this._clicked == null) {
// some code
}
}
this._clicked = null;
I am trying to apply the colors from the color = d3.scale.category10(); var to the gradient for the circle svg, what am I doing wrong? All I am seeing is the first color of the color = d3.scale.category10();(which is blue) to 0% opacity gradient but that is all. If I take the gradient out then I see the range I want which is from 1-4? Thanks in advance!
var nodes = d3.range(300).map(function() { return {radius: Math.random() * 12 + 4}; }),
root = nodes[0],
color = d3.scale.category10();
root.radius = 0;
root.fixed = true;
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) { return i ? 0 : -4000; })
.nodes(nodes)
.size([width, height]);
force.start();
var svg = d3.select("body").append("svg:svg")
.attr("width", width)
.attr("height", height);
var gradient = svg.append("defs").append("radialGradient")
.attr("id", "gradient")
.attr("cx", "50%")
.attr("cy", "50%");
gradient.append("stop")
.attr("offset", "75%")
.style("stop-color", function(d, i) { return color(i % 4); })
.attr("stop-opacity", "1");
gradient.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d, i) { return color(i % 4); })
.attr("stop-opacity", ".1");
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.style("fill", "url(#gradient)");
Your stop elements don't have any data joined with them, so in your function (d, i), i will always be 0. If you just want the two stops, you could do something like this:
gradient.append("stop")
.attr("offset", "75%")
.style("stop-color", color(0))
.attr("stop-opacity", "1");
gradient.append("stop")
.attr("offset", "100%")
.style("stop-color", color(1))
.attr("stop-opacity", ".1");
If instead you're just trying to fade the edges of your circles, a gradient isn't what you want at all. Instead, you'll need to apply a solid color to each circle, then create a single opacity-only gradient inside a mask, and apply that mask to each circle. Something like this:
var defs = svg.append('defs');
var gradient = defs.append('radialGradient')
.attr('id', 'fadient');
gradient.append('stop')
.attr('offset', '75%')
.attr('stop-color', 'white')
.attr('stop-opacity', 1)
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'white')
.attr('stop-opacity', .1)
var mask = defs.append('mask')
.attr('id', 'mask')
.attr('maskContentUnits', 'objectBoundingBox')
.append('circle')
.attr('fill', 'url(#fadient)')
.attr('cx', .5)
.attr('cy', .5)
.attr('r', .5)
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr('cx', function (d, i) { return 20 * i })
.attr('cy', 50)
.attr("r", function(d) { return d.radius; })
.attr('mask', 'url(#mask)')
.attr("fill", function (d, i) { return color(i); });