How to render timeseries/categorical stacked bar chart in dc.js, filtering stack and bar - dc.js

Most examples I have found are using data that has time and number
var data = [
{
"Time": "19-Jan-2018 11:24:49.000 UTC",
"Speed": 1.885
},
{
"Time": "19-Jan-2018 11:24:59.000 UTC",
"Speed": 1.875
},
{
"Time": "19-Jan-2018 11:25:00.000 UTC",
"Speed": 1.878
},
{
"Time": "19-Jan-2018 11:25:01.000 UTC",
"Speed": 1.876
}
]
I am looking to stack type
var data = [
{
"Time": "19-Jan-2018 11:24:49.000 UTC",
"type": "CAT"
},
{
"Time": "19-Jan-2018 11:24:59.000 UTC",
"type": "DOG"
},
{
"Time": "19-Jan-2018 11:25:00.000 UTC",
"type": "CAT"
},
{
"Time": "19-Jan-2018 11:25:01.000 UTC",
"Type": "BAT"
}
]
How can I stack categorical data, while allowing the user to select time/category pairs, as in the following Example?

I adapted the example to time/category data in this fiddle.
Those dates would only parse in Chrome, so
const parseDate = d3.utcParse("%d-%b-%Y %H:%M:%S.%L UTC");
data.forEach(d => {
d.Time = parseDate(d.Time);
})
I changed the key functions to use ,
function multikey(x,y) {
return x + ',' + y;
}
function splitkey(k) {
return k.split(',');
}
I also changed fake group stack_second to convert string-dates from the multikeys back into Dates, and to initialize categories to 0 (since every stack has to be present for every X).
function stack_second(group, categories) {
return {
all: function() {
var all = group.all(),
m = {};
// build matrix from multikey/value pairs
all.forEach(function(kv) {
var ks = splitkey(kv.key);
m[ks[0]] = m[ks[0]] || Object.fromEntries(categories.map(c=>[c,0]));
m[ks[0]][ks[1]] = kv.value;
});
// then produce multivalue key/value pairs
return Object.keys(m).map(function(k) {
return {key: new Date(k), value: m[k]};
});
}
};
}
Get the array of categories from the source data:
const categories = Array.from(new Set(data.map(d => d.Type)).values());
When dealing with date/time, you have to choose a d3 time interval appropriate for your data. Here minutes looked right. Using UTC d3-time methods everywhere because your source data is UTC.
const interval = d3.utcMinute;
Calculate xscale domain and apply:
let extent = d3.extent(data, d=>d.Time);
extent[1] = interval.offset(extent[1], 1)
chart
.x(d3.scaleTime().domain(extent))
.xUnits(interval.range)
Right number of ticks, also formatted in UTC:
chart.xAxis().ticks(d3.utcMinute).tickFormat(d3.utcFormat('%H:%M'))
Match colors between stacks and wedges with
.colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
Crossfilter initialization, using the interval and categories:
const interval = d3.utcMinute; // choose appropriate to your data
var cf = crossfilter(data),
timeTypeDim = cf.dimension(function(d) { return multikey(interval(d.Time), d.Type); }),
timeTypeGroup = timeTypeDim.group(), // reduceCount by default
stackedGroup = stack_second(timeTypeGroup, categories);
And here's the chart code for completeness, although we've already discussed the relevant parts:
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
chart
.width(600)
.height(400)
.colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
.controlsUseVisibility(true)
.x(d3.scaleTime().domain(extent))
.xUnits(interval.range)
.margins({left: 80, top: 20, right: 10, bottom: 20})
.brushOn(false)
.clipPadding(10)
.title(function(d) {
return d.key + '[' + this.layer + ']: ' + d.value[this.layer];
})
.legend(dc.legend().x(540).y(50))
.dimension(timeTypeDim)
.group(stackedGroup, categories[0], sel_stack(categories[0]))
.renderLabel(true);

Related

Need to display crossfilter data array values in separate lines instead of comma separated values in a single line

My data looks like below:
var data = [{
"Id": "1",
"startTime": 1554750660000,
"endTime": 1554751320000,
"Alerts": [
"Alert1","Alert2"
],
"Apps": [
"App1","App2"
]
}]
I have to display above data using crossfilter-data-table.
Currently, I am able to display the data like below:
StartTime EndTime Apps Alerts
04/08/2019 12
12:42:00 PM 12:49:00 PM App1,App2 Alert1,Alert2
Instead, I want to show it in below format.
Changes are: the array values of columns Apps, Alerts are not displayed as comma separated values. Instead they are shown in a separate line one after the other.
StartTime EndTime Apps Alerts
04/08/2019 12
12:42:00 PM 12:49:00 PM App1 Alert1
App2 Alert2
My current code looks like below:
import React from "react";
import * as dc from "dc";
import "dc/dc.css";
import * as d3 from "d3";
import { ChartTemplate } from "./chartTemplate";
import { css } from "glamor";
const tableFunc = (divRef, ndx) => {
const summaryTable = dc.dataTable(divRef);
const dimension = ndx.dimension(d => d.StartTime);
summaryTable
.dimension(dimension)
.showGroups(true)
.size(10000)
.group(d => {
return d.hour;
})
.columns([
{
label: "Start Time",
format: function(d) {
return d. startTime;
}
},
{
label: "End Time",
format: function(d) {
return d. endTime;
}
},
{
label: "Apps",
format: function(d) {
return d.Apps;
}
},
{
label: "Alerts",
format: function(d) {
return d.Alerts;
}
}
])
.sortBy(function(d) {
return d.startTime;
})
.order(d3.descending)
.on("renderlet", function(table) {
table.selectAll(".dc-table-group").classed("info", true);
});
return summaryTable;
};
const style = css({
"& tr": {
"&:hover": {
background: "#dddd"
}
},
"& td": {
textAlign: "left",
borderTop: "1px solid #ddd"
}
});
export const DataTable = props => (
<ChartTemplate
chartFunction={tableFunc}
styles={style}
title="Summary"
/>
);
What change i should do to display details in required format as above.Please help I'm really new to react, crossfilter and d3.
Assuming it's okay to display all the array values in one cell, I think the easiest way to get what you want would be simply to format the cell using <br> - the line break element.
E.g.
{
label: "Apps",
format: function(d) {
return d.Apps.join('<br>');
}
},
It would be a lot more difficult to create extra rows, since data table rows usually correspond 1:1 with rows of the data set.
Adding links
#zubug55 commented
In my real data, alerts look like:
"Alerts": [ "abc#link1","def#link2" ]
How do I break each abc#link1 on # and make abc a link to the url link1?
It's probably easiest to generate the anchor elements in HTML at the same time, like
return d.Alerts.map(function(a) {
var parts = a.split('#');
return '' + parts[0] + '';
}).join('<br>');

Showing sum of multiple values with amCharts

I would like to show sum of multiple values as one chart output with amCharts. I am using dataLoader with JSON to get the data. I know I have to create a function for but I couldn't understand how to get the data from the dataLoader to calculate
{
"balloonText": "[[title]] of [[valueAxis]]:[[value]]",
"lineThickness": 3,
"id": "sumValue",
"title": "sum Value",
"valueField": (function() {
var sumValues = "calculation";
return sumValues
}
this attempt is probably not correct but this is how I started
{
"balloonText": "[[title]] of [[valueAxis]]:[[value]]",
"lineThickness": 3,
"id": "LoadigTime",
"title": "Loadig Time",
"valueField": (function() {
var sumValues = (HomePageLoad + LoginToParametersLoad + ParametersLoad + AlarmsLoad + SwitchSideLoad + LoginToAdminLoad + AdminLoad) / 7;
return sumValues
})
}
valueField cannot be a function, only a string reference to a field in your data.
If the chart is meant to be displaying the sum of all of those fields in your data as a chart, simply add logic to your postProcess callback to create a new dataset containing your sums, e.g.
postProcess: function(data) {
var newData = [];
data.forEach(function(dataItem) {
var item = {
YOUR_CATEGORY_FIELD: dataItem.YOUR_CATEGORY_FIELD, //replace with your category field name
sum: 0
};
//loop through your item's keys and sum everything up, filtering out
//your category property
item.sum = Object.keys(dataItem).reduce(function(sum, key) {
if (key !== "YOUR_CATEGORY_FIELD") {
sum += dataItem[key]
}
return sum;
}, 0);
newData.push(item);
});
return newData;
},
// ...
graphs: [{
valueField: "sum",
// other props here
}]

Using dc.js with data dimension in array

I have an array with the following data for each node
[{
"id": "2130483",
"appId": "SIGERprod",
"time": 1500,
"dateStart": 1521564131000,
"timestamp": 1521564131000,
"dateEnd": 1521564131000,
"ipAddress": "10.110.11.111",
"principalName": "suiza_2#hotmail.com",
"contextPath": "/prueba",
"ruta": "/prueba/xhtml/formasPreCodificadas/llenarForma.xhtml",
"metodo": "POST",
"status": 200,
"year": 2018,
"month": 3,
"day": 20,
"hour": 10,
"minute": 42,
"second": 11
}]
I have the following code and this code grouped my request per date
$.getJSON(prefix + "/getFullData.htm", {idApp: App, dateStart: inicio, dateEnd: fin},
function (data) {
data.forEach(function (d) {
d.date = new Date(d.year, d.month, d.day, d.hour, d.minute, d.second, 0);
});
ndx = crossfilter(data);
dateDimension = ndx.dimension(function (d) {
return d.date;
});
var peticiones = dateDimension.group().reduceCount();
var minDate = dateDimension.bottom(1)[0].date;
var maxDate = dateDimension.top(1)[0].date;
solicitudesXIntervalo
.width(1000)
.height(200)
.mouseZoomable(true)
.dimension(dateDimension)
.group(peticiones)
.x(d3.time.scale().domain([minDate, maxDate]))
.yAxisLabel("request grouped by second");
and I get the following
I need to graph the time it takes each request (time vs date)
It sounds like you want the requests to be summed based on their durations rather than just counted.
If this is what you mean, it's a simple change:
var peticiones = dateDimension.group().reduceSum(function(d) {
return (d.dateEnd - d.dateStart) / 1000;
})
I am dividing by 1000 to get seconds instead of milliseconds; adjust as needed.
However, in your example data above, the duration is 0 (and wouldn't show up at all). So I am not sure if I understand your question. Please comment and/or edit your question if I've missed the mark.

ZoomRange Highstock works not correct?

I made a Highstock diagramm and got aproblem with zooming on the yAxis.
I have a Button and 2 textfield to get the wanted min/max values for the axis. With min:0, max: 100 it works well. With min:0, max:80 it doesn't (max will still be 100 in the Diagramm).
If I use the mouse for zooming it works well (even a min of: 3.7 and a max of 3.894 is possible). But using the mouse is not an Option, because in the later Diagramm there will be 3 yAxes with individual zoom.
$(function () {
var seriesOptions = [],
seriesCounter = 0,
names = ['MSFT', 'AAPL', 'GOOG'];
/**
* Create the chart when all data is loaded
* #returns {undefined}
*/
function createChart() {
$('#container').highcharts('StockChart', {
rangeSelector: {
selected: 4
},
chart:{
zoomType: 'xy'
},
yAxis: [
{
labels: {
format: '{value}',
},
height: '100%',
opposite: false,
plotLines: [{
value: 0,
width: 2,
color: 'silver'
}]
},
],
plotOptions: {
series: {
compare: 'percent'
}
},
tooltip: {
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
valueDecimals: 2
},
series: seriesOptions
},
function(chart){
$('#btn').click(function(){
var min = temp_min.value,
max = temp_max.value;
chart.yAxis[0].setExtremes((min),(max));
});
});
}
$.each(names, function (i, name) {
$.getJSON('https://www.highcharts.com/samples/data/jsonp.php?filename=' + name.toLowerCase() + '-c.json&callback=?', function (data) {
if(seriesCounter==0){
seriesOptions[i] = {
name: name,
data: data,
yAxis: 0
};
} else {
seriesOptions[i] = {
name: name,
data: data,
yAxis: 0
};
}
// As we're loading the data asynchronously, we don't know what order it will arrive. So
// we keep a counter and create the chart when all the data is loaded.
seriesCounter += 1;
if (seriesCounter === names.length) {
createChart();
}
});
});
});
JSFiddle
Another Question: Is it possible to set up a scrollbar for the yAxis as well?
Thanks for your help, Patrick
This is related with fact that tickInterval is not regular, so is rounded to value (like 100). The solution is using tickPositioner which calculates ticks, based on extremes which you define.
tickPositioner: function (min,max) {
var positions = [],
tick = Math.floor(min),
increment = Math.ceil((max - min) / 5);
for (tick; tick - increment <= max; tick += increment) {
positions.push(tick);
}
return positions;
},
http://jsfiddle.net/6s11kcwd/
The scrollbar is supported only for xAxis.

In dc.js, why is elasticX not working properly for Time Series Chart

Here is the jfiddle - http://jsfiddle.net/inasisi/6v639g9g/1/
As you can see the X axis is not scaled properly. I can calculate the min and max date and set the scale properly but don't want to do it after each filter. Would prefer if elasticX works properly.
Any ideas?
var chartGroup = "chartGroup";
data = [{
"run_date": "2013-01-20",
"current_grade": "Kindergarten",
"students": 1
}, {
"run_date": "2013-01-20",
"current_grade": "First",
"students": 2
}, {
"run_date": "2014-03-22",
"current_grade": "Kindergarten",
"students": 3
}, {
"run_date": "2014-03-22",
"current_grade": "First",
"students": 4
}, {
"run_date": "2015-10-06",
"current_grade": "Kindergarten",
"students": 5
}, {
"run_date": "2015-10-06",
"current_grade": "First",
"students": 21
}, {
"run_date": "2015-02-13",
"current_grade": "Kindergarten",
"students": 31
}, {
"run_date": "2015-02-13",
"current_grade": "First",
"students": 26
}, ];
var ndx = crossfilter(data);
var dateFormat = d3.time.format("%Y-%m-%d");
data.forEach(function (d) {
d.run_date = Date.parse(d.run_date);
});
var ndx = crossfilter(data);
filterDateDimension = ndx.dimension(function (d) {
return [d.run_date];
});
dateDimension = ndx.dimension(function (d) {
return [d.run_date];
});
var minDate = dateDimension.bottom(1)[0].run_date;
var maxDate = dateDimension.top(1)[0].run_date;
var runsStudentsGroup = dateDimension.group().reduceSum(function (fact) {
return fact.students;
});
var totalStudentsChart = dc.lineChart("#students_chart", chartGroup);
totalStudentsChart.renderArea(true)
.width(300)
.height(300)
.x(d3.time.scale())
.elasticY(true)
.elasticX(true)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.dimension(dateDimension)
//.colors('red')
.group(runsStudentsGroup);
dc.renderAll(chartGroup);
$('.day_filter').on('click', function () {
console.log(dateDimension.top(Infinity));
console.log($(this).val());
dateDimension.filter(function (d) {
console.log(d > new Date(2015, 0, 1));
return d > new Date(2015, 0, 1);
});
console.log(dateDimension.top(Infinity));
dc.redrawAll();
});
I had to fix a few things to get the chart to display and to get the filter to work at all. I'll just quote those without explaining, since those aren't what the question is about:
d.run_date = new Date(d.run_date);
//...
return d.run_date; // twice
//...
filterDateDimension.filter(function (d) {
//...
dc.redrawAll(chartGroup);
To answer your main question, which is frequently asked, crossfilter does not automatically remove empty bins. You can use a "fake group" to filter them out.
Adding:
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.value != 0;
});
}
};
}
//...
.group(remove_empty_bins(runsStudentsGroup));
Working fork of your fiddle here: http://jsfiddle.net/gordonwoodhull/8an2n1eL/5/
(The transition in this example is particularly screwy, and will be fixed in 2.1.)

Resources