I am following Mike Bostock's pattern for reusable charts - closures with getter/setters. But somehow, when I instantiate a new chart object with new properties, existing charts are being updated with these properties:
var chart1 = new StackedAreaChart();
d3.select('#chart1')
.data([data])
.call(chart1);
// a new chart - whose properties end up in chart1!
var chart2 = new StackedAreaChart().colors(['green', 'blue']);
Here's an example: http://jsfiddle.net/samselikoff/YZ6Ea/3/. Resize the window to see the first chart re-rendered, with a stylish but unexpected green tint.
I am baffled by this. I suspect (1) my setter is mutating the actual 'constructor', and (2) somehow chart1 is really a reference to this constructor, rather than being a separate instance of it like I thought.
I was thinking of using this.margin = ... instead of var margin =, but since this is the suggested pattern I wanted to post here first. What am I missing?
If you are following Mike's tutorial, you shouldn't be using new when creating the chart:
var chart1 = StackedAreaChart();
d3.select('#chart1')
.data([data])
.call(chart1);
StackedAreaChart will return a function, and that function will be called once for each element in the selection when the data is binded.
Related
I encapsulated d3 charts into function as suggested best practice from creator in blog Towards Reusable Charts. Is it possible to create optional functionalities on top of this chart, so calling specific function would trigger it, otherwise it would be omitted.
Working JSFiddle example (base working example from Rob Moore's blog)
In JS line 56 I added a function which I'd like to create and then conditionally call in line 67.
My current way of doing it, is creating a boolean and setting it to false and then calling function with argument true. Problem of doing it this way is that the code gets too many conditionals and edge cases.
P.S. this question is not meant to be a discussion how to correctly apply axis to the chart. This is just an example.
I think it's better to add additional functionalities after the chart is drawn
var runningChart = barChart().barPadding(2).fillColor('coral');
d3.select('#runningHistory')
.datum(milesRun)
.call(runningChart);
runningChart.x_axis(); // additional functionality
So that the original chart container can be saved in a variable and it can be used to append other functionalities. For example
function barChart() {
var charContainer;
function chart(selection){
charContainer = d3.select(this).append('svg')
.attr('height', height)
.attr('width', width);
}
chart.x_axis = function() {
// Add scales to axis
var x_axis = d3.axisBottom()
.scale(widthScale);
charContainer.append('g').call(x_axis);
return chart;
}
}
If there is any need to add additional functionality before the chart is drawn, then all the functionalities can be saved in a Javascript object and drawn like in this example. https://jsfiddle.net/10f7hdae/
For a time series visualization in d3, I want to highlight years on the axis. I've accomplished this by making my own xAxis renderer, which invokes the native axis function and then implements my own custom logic to format the ticks that it renders.
This is how I've done it (see working example on jsbin):
xAxis = d3.svg.axis()
.scale(xScale)
customXAxis = function(){
xAxis(this);
d3.selectAll('.tick', this)
.classed("year", isYear);
};
...
xAxis.ticks(10);
xAxisElement = canvas.append("g")
.classed("axis x", true)
.call(customXAxis);
This gets the job done, but feels wrong; and it hasn't really extended the axis, it's only wrapped it. Ideally my customXAxis would inherit the properties of d3's axis component, so I would be able to do things like this:
customXAxis.ticks(10)
Thanks to #meetamit and #drakes for putting this together. Here's what I've ended up with: http://bl.ocks.org/HerbCaudill/ece2ff83bd4be586d9af
Yep, you can do all that. Following mbostock's suggestions here in conjunction with `d3.rebind' you get:
// This outer function is the thing that instantiates your custom axis.
// It's equivalent to the function d3.svg.axis(), which instantiates a d3 axis.
function InstantiateCustomXAxis() {
// Create an instance of the axis, which serves as the base instance here
// It's the same as what you named xAxis in your code, but it's hidden
// within the custom class. So instantiating customXAxis also
// instantiates the base d3.svg.axis() for you, and that's a good thing.
var base = d3.svg.axis();
// This is just like you had it, but using the parameter "selection" instead of
// the "this" object. Still the same as what you had before, but more
// in line with Bostock's teachings...
// And, because it's created from within InstantiateCustomXAxis(), you
// get a fresh new instance of your custom access every time you call
// InstantiateCustomXAxis(). That's important if there are multiple
// custom axes on the page.
var customXAxis = function(selection) {
selection.call(base);
// note: better to use selection.selectAll instead of d3.selectAll, since there
// may be multiple axes on the page and you only want the one in the selection
selection.selectAll('.tick', this)
.classed("year", isYear);
}
// This makes ticks() and scale() be functions (aka methods) of customXAxis().
// Calling those functions forwards the call to the functions implemented on
// base (i.e. functions of the d3 axis). You'll want to list every (or all)
// d3 axis method(s) that you plan to call on your custom axis
d3.rebind(customXAxis, base, 'ticks', 'scale');// etc...
// return it
return customXAxis;
}
To use this class, you just call
myCustomXAxis = InstantiateCustomXAxis();
You can now also call
myCustomXAxis
.scale(d3.scale.ordinal())
.ticks(5)
And of course the following will continue to work:
xAxisElement = canvas.append("g")
.classed("axis x", true)
.call(myCustomXAxis);
In summary
That's the idiomatic way to implement classes within d3. Javascript has other ways to create classes, like using the prototype object, but d3's own reusable code uses the above method — not the prototype way. And, within that, d3.rebind is the way to forward method calls from the custom class to what is essentially the subclass.
After a lot of code inspection and hacking, and talking with experienced d3 people, I've learned that d3.svg.axis() is a function (not an object nor a class) so it can't be extended nor wrapped. So, to "extend" it we will create a new axis, run a selection on the base axis() to get those tick marks selected, then copy over all the properties from the base axis() in one fell swoop, and return this extended-functionality version.
var customXAxis = (function() {
var base = d3.svg.axis();
// Select and apply a style to your tick marks
var newAxis = function(selection) {
selection.call(base);
selection.selectAll('.tick', this)
.classed("year", isYear);
};
// Copy all the base axis methods like 'ticks', 'scale', etc.
for(var key in base) {
if (base.hasOwnProperty(key)) {
d3.rebind(newAxis, base, key);
}
}
return newAxis;
})();
customXAxis now fully "inherits" the properties of d3's axis component. You can safely do the following:
customXAxis
.ticks(2)
.scale(xScale)
.tickPadding(50)
.tickFormat(dateFormatter);
canvas.append("g").call(customXAxis);
*With the help of #HerbCaudill's boilerplate code, and inspired by #meetamit's ideas.
Demo: http://jsbin.com/kabonokeki/5/
I'm working with an issue of missing series when inverting a chart with more than one serie. It worked before we upgraded to v3.0 of Highcharts. So the technique of "dynamically" inverting the chart, is to copy the previous charts options, change the inverted option, then creating a new chart with these options.
options = jQuery.extend(true, {}, chart.options);
options.chart.inverted = isInverted;
chart.destroy();
chart = new Highcharts.Chart(options);
This worked "nicely" before. But now it only keeps one serie. Tried all sorts of solutions, copying the serie object, and adding each serie to the new object, without luck because the copied object uses pointers.
Any takers ?
I finally found the reason:
the chart.options object didn't contain the series, just the series object contained them. The series had been added using Chart.addSeries(). Shouldn't this method also make a options.series object?
Anyways, pushed in a new object in the array, got the settings from chart.series, and voila!
I would like to know, how can I add peoperties to some KineticJS object. For example - I create two rectangles and connect them with a Line. And I need the object "line" knows about the two rectangles.
I could create a class Connector with atributtes object1, object2 and line (Kinetic.Line). But I can add to canvas only the line, so that I would lost the reference to Connector object, if I tried to get the line from canvas - for example after clicking on that.
If I understand your question correctly, its fairly simple
var rect1 = new Kinetic.Rect({...});
var rect2 = new Kinetic.Rect({...});
var line = new Kinetic.Line({...});
line.r1 = rect1;
line.r2 = rect2;
Now you can simply access the 2 rectangles by using line.r1 and line.r2
I'm struggling to understand the correct way to update a highcharts chart. Supposing I have rendered a chart, and then I want to update it in some way. For instance, I may want to change the values of the data series, or I may want to enable dataLabels.
At the moment the only way I can figure out how to do this is to alter the chart options, and use new Highcharts.chart to tell highcharts to redraw.
However, I'm wondering whether this may be overkill and it might be possible to alter the chart 'in situ', without having to start from scratch with new Highcharts.chart. I notice there is a redraw() method, but I can't seem to get it to work.
Any help is very much appreciated.
Thanks,
Robin
Sample code is as follows and at the bottom there is a jsFiddle
$(document).ready(function() {
chartOptions = {
chart: {
renderTo: 'container',
type: 'area',
},
series: [{
data: [1,2,3]
}]
};
chart1 = new Highcharts.Chart(chartOptions);
chartOptions.series[0].data= [10,5,2];
chart1 = new Highcharts.Chart(chartOptions);
//The following seems to have no effect
chart1.series[0].data = [2,4,4];
chart1.redraw();
});
http://jsfiddle.net/sUXsu/18/
[edit]:
For any future viewers of this question, it's worth noting there is no method to hide and show dataLabels. The following shows how to do it: http://jsfiddle.net/supertrue/tCF8Y/
chart.series[0].setData(data,true);
The setData method itself will call the redraw method
you have to call set and add functions on chart object before calling redraw.
chart.xAxis[0].setCategories([2,4,5,6,7], false);
chart.addSeries({
name: "acx",
data: [4,5,6,7,8]
}, false);
chart.redraw();
var newData = [1,2,3,4,5,6,7];
var chart = $('#chartjs').highcharts();
chart.series[0].setData(newData, true);
Explanation:
Variable newData contains value that want to update in chart. Variable chart is an object of a chart. setData is a method provided by highchart to update data.
Method setData contains two parameters, in first parameter we need to pass new value as array and second param is Boolean value. If true then chart updates itself and if false then we have to use redraw() method to update chart (i.e chart.redraw();)
http://jsfiddle.net/NxEnH/8/
#RobinL as mentioned in previous comments, you can use chart.series[n].setData(). First you need to make sure you’ve assigned a chart instance to the chart variable, that way it adopts all the properties and methods you need to access and manipulate the chart.
I’ve also used the second parameter of setData() and had it false, to prevent automatic rendering of the chart. This was because I have multiple data series, so I’ll rather update each of them, with render=false, and then running chart.redraw(). This multiplied performance (I’m having 10,000-100,000 data points and refreshing the data set every 50 milliseconds).