For the numberDisplay I only see examples in which all values are summed. But I would like to apply a filter so that I only see the values for a certain type.
In the example JSFIDDLE there is now a numberDisplay where all values are summed.
Is it also possible to only summarize the values of which for example the type is "cash"?
So in this case I would like to see the number 6 if nothing is selected.
Thank you in advance for your help.
HTML
<div id="demo1">
<h2>Markers with clustering, popups and single selection</h2>
<i>Renewable energy plants in Bulgaria in 2012</i>
<div class="lineChart"></div>
<div class="pie"></div>
<h5>I want here onlny the total number for type "cash" (nothing selected 6)</h5>
<div class="number_cash"></div>
</div>
<pre id="data">date type value
1/1/2020 cash 1
1/1/2020 tab 1
1/1/2020 visa 1
1/2/2020 cash 2
1/2/2020 tab 2
1/2/2020 visa 2
1/3/2020 cash 3
1/3/2020 tab 3
1/3/2020 visa 3
</pre>
JavaScript
function drawMarkerSelect(data) {
var xf = crossfilter(data);
const dateFormatSpecifier = '%m/%d/%Y';
const dateFormat = d3.timeFormat(dateFormatSpecifier);
const dateFormatParser = d3.timeParse(dateFormatSpecifier);
data.forEach(function(d) {
var tempDate = new Date(d.date);
d.date = tempDate;
})
var groupname = "marker-select";
var facilities = xf.dimension(function(d) {
return d.date;
});
var facilitiesGroup = facilities.group().reduceSum(d => +d.value);
var typeDimension = xf.dimension(function(d) {
return d.type;
});
var typeGroup = typeDimension.group().reduceSum(d => +d.value);
var dateDimension = xf.dimension(function(d) {
return d.date;
});
var dateGroup = dateDimension.group().reduceSum(d => +d.value);
var minDate = dateDimension.bottom(1)[0].date;
var maxDate = dateDimension.top(1)[0].date;
//numberDisplay cash
var all = xf.groupAll();
var ndGroup = all.reduceSum(function(d) {
return d.value; //6
});
//numbers
var number_cash = dc.numberDisplay("#demo1 .number_cash", groupname)
.group(ndGroup)
.valueAccessor(function(d) {
return d;
});
var pie = dc.pieChart("#demo1 .pie", groupname)
.dimension(typeDimension)
.group(typeGroup)
.width(200)
.height(200)
.renderLabel(true)
.renderTitle(true)
.ordering(function(p) {
return -p.value;
});
var lineChart = dc.lineChart("#demo1 .lineChart", groupname)
.width(450)
.height(200)
.margins({
top: 10,
bottom: 30,
right: 10,
left: 70
})
.dimension(dateDimension)
.group(dateGroup, "total spend")
.yAxisLabel("Transaction spend")
.renderHorizontalGridLines(true)
.renderArea(true)
.legend(dc.legend().x(1200).y(5).itemHeight(12).gap(5))
.x(d3.scaleTime().domain([minDate, maxDate]));
lineChart.yAxis().ticks(5);
lineChart.xAxis().ticks(4);
dc.renderAll(groupname);
}
const data = d3.tsvParse(d3.select('pre#data').text());
drawMarkerSelect(data);
CSS
pre#data {
display: none;
}
.marker-cluster-indiv {
background-color: #9ecae1;
}
.marker-cluster-indiv div {
background-color: #6baed6;
}
Crossfilter is a framework for mapping and reducing in JavaScript. The dimension and group key functions determine the mapping to group bins, and the group reduce functions determine how to add or remove a row of data to/from a bin.
When you use a groupAll, there is just one bin.
With that in mind, you can count cash rows at face value, and count other rows as zero, by writing your reduceSum function as
var ndGroup = all.reduceSum(function(d) {
return d.type === 'cash' ? d.value : 0;
});
Fork of your fiddle.
Related
I have two charts, the first, a line chart, in which the user can brush.
Based on the selection, a bar chart (only one bar) updates its value thanks to a specific function. I would like to apply this specific function in an efficient manner to the new array.
I started by following the complex reduce example. There is something wrong with my logic because the function std gets
the all dataset instead of an array. It seems that to put the function within the valueAccessor is not the right thing to do.
This is my code:
/**********************************
* Step0: javascript functions *
**********************************/
// instead of calculating the desired metric on every change, which is slow, we'll just maintain
// these arrays of rows and calculate the metrics when needed in the accessor
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
function groupArrayRemove(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.left(elements, keyfn(item));
if(keyfn(elements[pos])===keyfn(item))
elements.splice(pos, 1);
return elements;
};
}
function groupArrayInit() {
return [];
}
/**********************************
* Step1: Load data from json file *
**********************************/
d3.json("{% url "block__time_series" portfolio value_date %}").then(function(data){
const dateFormatSpecifier = "%Y-%m-%d";
const dateFormat = d3.timeFormat(dateFormatSpecifier);
const dateFormatParser = d3.timeParse(dateFormatSpecifier);
const numberFormat = d3.format('.2f');
data.forEach(function(d) {
d.dd = dateFormatParser(d.date);
d.month = d3.timeMonth(d.dd); // pre-calculate month for better performance
d.returns = +d.returns;
d.value = +d.value;
});
/******************************************************
* Step2: Create the dc.js chart objects & ling to div *
******************************************************/
const myBarChart = new dc.BarChart('#my_bar_chart');
const myLineChart = dc.compositeChart('#my_line_chart');
const palette_color_block4 = ["#6c5373", "#8badd9", "#b6d6f2", "#45788c", "#6E87F2", "#996A4E",
"#BF7761", "#735360", "#D994B0", "#6C5373", "#7F805E", "#A6A27A", "#48BDCC", "#FFC956", "#f2f2f2"]
/************************************************
* Step3: Run the data through crossfilter *
************************************************/
var facts = crossfilter(data), // Gets our 'facts' into crossfilter
returns = function (d) {return +d.returns}
/*Here my function that I want to use */
function std(kv) {
return d3.deviation(kv.value, returns);
}
/******************************************************
* Step4: Create the Dimensions *
******************************************************/
const dateDimension = facts.dimension(d => d.dd);
var returnsDimension = facts.dimension(returns);
var volGroup = dateDimension.group().reduce(groupArrayAdd(returns), groupArrayRemove(returns),
groupArrayInit);
var valueGroup = dateDimension.group().reduceSum(function (d) {return d.value; });
const moveMonths = facts.dimension(d => d.month);
const monthlyMoveGroup = moveMonths.group().reduceSum(d => d.value);
/***************************************
* Step5: Create the Visualisations *
***************************************/
myBarChart /* dc.BarChart('#my_bar_chart', 'chartGroup')*/
.width(400)
.height(200)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal)
.colorAccessor(d => d.key)
.ordinalColors(palette_color_block4)
.margins({left: 80, top: 30, right: 10, bottom: 20})
.elasticY(false)
.brushOn(false)
.controlsUseVisibility(false)
.valueAccessor(std)
.dimension(returnsDimension)
.group(volGroup);
mylineChart /*dc.compositeChart('#my_line_chart', 'chartGroup')*/
.width(800)
.height(200)
.transitionDuration(1000)
.margins({top: 20, right: 10, bottom: 10, left: 10})
.dimension(moveMonths)
.mouseZoomable(true)
.round(d3.timeMonth.round)
.xUnits(d3.timeMonths)
.renderHorizontalGridLines(true)
.legend(new dc.Legend().x(800).y(10).itemHeight(13).gap(5))
.brushOn(true)
.title( function(d) { return dateFormat(d.key) + ': ' + numberFormat(d.value);
})
.valueAccessor(function (d) { return d.value})
.compose([
dc.lineChart(mylineChart).group(valueGroup , data[0].name)
]);
/****************************
* Step6: Render the Charts *
****************************/
dc.renderAll();
});
I am trying to display a stacked bar chart with dates as xAxis. it display the number of sport session by type of sport.
The idea is to have for a specific time range the number of sessions displayed. For example for the last 4 weeks, the number of sessions per day will be displayed, and for the last 12 weeks, it will display the number of sessions per week.
These values are being calculated and displayed fine. The issue is that they are displayed as a 1px wide bar, instead of a "wide" automatically calculated bar width.
If someone have an idea how this fix this kind of issue... please help!
Data are structured as follows. I only show concerned data
const sessions_summary = [
{
activity_name: 'regular_biking',
date_time: '2020-03-18T15:57:47.853Z',
// ...
},
{
activity_name: 'swimming',
date_time: '2020-03-18T15:57:47.853Z'
},
{
activity_name: 'running',
date_time: '2020-03-19T15:57:47.853Z'
},
// ...
];
Crossfilter:
const ndx = crossfilter(sessions_summary);
const Dimension = ndx.dimension(function(d) {
return d3.timeDay(new Date(d.date_time));
});
Scaletime:
const today = new Date(new Date().toDateString());
const minDate = d3.timeDay(
new Date(
new Date().setDate(
today.getDate() - parseFloat(timeranges[timerange_name]) // 7 or 30 or 90 or 180 or 360 : number of days, depends on the interval selected in Select Entry
)
)
);
let maxDate = d3.timeDay(today);
maxDate = d3.timeDay.offset(maxDate, 1);
const scaletime = d3.scaleTime().domain([minDate, maxDate]);
Chart.x(scaletime);
const interval = intervals[timerange_name]; // d3.timeDay or d3.timeWeek or d3.timeMonth, depending on the choice made in Select Entry
Chart.xUnits(interval);
Group:
const types = [...new Set(sessions_summary.map(session => session.type))];
Group = Dimension.group(function(k) {
return interval(k);
}).reduce(
function(p, v) {
if (v.type in p.types) {
p.types[v.type]++;
} else {
p.types[v.type] = 1;
}
return p;
},
function(p, v) {
p.types[v.type]--;
if (p.types[v.type] === 0) {
delete p.types[v.type];
}
return p;
},
function() {
return {
types: {}
};
}
);
Chart.group(Group, types[0], sel_stack(types[0])).render();
for (let i = 1; i < types.length; i++) {
Chart.stack(Group, types[i], sel_stack(types[i]));
}
Bar Chart:
const Chart = dc.barChart('#sessions_chart');
Chart.width(968)
.height(240)
.elasticY(true)
.margins({
left: 40,
top: 10,
right: 20,
bottom: 40
})
.gap(5)
.centerBar(true)
.round(d3.timeDay.round)
.alwaysUseRounding(true)
.xUnits(d3.timeDays)
.brushOn(false)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(false)
.dimension(Dimension)
.title(d => {
return (
'Date: ' +
new Date(d.key).toDateString() +
'\n' +
'Sessions: ' +
Object.keys(d.value.types)
);
});
Chart.legend(
dc
.legend()
.x(40)
.y(465)
.gap(10)
.horizontal(true)
.autoItemWidth(true)
);
Chart.render();
Complete code can be found on JSFiddle
Thanks in advance
[SOLVED]
The issue was the double xUnits, and the wrong use of d3.TimeDay instead of d3.TimeDays.
What is the right way to filter multi line chart using a time series chart as filter?
I need a time series chart for my focus chart that it is shown the image below. Whenever I brush on time series chart my focus chart needs to be filtered with respect to time series chart.
The time series chart needs to contain only X axis and time as its dimension and it should be interactive with focus chart with respect to time.
var totalNumber = null;
// ------ main chart function -------
function makeCompetitiveGraphs(error, keywords_data) {
errorHandle(error);
cleanedData = getCompositeChartData(keywords_data);
console.log("===", cleanedData);
minDate = moment.min(cleanedData.timeStamp);
maxDate = moment.max(cleanedData.timeStamp);
margins = { top: 27, right: 27, bottom: 36, left: 54 };
// create composite chart.
var composite = dc.compositeChart('#competitiveChart');
// create cross filter
var cf = crossfilter(cleanedData.keywordData);
// create dimensions.
var keywordDateDimension = cf.dimension(function (dp) { return dp.date;
});
var Group = keywordDateDimension.group();
// compose for key words
composeCharts = composeKeywords(dc, composite, keywordDateDimension);
// create chart.
composite
.width(width())
.height(height())
.transitionDuration(1000)
.x(d3.time.scale().domain([minDate, maxDate]))
.ordering(function (d) { return d.value; })
.elasticY(true)
.elasticX(true)
.margins(margins)
.legend(
dc.legend()
.x(1100)
.y(10)
.itemHeight(16)
.gap(8)
.horizontal(false)
)
.renderHorizontalGridLines(true)
.brushOn(false);
// compose the chart array.
composite.compose(composeCharts);
// render the chart
composite.render();
function getCompositeChartData(keywords) {
let momentTimeStamps = [];
let totalKeywordPerDay = [];
let allKeywords = [];
// clean data for d3js chart's
keywords.forEach((kob) => {
kob.sd.forEach((ob) => {
allKeywords.push({
name: kob.kn,
total: ob.value.total__,
date: new Date(moment(ob._id.mention_created_date_, "MMM-DD-YYYY-hh")._d),
});
momentTimeStamps.push(moment(moment(ob._id.mention_created_date_, "MMM-DD-
YYYY-hh")._d));
totalKeywordPerDay.push(ob.value.total__);
});
});
console.log("--------", allKeywords)
// apply date filter.
allKeywords = limiteDataToDateFilter(allKeywords);
return { "keywordData": allKeywords, "timeStamp": momentTimeStamps,
"totalKeywordPerDay": totalKeywordPerDay };
}
function limiteDataToDateFilter(allKeywords) {
cleanedDateWithDates = [];
allKeywords.forEach(element => {
if (moment(element.date).isAfter(moment().date(1).month(6)) &&
moment(element.date).isBefore(moment().date(30).month(8))) {
cleanedDateWithDates.push(element);
}
});
return cleanedDateWithDates;
}
function getReduce(keyword, keywordDateDimension) {
return keywordDateDimension.group().reduceSum(function (dp) {
return dp.name === keyword ? dp.total : 0;
});
}
function composeKeywords(dc, composite, keywordDateDimension) {
composeChartsData = []
keywordsParams.forEach(keyword => {
keyword.chart = dc.lineChart(composite)
.dimension(keywordDateDimension)
.colors(keyword.color)
.group(getReduce(keyword.word, keywordDateDimension), keyword.word)
.interpolate('basis')
composeChartsData.push(keyword.chart);
});
return composeChartsData;
}
I have a Composite Chart with two lines built with the following code:
function fakeGrouper(source_group) {
return {
all:function () {
var cumulate = 100;
var result = [];
return [{key: 0, value: cumulate}]
.concat(source_group.all().map(function(d) {
cumulate -= d.value;
return {key:d.key, value:cumulate};
}));
}
};
}
var recDim = cf1.dimension(dc.pluck('recidiefNa'));
var recGroup = recDim.group().reduceCount();
var RecGroup = fakeGrouper(recGroup);
var resDim = cf1.dimension(dc.pluck('residuNa'));
var resGroup = resDim.group().reduceCount();
var ResGroup = fakeGrouper(resGroup);
var scChart = dc.compositeChart("#scStepChart");
scChart
.width(600)
.height(400)
.x(d3.scale.linear().domain([0,52]))
.y(d3.scale.linear().domain([0,100]))
.clipPadding(10)
.brushOn(false)
.xAxisLabel("tijd in weken")
.yAxisLabel("percentage vrij van residu/recidief")
.legend(dc.legend().x(70).y(250).itemHeight(13).gap(5))
.compose([
dc.lineChart(scChart)
.dimension(recDim)
.group(RecGroup)
.renderDataPoints(true)
.interpolate("step-after")
,
dc.lineChart(scChart)
.dimension(resDim)
.group(ResGroup)
.colors(['#ff9933'])
.renderDataPoints(true)
.interpolate("step-after")
])
.xAxis().ticks(4);
scChart.render();
Which renders perfectly except for the fact that the legend has 0 as label for both chart. Check out my fiddle here: http://jsfiddle.net/8v9faput/
Now i read somewhere that you can solve this by changing the group from:
.group(GROUP)
to
.group(GROUP, LABELNAME)
However once i did this the second chart went black in the legend, and the lines are gone in the chart. as shown in this second fiddle. http://jsfiddle.net/ojdg3ny1/
Any ideas why and how to fix this? I think it might have something to do with me using a fake group.
You're passing the .colors() an array. You should pass it single value like so :
.colors('#ff9933')
So you chart function looks like this :
.legend(dc.legend().x(70).y(250).itemHeight(13).gap(5))
.compose([
dc.lineChart(scChart)
.dimension(recDim)
.group(RecGroup, "Recidief")
.colors('red')
.renderDataPoints(true)
.interpolate("step-after")
,
dc.lineChart(scChart)
.dimension(resDim)
.group(ResGroup, "Residu")
.colors('#ff9933')
.renderDataPoints(true)
.interpolate("step-after")
])
.xAxis().ticks(10)
scChart.render();
Updated fiddle : http://jsfiddle.net/reko91/8v9faput/1/
I'm pushing two datasets into the same page.
They're both coming from separate mongodb tables, but the records are linked by a primary key 'plankey'.
I want to add a filter from this graph to the one in the second dataset.
Main chart function:
function loadProjectData(productName) {
// $('#progress_dialog').show();
buildDataLoaded = false;
$.get('/getbuildresults.json?product=' + productName, function (data) {
stats = data;
if (stats != null && stats.length > 0) {
// Convert dates to real dates
stats.forEach(function (d) {
d.builddate = new Date(d.builddate);
});
// feed it through crossfilter
ndx = crossfilter(stats);
overall = ndx.dimension(function (d) {
return d3.time.month(d.builddate);
});
overallGroup = overall.group().reduce(buildReduceAdd, buildReduceRemove, buildReduceInitial);
//Test Types Graph Data Sorter
testTypesDimension = ndx.dimension(function (d) {
return d3.time.week(d.builddate)
})
}
overallChart = dc.compositeChart("#overall-timeline-chart")
.width(chartWidth) // (optional) define chart width, :default = 200
.height(250) // (optional) define chart height, :default = 200
.transitionDuration(500) // (optional) define chart transition duration, :default = 500
.margins({
top: 10,
right: 50,
bottom: 30,
left: 40
})
.dimension(failedTestDimension)
.group(failedTestDimensionGroup)
.elasticY(true)
.mouseZoomable(false)
.elasticX(false)
.renderHorizontalGridLines(true)
.x(d3.time.scale().domain(timelineDomain))
.round(d3.time.month.round)
.xUnits(d3.time.months)
.title(function (d) {
return "Value: " + d.value;
})
.renderTitle(true)
.brushOn(true);
// Loop through available plans and create chart for each and then compose
var charts = [];
var plans = buildGroup.all();
plans.forEach(function (plan) {
charts.push(dc.lineChart(overallChart).dimension(failedTestDimension).group(failedTestDimensionGroup)
.valueAccessor(function (d) {
return d.value.result[plan.key] ? d.value.result[plan.key].failed : null;
}));
});
overallChart.compose(charts);
Second graph function (this is where I want to add the filter from the above graph):
function loadTestResultsData() {
// $('#progress_dialog').show();
testDataLoaded = false;
$.get('/gettestresults.json', function(data) {
stats = data;
if (stats != null && stats.length > 0) {
// Convert dates to real dates
stats.forEach(function (d) {
d.rundate = new Date(d.rundate);
});
// feed it through crossfilter
support_ndx = crossfilter(stats);
//Support Cases Chart
// Dimension and Group for monthly support cases
supportCasesPerMonth = support_ndx.dimension(function(d){ return d.methodName });
supportCasesPerMonthGroup = supportCasesPerMonth.group();
buildTypesChart = dc.rowChart("#failed-tests-chart")
.width(750) // (optional) define chart width, :default = 200
.height(300) // (optional) define chart height, :default = 200
.group(supportCasesPerMonthGroup) // set group
.dimension(supportCasesPerMonth) // set dimension
// (optional) define margins
.margins({
top: 20,
left: 10,
right: 10,
bottom: 20
})
// (optional) define color array for slices
.colors(['#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#dadaeb'])
// (optional) set gap between rows, default is 5
.gap(7);
}
testDataLoaded = true;
dataLoaded();
});
};
You have two basic approaches. The first to be prefered.
1) Join the data first. I would suggest using something like queue
queue()
.defer(d3.json, '/getbuildresults.json?product=' + productName)
.defer(d3.json, '/gettestresults.json')
.await(ready);
function ready(error, products, stats) {
var productMap = {};
products.forEach(function (d) {
d.builddate = new Date(d.builddate);
productMap[d.plankey] = d;
});
stats.forEach(function (d) {
d.rundate = new Date(d.rundate);
$.extend(d, productMap[d.plankey]);
});
ndx = crossfilter(stats);
// build other dimensions/groups
// build charts
});
2) Your other option is to link the charts by using a trigger to filter on the plankey. So on both data sets, create a crossfilter linked dimension for plankey. Then, on the filter trigger from the second chart, see what plankeys have been set with something like
var keys = C2PlanKeysDim.all()
.filter(function(d){return d.value>0;})
.map(function(d){return d.key;});`
Then on chart 1, filter by the key on C1PlanKeysDim or whatever you call it, and trigger a chart redraw to take into account the filter.