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/
Related
I'm trying hard to make dashboard with Blazor. But I'm new to js and css so feel some hard.
Currently I'm implementing chart components and I'd like to add vertical line which is moved automatically like below image.
I searched a lot of pages, however I didn't even know what to do.
I'm not asking you to write code, but it would be nice to tell me about sites, methods or libraries that I can refer to.
Situation
I'm using Blazorise Chart .net core 5.0
My tries
Create a data set to be expressed like a vertical line, set a timer, and move the vertical line data once per second.(The StateHasChange() function should be continues to be called.
Draw vertical line using css(But I couldn't know how to move it)
Thank you for reading.
Since Blazorise uses ChartJs internally to draw the charts, you might be able to use the ChartJs annotation plugin. You might need to leverage a JavaScript Interop method to inject the plugin options.
Alternatively you can simply draw the line yourself, as the Chart renders in a <canvas /> element:
Heres a piece of code i use (Personally I use ChartJs.Blazor, and my chart is a Bar chart, so you will need to adapt the code to fit your own needs and library)
annotationConfig: function (canvasID, indexToDrawAt) {
var originalLineDraw = Chart.controllers.bar.prototype.draw;
Chart.helpers.extend(Chart.controllers.bar.prototype, {
draw: function () {
originalLineDraw.apply(this, arguments);
var chart = this.chart;
var ctx = chart.chart.ctx;
var index = chart.config.options.lineAtIndex;
if (index) {
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-0'];
var x1 = xaxis.left;
var y1 = yaxis.getPixelForValue(index);
var x2 = xaxis.right;
var y2 = yaxis.getPixelForValue(index);
ctx.save();
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.strokeStyle = '#3C3E4F';
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
}
});
let chart = window.ChartJsInterop.BlazorCharts.get(canvasID);
chart.options.lineAtIndex = indexToDrawAt;
chart.update();
}
This code adds a option to the chart called lineAtIndex, which when set, draws a line across the canvas at the provided index.
I use the method as such:
if (ShowAvgLine)
{
if (currentAverage != 0.0)
{
jSRuntime.InvokeVoidAsync("generalInterop.annotationConfig", _config.CanvasId, currentAverage);
}
}
If you get the above to work with your library, you might try your previous solution of making timed calls to the interop method to move it.
I don´t know Blazorise Chart, maybe there is also a built in solution.
If you want to solve it in a pretty hacky way, you could just create a <div> which has the same height as your chart. Then you can use the top and left CSS properties to set the div at the leftmost position over the chart (you may have to give the div a z-index and a position: absolute so that it appears over the chart). The positions should be saved in a C# variable. Now you can implement a function which gets called every second which increments the left value and calls the StateHasChanged() method.
This is a really hacky solution but it should work as expected. But you have to beware, that on different devices the chart can be displayed in a other way, so maybe the div won´t align anymore.
I'm using the d3 parcoords library. When I modify data plotted in a parallel coordinates chart, and reload the chart, the scale of the axis isn't refreshed.
I tried calling .autoscale(), but if I call it before .render(), it has no effect, and if I call it after .render(), then no polylines are drawn anymore.
$("#example").html(""); //to make sure "ghost" axes are removed
parcoords = d3.parcoords()("#example")
.data(parsedData)
.createAxes()
.brushMode("1D-axes")
.reorderable()//;
.detectDimensions()
.dimensions(dimensionObj)
.autoscale() //no visible effect if placed here
.render()
.updateAxes();
Not sure if this is related (although the issue started at the same time), I started specifying an order for the dimensions. To do this, instead of using an array containing the axes (dimensionArray), I use a JS object dimensionObj containing numbered "index" keys, as follows:
//dimensionArray = ["axis1", "axis2", "axis3", "axis4"]; //orderless array
//Default axes to display with predefined ordering
dimensionObj = {
"axis1": {index:0},
"axis2": {index:1},
"axis3": {index:2},
"axis4": {index:3}
}
For illustration purposes, the following screenshots show how on the top image, the scales are properly set, but on the second (updated) chart, some new polylines are going to 0 on the 1st and 3rd axis, but the scale isn't updated so lines go out of range.
Is there a simple way to refresh the axis scales when reloading a chart? Is it possible that using the JS object in .dimensions() is creating some conflicts with the underlying scale function?
Found what was causing this behaviour: an if statement in d3.parcoords.js pc.autoscale function which was only resetting scales if the yscale was not previously defined. Essentially I edited the if statement from original:
d3.keys(__.dimensions).forEach(function(k) {
if (!__.dimensions[k].yscale){
__.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
}
});
to this (of course the if statement could be dropped altogether, I simply kept it in this form in case I need to revert to original version later for any reason):
d3.keys(__.dimensions).forEach(function(k) {
if (!__.dimensions[k].yscale || __.dimensions[k].yscale){
__.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
}
});
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 trying to create several charts using the d3.chart framework. This seems like it would be a common use case: the whole point of d3.chart is for the charts to be reusable after all! If you can just point me to an example that would be awesome (:
I went through this (https://github.com/misoproject/d3.chart/wiki/quickstart) tutorial to create a very basic "circle graph". (I copied the code exactly). Now what I want to do is create a separate chart for several sets of data.
I edited it slightly.
Before editing, to set up the chart we called:
var data = [1,3,4,6,10];
var chart = d3.select("body")
.append("svg")
.chart("Circles")
.width(100)
.height(50)
.radius(5);
chart.draw(data);
I tried to change it to:
var data = [{key:1, values:[1,3,4,6,10]},
{key:2, values:[5,2,10,8,11]},
{key:3, values:[1,5,9,16,12]}]
var chart = d3.select("body")
.selectAll("chart)
.data(data)
.enter()
.append("svg")
.chart("Circles")
.width(100)
.height(50)
.radius(5);
chart.draw(function(d) { return d.values; });
This doesn't work however. You can see the corner of a circle in 3 diferent places, but
the rest of it is cut off. However if replace
chart.draw(function(d) { return d.values; });
with
chart.draw([1,3,4,6,10]);
it correctly generates 3 circle graphs, all with that one dataset. And when I add
chart.draw(function(d) { console.log(d.values) return d.values; });
The console shows the 3 arrays I'm trying to pass it! I don't understand what is happening here that's breaking the code. Shouldn't it be the exact same thing as passing the actual arrays to 3 separate charts?
Here's a link to the JS bin with it set up: http://jsbin.com/jenofovogoke/1/edit?html,js,console,output Feel free to mess around with it!
The code is wayyy at the bottom.
I'm pretty new to java script and d3, and entirely new to d3.chart. Any help would be super appreciated!
I asked Irene Ros, who helps run d3.chart, and she informed me that the problem is that d3.chart's draw method can only take an array or a data object- it cannot take a function. She gave me a few helpful hints for ways to get around this: by using a transform function to edit my data within the chart, rather than using a function, or by creating a chart that holds multiple charts (see https://gist.github.com/jugglinmike/6004102 for a great example of this).
However in the end I found the simplest solution for me was to manually set the data. It feels like a bit off a hack because D3 does this for you already, but it was much simpler than changing the whole set up of my chart, and allows for nested data (yay!).
var svg = d3.select("body")
.selectAll("svg")
.data(data)
.enter()
.append("svg");
svg.each(function(d, i) {
var chart = d3.select(this)
.chart("Circles")
.height(50)
.width(100)
.radius(5)
var data = d.values;
chart.draw(data);
});
Posting both question & answer here to save somebody else the same trouble later...
When I create two sunburst charts using d3.layout.partition, the first sunburst's slice proportions are overwritten by the second sunburst's slice proportions upon resize of the slices.
The two charts pass different .value accessor functions into the partition layout, e.g.
d3.layout.partition()
.sort(null)
.value(function(d) { return 1; });
vs.
d3.layout.partition()
.sort(null)
.value(function(d) { return d.size; });
And they generate their own list of nodes that are not shared between the two sunbursts. However, if I re-call the d3.svg.arc generator to resize to larger radius (but not change overall proportions), the slice angles are suddenly overwritten.
See the example here: http://bl.ocks.org/explunit/ab8cf15534f7fec5ac6d
The problem is that while partition.nodes() seems to generate a new data structure (e.g if you give it some .key functions, it writes the extra properties (e.g. .x, .y, .dx, dy) to the underlying data and does not make a copy of the data. Thus if the data structure is shared between the two charts, these .x, .y, .dx, dy properties will bleed through to the other graphs.
This seems like a bug to me, but in reading this old GitHub issue it seems to be treated as "by design". Perhaps it will be reconsidered in future versions.
One workaround is to use something like Lodash/Underscore cloneDeep or Angular's copy to make each chart have it's own copy of the data.
makeSunburst(null, _.cloneDeep(root), countAccessorFn);
makeSunburst(null, _.cloneDeep(root), sizeAccessorFn);
See example here: http://bl.ocks.org/explunit/e9efb830439247eea1be
An alternative to copying the whole dataset for each chart would be to simply recompute the partition before re-rendering.
Instead of having makeSunburst() be a function of the accessor, make it a function of the partition. Pass a different partition function to each chart:
// create separate partition variables
var countPartition = d3.layout.partition().sort(null).value(countAccessorFn);
var sizePartition = d3.layout.partition().sort(null).value(sizeAccessorFn);
// make the charts as a function of partition
charts.push(makeSunburst(root, countPartition));
charts.push(makeSunburst(root, sizePartition));
Then before applying the transition, simply update the nodes variable to reflect the associated partition:
addToRadius: function(radiusChange) {
radius += radiusChange;
ringRadiusScale.range([0, radius]);
// update the data before re-rendering each chart
nodes = partition.nodes(dataRoot);
path.transition().attr('d', arc);
}
Now when you update each chart, it is using the correct partition.
Here's an updated example.