i'm using a rowchart to show the total of sales by item of a salesman.
Already tried a composite chart unsuccessfully like many posts from the google, but none of the examples uses a rowchart.
I need to do like the image, creating the red lines to represent the sale value target for each item, but i dont know how, can you guys help me? Thanks!
Actually this is my code to plot the rowchart
spenderRowChart = dc.rowChart("#chart-row-spenders");
spenderRowChart
.width(450).height(200)
.dimension(itemDim)
.group(totalItemGroup)
.elasticX(true);
Obviously you need a source for the target data, which could be a global map, or a field in your data.
I've created an example which pulls the data from a global, but it would also take from the data if your group reduction provides a field called target.
Then, it adds a new path element to each row. Conveniently the rows are already SVG g group elements, so anything put in there will already be offset to the top left corner of the row rect.
The only coordinate we are missing is the height of the rect, which we can get by reading it from one of the existing bars:
var height = chart.select('g.row rect').attr('height');
Then we select the gs and use the general update pattern to add a path.target to each one if it doesn't have one. We'll make it red, make it visible only if we have data for that row, and start it at X 0 so that it will animate from the left like the row rects do:
var target = chart.selectAll('g.row')
.selectAll('path.target').data(function(d) { return [d]; });
target = target.enter().append('path')
.attr('class', 'target')
.attr('stroke', 'red')
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M0,0 v' + height;
}).merge(target);
The final .merge(target) merges this selection into the main selection.
Now we can now animate all target lines into position:
target.transition().duration(chart.transitionDuration())
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M' + (chart.x()(d.value.target || _targets[d.key] || 0)+0.5) + ',0 v' + height;
});
The example doesn't show it, but this will also allow the targets to move dynamically if they change or the scale changes. Likewise targets may also become visible or invisible if data is added/removed.
thank you, due the long time to have an answer i've developed a solution already, but, really thank you and its so nice beacause its pretty much the same ideia, so i think its nice to share the code here too.
The difference its in my code i use other logic to clear the strokes and use the filter value of some other chart to make it dynamic.
.renderlet(function(chart) {
dc.events.trigger(function() {
filter1 = yearRingChart.filters();
filter2 = spenderRowChart.filters();
});
})
.on('pretransition', function(chart) {
if (aux_path.length > 0){
for (i = 0; i < aux_path.length; i++){
aux_path[i].remove();
}
};
aux_data = JSON.parse(JSON.stringify(data2));
aux_data = aux_data.filter(venda => filter1.indexOf(venda.Nome) > -1);
meta_subgrupo = [];
aux_data.forEach(function(o) {
var existing = meta_subgrupo.filter(function(i) { return i.SubGrupo === o.SubGrupo })[0];
if (!existing)
meta_subgrupo.push(o);
else
existing.Meta += o.Meta;
});
if (filter1.length > 0) {
for (i = 0; (i < Object.keys(subGrupos).length); i++){
var x_vert = meta_subgrupo[i].Meta;
var extra_data = [
{x: chart.x()(x_vert), y: 0},
{x: chart.x()(x_vert), y: chart.effectiveHeight()}
];
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear);
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.extra').data([extra_data]);
path = path.enter()
.append('path')
.attr('class', 'oeExtra')
.attr('stroke', subGruposColors[i].Color)
.attr('id', 'ids')
.attr("stroke-width", 2)
.style("stroke-dasharray", ("10,3"))
.merge(path)
path.attr('d', line);
aux_path.push(path);
}
}
})
And that's how it looks
Assume I have a histogram script that builds a 960 500 svg graphic. How do I make this responsive so on resize the graphic widths and heights are dynamic?
<script>
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) { return d.x; }))
.rangeRoundBands([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
.range([0, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("rect")
.data(histogram)
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return height - y(d.y); })
.attr("height", function(d) { return y(d.y); });
svg.append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height)
.attr("y2", height);
</script>
Full example histogram gist is:
https://gist.github.com/993912
There's another way to do this that doesn't require redrawing the graph, and it involves modifying the viewBox and preserveAspectRatio attributes on the <svg> element:
<svg id="chart" viewBox="0 0 960 500"
preserveAspectRatio="xMidYMid meet">
</svg>
Update 11/24/15: most modern browsers can infer the aspect ratio of SVG elements from the viewBox, so you may not need to keep the chart's size up to date. If you need to support older browsers, you can resize your element when the window resizes like so:
var aspect = width / height,
chart = d3.select('#chart');
d3.select(window)
.on("resize", function() {
var targetWidth = chart.node().getBoundingClientRect().width;
chart.attr("width", targetWidth);
chart.attr("height", targetWidth / aspect);
});
And the svg contents will be scaled automatically. You can see a working example of this (with some modifications) here: just resize the window or the bottom right pane to see how it reacts.
Look for 'responsive SVG' it is pretty simple to make a SVG responsive and you don't have to worry about sizes any more.
Here is how I did it:
d3.select("div#chartId")
.append("div")
.classed("svg-container", true) //container class to make it responsive
.append("svg")
//responsive SVG needs these 2 attributes and no width and height attr
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 600 400")
//class to make it responsive
.classed("svg-content-responsive", true);
The CSS code:
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
}
.svg-content-responsive {
display: inline-block;
position: absolute;
top: 10px;
left: 0;
}
More info / tutorials:
http://demosthenes.info/blog/744/Make-SVG-Responsive
http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php
I've coded up a small gist to solve this.
The general solution pattern is this:
Breakout the script into computation and drawing functions.
Ensure the drawing function draws dynamically and is driven of
visualisation width and height variables (The best way to do this is
to use the d3.scale api)
Bind/chain the drawing to a reference
element in the markup. (I used jquery for this, so imported it).
Remember to remove it if it's already drawn. Get the dimensions from
the referenced element using jquery.
Bind/chain the draw function to
the window resize function. Introduce a debounce (timeout) to this
chain to ensure we only redraw after a timeout.
I also added the minified d3.js script for speed.
The gist is here: https://gist.github.com/2414111
jquery reference back code:
$(reference).empty()
var width = $(reference).width();
Debounce code:
var debounce = function(fn, timeout)
{
var timeoutID = -1;
return function() {
if (timeoutID > -1) {
window.clearTimeout(timeoutID);
}
timeoutID = window.setTimeout(fn, timeout);
}
};
var debounced_draw = debounce(function() {
draw_histogram(div_name, pos_data, neg_data);
}, 125);
$(window).resize(debounced_draw);
Enjoy!
Without Using ViewBox
Here is an example of a solution that does not rely on using a viewBox:
The key is in updating the range of the scales which are used to place data.
First, calculate your original aspect ratio:
var ratio = width / height;
Then, on each resize, update the range of x and y:
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
Note that the height is based on the width and the aspect ratio, so that your original proportions are maintained.
Finally, "redraw" the chart – update any attribute that depends on either of the x or y scales:
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y.range()[1] - y(d.y); })
.attr("height", function(d) { return y(d.y); });
}
Note that in re-sizing the rects you can use the upper-bound of the range of y, rather than explicitly using the height:
.attr("y", function(d) { return y.range()[1] - y(d.y); })
var n = 10000, // number of trials
m = 10, // number of random variables
data = [];
// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
for (var s = 0, j = 0; j < m; j++) {
s += Math.random();
}
data.push(s);
}
var histogram = d3.layout.histogram()
(data);
var width = 960,
height = 500;
var ratio = width / height;
var x = d3.scale.ordinal()
.domain(histogram.map(function(d) {
return d.x;
}))
var y = d3.scale.linear()
.domain([0, d3.max(histogram, function(d) {
return d.y;
})])
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", height);
var rects = svg.selectAll("rect").data(histogram);
rects.enter().append("rect");
function redraw() {
rects.attr("width", x.rangeBand())
.attr("x", function(d) {
return x(d.x);
})
// .attr("y", function(d) { return height - y(d.y); })
.attr("y", function(d) {
return y.range()[1] - y(d.y);
})
.attr("height", function(d) {
return y(d.y);
});
}
function resize() {
x.rangeRoundBands([0, window.innerWidth]);
y.range([0, window.innerWidth / ratio]);
svg.attr("height", window.innerHeight);
}
d3.select(window).on('resize', function() {
resize();
redraw();
})
resize();
redraw();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Lots of complex answers here.
Basically all you need to do is ditch the width and height attributes in favor of the viewBox attribute:
width = 500;
height = 500;
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
If you have margins, you can just add them there into the width/height then just append the g thereafter and transform it like you would normally.
If you are using d3.js through c3.js the solution to the responsiveness issue is quite straightforward :
var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());
where the generated HTML looks like :
<div id="chart">
<svg>...</svg>
</div>
In the case that you are using a d3 wrapper like plottable.js, be aware that the easiest solution might be adding an event listener and then calling a redraw function (redraw in plottable.js). In the case of plottable.js this will work excellently (this approach is poorly documented):
window.addEventListener("resize", function() {
table.redraw();
});
Shawn Allen's answer was great. But you may not want to do this every single time. If you host it on vida.io, you get automatic responsive for your svg visualization.
You can get responsive iframe with this simple embed code:
<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>
#vida-embed iframe {
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
http://jsfiddle.net/dnprock/npxp3v9d/1/
Disclosure: I build this feature at vida.io.
In case people are still visiting this question - here’s what worked for me:
Enclose the iframe in a div and use css to add a padding of, say, 40% to that div (the percentage depending on the aspect ratio you want). Then set both width and height of the iframe itself to 100%.
In the html doc containing the chart to be loaded in the iframe, set width to the width of the div that the svg is appended to (or to the width of the body) and set height to width * aspect ratio.
Write a function that reloads the iframe content upon window resize, so as to adapt the size of the chart when people rotate their phone.
Example here on my website:
http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website
UPDATE 30 Dec 2016
The approach I described above has some drawbacks, especially that it doesn’t take the height into account of any title and captions that are not part of the D3-created svg. I’ve since come across what I think is a better approach:
Set the width of the D3 chart to the width of the div it’s attached to and use the aspect ratio to set its height accordingly;
Have the embedded page send its height and url to the parent page using HTML5’s postMessage;
On the parent page, use the url to identify the corresponding iframe (useful if you have more than one iframe on your page) and update its height to the height of the embedded page.
Example here on my website: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution
One of the basic principles of the D3 data-join is that it is idempotent. In other words, if you repeatedly evaluate a data-join with the same data, the rendered output is the same. Therefore, as long as you render your chart correctly, taking care withe your enter, update and exit selections - all you have to do when the size changes, is re-render the chart in its entirety.
There are a couple of other things you should do, one is de-bounce the window resize handler in order to throttle it. Also, rather than hard-coding widths / heights, this should be achieved by measuring the containing element.
As an alternative, here is your chart rendered using d3fc, which is a set of D3 components that correctly handle data-joins. It also has a cartesian chart that measures it containing element making it easy to create 'responsive' charts:
// create some test data
var data = d3.range(50).map(function(d) {
return {
x: d / 4,
y: Math.sin(d / 4),
z: Math.cos(d / 4) * 0.7
};
});
var yExtent = fc.extentLinear()
.accessors([
function(d) { return d.y; },
function(d) { return d.z; }
])
.pad([0.4, 0.4])
.padUnit('domain');
var xExtent = fc.extentLinear()
.accessors([function(d) { return d.x; }]);
// create a chart
var chart = fc.chartSvgCartesian(
d3.scaleLinear(),
d3.scaleLinear())
.yDomain(yExtent(data))
.yLabel('Sine / Cosine')
.yOrient('left')
.xDomain(xExtent(data))
.xLabel('Value')
.chartLabel('Sine/Cosine Line/Area Chart');
// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.y; })
.decorate(function(selection) {
selection.enter()
.style('stroke', 'purple');
});
var cosLine = fc.seriesSvgArea()
.crossValue(function(d) { return d.x; })
.mainValue(function(d) { return d.z; })
.decorate(function(selection) {
selection.enter()
.style('fill', 'lightgreen')
.style('fill-opacity', 0.5);
});
var gridlines = fc.annotationSvgGridline();
// combine using a multi-series
var multi = fc.seriesSvgMulti()
.series([gridlines, sinLine, cosLine]);
chart.plotArea(multi);
// render
d3.select('#simple-chart')
.datum(data)
.call(chart);
You can see it in action in this codepen:
https://codepen.io/ColinEberhardt/pen/dOBvOy
where you can resize the window and verify that the chart is correctly re-rendered.
Please note, as a full disclosure, I am one of the maintainers of d3fc.
I would avoid resize/tick solutions like the plague since they are inefficient and can cause issues in your app (e.g. a tooltip re-calculates the position it should appear on window resize, then a moment later your chart resizes too and the page re-layouts and now your tooltip is wrong again).
You can simulate this behaviour in some older browsers that don't properly support it like IE11 too using a <canvas> element which maintains it's aspect.
Given 960x540 which is an aspect of 16:9:
<div style="position: relative">
<canvas width="16" height="9" style="width: 100%"></canvas>
<svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
</svg>
</div>
You can also use bootstrap 3 to adapt the size of a visualization. For example, we can set up the HTML code as:
<div class="container>
<div class="row">
<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>
</div>
</div>
I have set up a fixed height because of my needs, but you can leave the size auto as well. The "col-sm-6 col-md-4" makes the div responsive for different devices. You can learn more at http://getbootstrap.com/css/#grid-example-basic
We can access the graph with the help of the id month-view.
I won't go into much detail about the d3 code, I will only input the part that is needed for adapting to different screen sizes.
var width = document.getElementById('month-view').offsetWidth;
var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;
The width is set by getting the width of the div with the id month-view.
The height in my case should not include the entire area. I also have some text above the bar so I need to calculate that area as well. That's why I identified the area of the text with the id responsivetext. For calculating the allowed height of the bar, I subtracted the height of the text from the height of the div.
This allows you to have a bar that will adopt all the different screen/div sizes. It might not be the best way of doing it, but it surely works for the needs of my project.
I have two datasets in my chart and I'm switching between them by clicking to the paragraphs (I'm drawing the buttons there later). The datasets have the different dimensions so I want to replace one another in the tooltip according to the chosen dataset. I can adjust the tooltip once
.on("mouseover", function(d, i) {
var tickDate = d3.select(d3.selectAll(".axis .tick text")[0][i]).data()[0];
console.log (tickDate);
var formatDate = RU.timeFormat("%B %Y");
var tooltipDate = formatDate(tickDate);
//Get this bar's x/y values, then augment for the tooltip
var xPosition = parseFloat(d3. select(this). attr("x")) + ((barWidth - barPadding) / 2);
var yPosition = parseFloat(d3. select(this). attr("y")) / 2 + h / 2;
//Update the tooltip position and value
d3.select("#tooltip" )
.style("left" , xPosition + "px")
.style("top" , yPosition + "px")
.select("#value")
.text(d + " DIMENSION1");
d3.select("#tooltip" )
.select("#label")
.text(tooltipDate);
//Show the tooltip
d3.select("#tooltip" ).
classed("hidden" , false);
d3.select(this)
.attr("fill", "orange");
})
but I don't manage to refresh it after the switching. There is the text "DIMENSION1" in both cases now, my purpose is the text "DIMENSION2" appearance after switching and return "DIMENSION1" after the choosing of the initial dataset.
Here is my fiddle: https://jsfiddle.net/anton9ov/dw1xp1ux/
Several problems here :
Avoid code duplication by creating a function transition(dataset, dimension) called for the transitions
You don't update the mouseover event when you change you dataset. So call your mouseover function each time you update the data
svg.selectAll("rect").on("mouseover", function(d, i) {
// Your mouseover function
});
See this fiddle https://jsfiddle.net/bfz97hdw/
(I also fixed the color issue)
I am working with NVD3 to make a sunburst chart. I'm new to D3 in general so I'm using NVD3 to abstract some of the more complicated things away. Right now I am working with a simple example I got from the web, but am trying to color the nodes (arcs) based on the size attribute in the JSON. I know that NVD3 has the option to use a color function in the chart options so that's what I tried to use like so:
chart: {
type: 'sunburstChart',
height: 450,
color: function(d) {
if (d.size > 3000) {
return "red";
} else if (d.size <= 3000) {
return "green";
} else {
return "gray";
}
},
duration: 250
}
But as you can see from the plunker I am working on that results in just gray, because it isn't actually getting a value from d.size (it's just undefined and I'm not sure why).
Using regular D3 which I am trying to avoid, I can get the desire result from this:
var path = g.append("path")
.attr("class","myArc")
.attr("d", arc)
.attr("name",function(d){return "path Arc name " + d.name;})
.style("fill", function(d) {
if(d.size > 3000) {
return "green";
} else if( d.size < 3000){
return "red";
} else {
return "gray";
}
})
.on("click", click)
... //etc
I had modified a regular D3 sunburst example to get that with the desired result:
I know the labels are jacked up but that isn't important here. I'd just like to get the same result as regular D3 but with the abstraction of NVD3. I haven't found any good examples that mention using the color: function() at all. Thanks in advance.
Firstly use these javascript libraries from github distributions:
<script src="http://krispo.github.io/angular-nvd3/bower_components/nvd3/build/nv.d3.js"></script>
<script src="http://krispo.github.io/angular-nvd3/bower_components/angular-nvd3/dist/angular-nvd3.js"></script>
Chart options should be like this:
$scope.options = {
"chart": {
"type": "sunburstChart",
"height": 450,
"duration": 250,
"width": 600,
"groupColorByParent": false,//you missed this
color: function(d, i) {
//search on all data if the name is present done to get the size from data
var d2 = d3.selectAll("path").data().filter(function(d1) {
return (d1.name == d)
})[0];
//now check the size
if (d2.size > 3000) {
return "red";
} else if (d2.size <= 3000) {
return "green";
} else {
return "gray";
}
},
duration: 250
}
}
working code here
I have been trying to add a popup displaying the data type shown in each layer of my stacked area chart. Here is what I have done so far:
Basically when I mouse over either layer of the stacked area chart, im getting the same type displayed in the popup, I can see why this is, but I have no idea how to make it state one value when mouse is over that layer, and the other value when its over the second layer.
Please could anyone help?
/* y-axis scale is the largest data item scaled to height of the panel */
yScale = pv.Scale.linear(0, pv.max(data, function (d) {
var totalValue = 0;
for (i = 0; i < stackLayers.length; i++) {
totalValue = totalValue + d[stackLayers[i]];
}
return totalValue;
})).range(0, h),
colors = pv.Colors.category20().range();
/* render the area's using the predefined protovis stack layout */
vis.add(pv.Layout.Stack)
.layers(stackLayers)
.values(data)
.x(function (d) {
return xScale(d.x);
})
.y(function (d, p) {
return yScale(d[p]);
})
.layer.add(pv.Area)
.interpolate("cardinal")
.tension(0.97)
.fillStyle(function (d) {
return colors[this.parent.index];
})
.text(function (d) {
if(stackLayers[i]== "HAL")
{return "Dave";}
else
{return "Sorry";}
/* return (stackLayers[i]== "Hal" ? "Dave" : "Sorry");*/
})
.event("mouseover", pv.Behavior.tipsy({gravity:"s", fade:true}))
.anchor("top").add(pv.Line)
.strokeStyle(function (d) {
return colors[this.parent.index].darker();
})
.interpolate("cardinal")
.tension(0.97)
.lineWidth(2);
/* Y-axis rule. */
vis.add(pv.Rule)
.data(yScale.ticks(5))
.left(0)
.width(w)
.bottom(yScale)
.strokeStyle(function (d) {
return d ? "rgba(128,128,128,.2)" : "#000";
})
.anchor("left").add(pv.Label)
.text(yScale.tickFormat);