Data doesn't show in my line chart. It just shows the y-axis and x-axis.
My data arrays are like this:
time_list = ['00:00:00', '01:00:00', '02:00:00']
val_list = [0.7274859768018719, 0.6894762867153069, 0.6994151884676558]
I set up the chart like this:
<div id="#chart-cpu-performance"></div>
<script type="text/javascript">
var cpuperformance = dc.lineChart("#chart-cpu-performance");
var connection = new WebSocket(`ws://localhost:8001/websocket`);
function render_plots(dim, val) {
cpuperformance
.width(990)
.height(540)
.dimension(dim)
.group(val)
.x(d3.scaleBand().domain(['00:00:00', '04:00:00', '08:00:00','12:00:00',
'16:00:00', '20:00:00','23:00:00']).rangeRound([0,100]))
.y(d3.scaleLinear().domain([0,1]))
.curve(d3.curveLinear)
.renderArea(false)
.renderDataPoints(true);
cpuperformance.render();
}
connection.onmessage = function(event){
//get data and parse
var newData = JSON.parse(event.data);
var updateObject = []
newData.time_list.forEach(function additem(item, index) {
updateObject.push({time: item, avg_value: newData.val_list[index]})
});
var xfilter = crossfilter(updateObject)
var timeDim = xfilter.dimension(function(d) {return d.time;});
var avg_vPertime = timeDim.group().reduceSum(function(d) {return +d.avg_value;});
render_plots(timeDim, avg_vPertime)
}
</script>
Did I miss some parameters for cpuperformance in the render_plots function?
Yes, you missed xUnits. Also I suggest using .elasticX(true) rather than specifying the domain yourself.
Unfortunately most coordinate grid charts are not able to determine automatically what kind of data keys they are dealing with, the primary kinds being numeric, ordinal, and time.
So if your data is ordinal, you need to have
.xUnits(dc.units.ordinal)
A lot of the logic is different for charts with an ordinal X scale, and this parameter directs dc.js to use the ordinal logic. Other values tell bar charts how wide to make the bars for the numeric (dc.units.integers, dc.units.fp.precision) and time scales (d3.timeHours etc).
Also in this example, only one of the data points matches the domain you passed to scaleBand. So you'll only see one point.
It's easier to use
.elasticX(true)
.x(d3.scaleBand().rangeRound([0,100]))
and let the chart figure out what to put in the domain.
Fiddle with a working version of your code.
Related
I want to modify the default transition on a multi-series chart and managed to achieve it using the "pretransition" event listener, but doing it this way seems to disable the highlighting you get "for free" with the DC legend. Assuming Michelson–Morley experiment data is loaded the usual way my code looks like this:
// MULTI-SERIES LINE CHART
const runDimension = morley.dimension(d => [d.Expt, d.Run]);
const runGroup = runDimension.group().reduceSum(d => d.Speed);
const multiSeries = new dc.SeriesChart("#multi-series");
multiSeries
.width(500)
.height(300)
.chart(cht => dc.lineChart(cht).curve(d3.curveBasis))
.dimension(runDimension)
.group(runGroup)
.keyAccessor(d => d.key[1])
.valueAccessor(d => d.value)
.seriesAccessor(d => d.key[0])
.legend(dc.legend().x(50).y(250).itemHeight(15).gap(5).horizontal(2).legendWidth(500))
.x(d3.scaleLinear().domain([1, 20]));
multiSeries.on("pretransition", function(chart) {
const path = chart.selectAll("path.line");
const totalLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1000)
.attr("stroke-dashoffset", 0);
});
Is there a way to keep both the custom transition and the legend functionality?
Note: there are three answers below. The first is a red herring, the second is a klunky solution, and the final answer nails it! I'm preserving the false leads because they contain some useful info.
namespacing event listeners
To avoid conflicts, you can add a name as documented in D3's dispatch.on:
The type may be optionally followed by a period (.) and a name; the optional name allows multiple callbacks to be registered to receive events of the same type, such as start.foo and start.bar.
In your example:
multiSeries.on("pretransition.foo", function(chart) {
Use whatever name you want, as long as it's unique.
Internally, dc.js should do this as well, to prevent conflicts proactively.
legend matching
Digging deeper into your example, the actual source of conflict is that the legend matches with associated charts based on the color and the dashstyle. (Yes, it is weird that the legend matches items based on their attributes rather than by ID.)
I was able to get a basic example working by changing the legend items, but I think it was only because totalLength is incorrect (0).
let leg = chart.legendables;
chart.legendables = function() {
return leg.apply(chart).map(l => ({...l, dashstyle: [0,0]}));
}
It also changed the legend from squares to lines:
Fiddle.
Anyway, that's the reason it wasn't working - nothing to do with events, just the weird way the legend works. Chances are this solution won't work if totalLength and the dashstyle are correct. I didn't see the affect of your transitions, so it's likely something is wrong.
overriding the line chart
It's pretty easy to override the logic in the line chart so that it only looks at colors, not dash styles.
One way to do this is by deriving an ES6 class from it:
class LineChartNoDashMapping extends dc.LineChart {
_colorFilter (color, dashstyle, inv) {
return function () {
const item = d3.select(this);
const match = item.attr('stroke') === color || item.attr('fill') === color;
return inv ? !match : match;
};
}
}
_colorFilter is copied straight from dc.LineChart (source) and we just remove the clause having to do with dash styles. (We also have to qualify d3.select.)
Plug our new class into the series chart:
.chart(function(c) { return new LineChartNoDashMapping(c).curve(d3.curveCardinal); })
Now the legendables override isn't needed, and the legend works as expected. Much nicer!
New fiddle.
I'm trying to make a Pie chart that shows each months expenses on different category.Like,when I give January ,the slices should display Grocery,fuel,rent etc.How can I make it with this data?
code
function show_monthly_exp_distribution(ndx) {
var dim = ndx.dimension(dc.pluck('Month'));
var group = dim.group().reduceCount(dc.pluck());
dc.pieChart("#exp-pie")
.height(300)
.width(800)
.radius(70)
.transitionDuration(1000)
.dimension(dim)
.group(group)
.legend(dc.legend().gap(7));
}
csv data
Month,Utility Bills,Groceries,Dining Out,Fuel,Rent,totalexp
January,100,500,100,90,1000,1400
February,120,450,50,120,1000,1740
March,130,550,120,60,1000,1860
April,100,300,80,150,1000,1630
May,90,600,75,80,1000,1845
June,130,560,150,90,1100,2030
July,70,610,120,100,1100,2000
August,120,459,100,80,1100,1859
September,140,432,80,90,1100,1842
October,60,456,110,110,1100,1836
November,80,487,60,180,1200,2007
December,150,390,210,100,1200,2050
As Gordon suggested, start by changing the format of your data so you have only one datapoint per row. If you can't change the data on the csv, you can do it in js (code not tested, might work directly... or not ;)
var data = [];
var type = "Utility Bills,Groceries,Dining Out,Fuel,Rent".split(",");
d3.csv("yourcsv", function(d) {
type.forEach(function(c){
data.push({month:d.month,type:c,amount:+d[c]});
});
return null; //doesn't matter what you return, discard the initial csv anyway)
}).then (function(dummy){
ndx=crossfilter(data);
});
Once you have done that, you can easily have one graph to filter the month (or selectMenu graph) and your pieChart
I'm mapping a series of points with amChart; after loading the data from an external JSON source, the map re-centers instead of staying at the point I'd set earlier with chart.homeGeoPoint.
I believe I need to use an event listener and set the homeGeoPoint after the map renders the points... but I'm at a bit of a loss; the only events I've found are from dataSource.events, and those appear to be related to fetching/parsing the JSON, as opposed to rendering the map.
// Create map instance
var chart = am4core.create("chartdiv", am4maps.MapChart);
// Set map definition
chart.geodata = am4geodata_region_world_northAmericaLow;
// Set projection
chart.projection = new am4maps.projections.Miller();
// Initial Position / Zoom
chart.homeZoomLevel = 2.6;
chart.homeGeoPoint = {
latitude: 39,
longitude: -96.2456
};
// Series for World map
var worldSeries = chart.series.push(new am4maps.MapPolygonSeries());
worldSeries.useGeodata = true;
// Markers
// Create image series
var imageSeries = chart.series.push(new am4maps.MapImageSeries());
// Create a circle image in image series template so it gets replicated to all new images
var imageSeriesTemplate = imageSeries.mapImages.template;
var circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 5;
circle.fill = am4core.color("#116ad6");
circle.stroke = am4core.color("#FFFFFF");
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = "{title}";
// Set property fields
imageSeriesTemplate.propertyFields.latitude = "latitude";
imageSeriesTemplate.propertyFields.longitude = "longitude";
imageSeriesTemplate.propertyFields.url = "url";
// Load data
imageSeries.dataSource.url = "/foo/map-points.php";
imageSeries.dataSource.parser = new am4core.JSONParser();
imageSeries.dataSource.parser.options.emptyAs = 0;
// Center after render
imageSeries.dataSource.events.on("done", function(ev) {
// This doesn't work - perhaps it is firing too early?
chart.homeGeoPoint = {
latitude: 39,
longitude: -96.2456
};
});
By request, here is a foo.json file for expirmenting with.
[{"title":"ISP","url":"\/airport\/kisp\/","latitude":40.7952,"longitude":-73.1002},{"title":"AEX","url":"\/airport\/kaex\/","latitude":31.3274,"longitude":-92.5486}]
What do I need to do to make sure the map stays centered on my desired location after the JSON data are loaded and rendered?
I've created an issue on GitHub in regards to why the map re-positions on the MapImageSeries' dataSource load and how to better work with that. (If you've a GitHub account, please subscribe to the issue.)
In the meantime, presuming the first time your dataSource gets its data that the user hasn't moved the map and we want to maintain homeGeoPoint as the current center, we can chain events to achieve that.
When the dataSource is "done" with its data, that doesn't necessarily imply anything has been done on the actual map level. The data still needs to propagate to the MapImageSeries, that still needs to create MapImages per data item, have the data validated/parsed there, and for whatever reason the map position shifts around. So the first time that happens (using events.once instead of events.on), we then listen for the MapImageSeries' "datavalidated" event also only one time (because "datavalidated" will have run before this, e.g. as soon as you create the MapImageSeries, if no data is supplied or it's taking some time, it will still run the event and the "inited" event, i.e. I guess you can say the series itself will successfully render nothing).
And to center the map we use chart.goHome(0);, this method will zoom to your homeGeoPoint and homeZoomLevel, the 0 is for how long the animation duration should run, i.e. just do the work, don't animate.
So all that together will look something like this:
// Center after render
imageSeries.dataSource.events.once("done", function(ev) {
imageSeries.events.once("datavalidated", function() {
chart.goHome(0);
});
});
Here's a demo:
https://codepen.io/team/amcharts/pen/239bfdc8689c65468df32d71b29759b8
Even though the map does re-position once the MapImageSeries loads, then it re-centers with the above code, I haven't actually seen the map shift at all anymore. So it looks to me the above code is doing the job of maintaining the homeGeoPoint. Let me know if that is still the case once implemented in your application.
I am trying to realize a dashboard to display basic data.
I am actually completely stuck on an issue. Strangely enough, I couldn't find anything even similar to it online, so I don't have many leads on how to move forward.
I have mainly two charts:
a lineChart called "stackChart" that
displays consumption as a base layer with its valueAccessor function
dispalys production as a stacked layer with its value Accessor function
a barChart called "volumeChart" that is simply the rangeChart for the lineChart
I use radio buttons to select whether to aggregate the grouped data by sum or by average (using the same approach as this example) and then I just use:
stackChart.valueAccessor(/*function with new value (avg or sum)*/);
dc.redrawAll();
to refresh the base layer (consumption).
What I don't manage to do is to refresh the "stacked layer" by updating its valueAccessor! I can't find any way to access its valueAccessor (or, worst case, just completely remove the stacked layer and then add a new refreshed stacked layer using just ".stack(...)").
Here is the respective part of my code where the chart is built:
// Charts customization #js
stackChart
.renderArea(true)
.height(350)
.transitionDuration(1500)
.dimension(dateDim)
.group(powByTime, "Consumption")
// BASE LAYER valueAccessor HERE
.valueAccessor(function(d) { return d.value.conSum; })
.x(d3.time.scale().domain([minDate, maxDate]))
.xUnits(d3.time.days)
.elasticY(true)
.renderHorizontalGridLines(true)
.legend(dc.legend().x(80).y(0).itemHeight(13).gap(5))
.brushOn(false)
// STACKED LAYER HERE
.stack(powByTime, "Production", function(d) { return d.value.prodSum; })
.rangeChart(volumeChart)
.controlsUseVisibility(true)
;
And here is where I look for changes in the radio buttons and re-draw the layers:
// Listen for changes
d3.selectAll('#select-operation input')
.on('click', function() {
var aggrMode = this.value; // fetch "avg" or "sum" from buttons
// UPDATE BASE LAYER HERE:
stackChart.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; });
// ???HOW TO UPDATE STACKED LAYER valueAccessor function???
//stackChart.stack.valueAccessor(function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
dc.redrawAll();
});
If you need more details on what I am trying to do and full code you can check here.
As a reference, here is what it looks like:
I don't really know dc.js, but it may be possible that you can't change an accessor once it's been set. Try writing a single function for your accessor that will return either the sum or the average, depending on the state of some variable that you can set.
#Ryan's solution will probably work fine (and may be a better design), but here's the lowdown on the dc.js API with respect to stacking, in case you need it.
As described in this issue the group and stack API is pretty weird. It grew organically, in a backward-compatible way, so both the stacks and the value accessors on top of the stacks sort of branch out in a beautiful fractal of... well, no it's pretty messy.
But the issue also suggests the solution for your problem. Since chart.group() resets the set of stacks, just go ahead and build them all from scratch in your event handler:
stackChart.group(powByTime, "Consumption") // this resets the stacks
.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; })
.stack(powByTime, "Production", function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
Internally it's just emptying an array of layers/stacks and then populating it with some references.
This is quite efficient since dc.js doesn't store your data except where it is bound to the DOM elements. So it is the same amount of work to redraw using the old group and value accessor as it is to redraw using new ones.
I've created an NVD3 multiBarChart and placed it in a jQuery resizable container. When resizing the chart, each render incurs the same delay as when the chart is first drawn: staggered left-to-right delayed drawing of the bars. This looks cool when the chart is first drawn, but it's a nuisance when resizing the chart. I've experimented with nv.d3.css, reducing every delay to 0ms to no avail. Haven't yet inspected the NVD3 JS and am hoping not to need to.
Fiddle:
http://jsfiddle.net/a5Fnj/10/
var container = $("#mycontainer");
$(container[0]).resizable();
var svg = d3.select(container[0]).append("svg");
nv.addGraph(function () {
var chart = nv.models.multiBarChart();
chart.xAxis.tickFormat(d3.format(',f'));
chart.yAxis.tickFormat(d3.format(',.1f'));
d3.select(container[0]).select("svg")
.datum(exampleData())
.transition().duration(0).call(chart);
nv.utils.windowResize(chart.update);
this.stackedbar = chart;
});
function exampleData() {
return stream_layers(3, 10 + Math.random() * 100, .1).map(function (data, i) {
return {
key: 'Stream' + i,
values: data
};
});
}
As of NVD3 1.7.1 you can use the duration option:
chart.duration(0);
I used transitionDuration: -1 that worked for a stackedAreaChart.
Edit
This helped remove the transition when appending chart data, not the re-size issue, please check the comments below.
In the latest version (from github), you can set .transitionDuration():
chart.transitionDuration(0);
Edit: Even with this, some of the transitions/durations are hardcoded in the NVD3 source. The only way to get rid of those is to modify the source.