Reputation: 802
This dc.js sample shows a line chart (source). I wonder how one can replace its one focusChart with a set of horizon charts using d3-horizon-chart (example source)?
Here (JSFiddle code sample) is where I get stuck -it shows no data on dc charts... Please help=(
class dcHorizonChart {
constructor(parent, groupByKeyName, valueKeyName, group ) {
this.data = null;
this.height = 30
this._groupByKeyIdx = groupByKeyName;
this._valueKeyName = valueKeyName;
this._root = d3.select(parent);
dc.registerChart(this, group);
}
groupAll(groupAll) {
console.log("gac")
if(!arguments.length)
return this._groupAll;
this._groupAll = groupAll;
return this;
}
setData(data) {
this.data = data
}
render() {
console.log("called once");
this.redraw();
}
redraw() {
// console.log(this.data.all())
//console.log(this.data.all())
var ndata = Enumerable.From(this.data.all()).Select("r=> { 'name': r.key["+this._groupByKeyIdx+"], 'val': r.key["+this._valueKeyName + "]}");
ndata = ndata.GroupBy("$.name", "$.val" ).Select("{'id': $.Key(), 'values':$.ToArray()}")
.ToArray();
console.log(ndata)
this._root.html(null)
this._root.selectAll('.horizon')
.data(ndata)
.enter()
.append('div')
.attr('class', 'horizon')
.each(function(d) {
d3.horizonChart()
.title(d.id)
.call(this, d.values);
});
console.log("called");
/*
d3.select('body').selectAll('.horizon')
.data(stocks)
.enter()
.append('div')
.attr('class', 'horizon')
.each(function(d) {
d3.horizonChart()
.title(d.stock)
.call(this, d.values);
});
this._rect.transition()
.duration(this._duration)
.attr('fill', this._colors(this._groupAll.value()));
*/
}
}
function loadStockData(stock, callback) {
d3.csv('https://bost.ocks.org/mike/cubism/intro/stocks/' + stock + '.csv').then(function(rows) {
rows = rows.map(function(d) {
return [d3.timeParse(d.Date), +d.Open];
}).filter(function(d) {
return d[1];
}).reverse();
var date = rows[0][0],
compare = rows[400][1],
value = rows[0][1],
values = [],
indices = [];
rows.forEach(function(d, i) {
values.push(value = (d[1] - compare) / compare);
indices.push(i);
});
callback({
'stock': stock,
'values': values,
'indices': indices
});
});
}
var promises = [];
['AAPL', 'GOOG', 'MSFT'].forEach(function(stock) {
promises.push(new Promise(function(resolve, reject) {
var r = loadStockData(stock, resolve);
}));
});
Promise.all(promises).then(function(stocks) {
console.log(stocks);
var data = [];
data = Enumerable.From(stocks)
.SelectMany( "val, index=>" +
"Enumerable.From(val.values)" +
".Select(\"v,i => {'value': v, 'idx':i, 'name':'\" + val.stock + \"' } \")")
.ToArray();
/*
for(var i = 0; i < stocks.length; i++) {
for(var j= 0; j < stocks[i].indices.length; j++) {
data.push({ 'idx':stocks[i].indices[j], 'name': stocks[i].stock, 'value': stocks[i].values[j] })
}
}
*/
console.log(data);
var ndx, runDimension, runGroup, overviewRunDimension, overviewRunGroup;
ndx = crossfilter(data);
var allDim = ndx.dimension(function(d){ return [d.idx, d.name, d.value] ;});
runDimension = ndx.dimension(function(d) {return [d.name, d.idx]; });
overviewRunDimension = ndx.dimension(function(d) {return [d.name, d.idx]; });
runGroup = runDimension.group().reduceSum(function(d) { return d.value; });
overviewRunGroup = overviewRunDimension.group().reduceSum(function(d) { return d.value; });
var horizonChart = new dcHorizonChart("#test-hc", 1,2);
horizonChart.setData(allDim.group());
var overviewChart = dc.seriesChart("#test-overview");
overviewChart
.width(768)
.height(100)
.chart(function(c) { return dc.lineChart(c).curve(d3.curveCardinal); })
.x(d3.scaleLinear().domain([0,20]))
.brushOn(true)
.xAxisLabel("Run")
.clipPadding(10)
.dimension(runDimension)
.group(runGroup)
.seriesAccessor(function(d) {return "Expt: " + d.key[0];})
.keyAccessor(function(d) {return +d.key[1];})
.valueAccessor(function(d) {return +d.value;});
dc.renderAll();
});
body {
margin: 0;
padding: 0;
}
.horizon {
border-top: solid 1px #000;
border-bottom: solid 1px #000;
overflow: hidden;
position: relative;
}
.horizon + .horizon {
border-top: none;
}
.horizon canvas {
display: block;
image-rendering: pixelated;
}
.horizon .title,
.horizon .value {
bottom: 0;
line-height: 30px;
margin: 0 6px;
position: absolute;
font-family: sans-serif;
text-shadow: 0 1px 0 rgba(255,255,255,.5);
white-space: nowrap;
}
.horizon .title {
left: 0;
}
.horizon .value {
right: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>dc.js - Custom Chart Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="//dc-js.github.io/dc.js/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="//unpkg.com/dc@4/dist/style/dc.css" />
<script src="//d3js.org/d3.v5.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter2/1.4.4/crossfilter.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dc/3.2.1/dc.min.js"></script>
<script src="//npmcdn.com/d3-horizon-chart/build/d3-horizon-chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/linq.js/2.2.0.2/linq.js"></script>
</head>
<body>
<div class="container">
<div id="test-overview"></div>
<div id="bar"></div>
<br/>
AAA
<br>
<div id="test-hc"></div>
</div>
</body>
</html>
Upvotes: 1
Views: 395
Reputation: 20120
I created a horizon chart example for dc.js.
The main problem with integrating d3-horizon-chart with dc.js is that it doesn't have an X scale - it just displays each value as a 1-pixel column of color, in the array order.
This means that it doesn't use the group keys, and the data must be sorted in the correct order before drawing the horizon chart.
Here is the definition of the chart:
class HorizonChart {
constructor(parent, group) {
this._group = null;
this._colors = null;
this._seriesAccessor = null;
this._root = d3.select(parent);
dc.registerChart(this, group);
}
// initialization functions for user
group(group) {
if(!arguments.length)
return this._group;
this._group = group;
return this;
}
// takes array of colors (not scale)
colors(colors) {
if(!arguments.length)
return this._colors;
this._colors = colors;
return this;
}
seriesAccessor(seriesAccessor) {
if(!arguments.length)
return this._seriesAccessor;
this._seriesAccessor = seriesAccessor;
return this;
}
valueAccessor(valueAccessor) {
if(!arguments.length)
return this._valueAccessor;
this._valueAccessor = valueAccessor;
return this;
}
// interface for dc.js chart registry
render() {
this.redraw();
}
redraw() {
const nester = d3.nest().key(this._seriesAccessor),
nesting = nester.entries(this._group.all());
let horizon = this._root.selectAll('.horizon')
.data(nesting);
horizon = horizon.enter()
.append('div')
.attr('class', 'horizon')
.merge(horizon);
const colors = this._colors,
valueAccessor = this._valueAccessor;
horizon
.each(function(series) {
d3.select(this).selectAll('*').remove();
d3.horizonChart()
.colors(typeof colors === 'function' ? colors(series.key) : colors)
.title(series.key)
.call(this, series.values.map(valueAccessor));
});
}
It implements a small part of the interface of the dc.js series chart. One difference is that d3-horizon-chart takes an array of colors and decides how many positive and negative bands to draw based on the length of that array. So HorizonChart.colors()
accepts either an array of colors, or a function that takes the series key and returns an array of colors.
It uses d3.nest() to split the data by the series accessor. Then it adds or removes divs for the horizon charts, removes anything inside each div and draws a horizon chart there.
Since the data needs to be sorted, and what we get from group.all()
for a multikey will be sorted wrong, we can use a fake group to order by the two elements of the key:
function sort_multikey_group(group) {
return {
all: () => {
return group.all().slice().sort(
({key: keyA}, {key: keyB}) => d3.ascending(keyA[0],keyB[0]) || d3.ascending(keyA[1],keyB[1]));
}
};
}
We apply the fake group when initializing the horizon chart:
var horizonChart = new HorizonChart("#horizon"),
horizonChart
.group(sort_multikey_group(exptRunGroup))
.colors(n => [d3.schemeBlues, d3.schemeOranges, d3.schemeGreens, d3.schemeReds, d3.schemePurples][n-1][6]) // levels * 2
.seriesAccessor(d => d.key[0])
.valueAccessor(d => d.value - 500)
The function parameter to .colors()
chooses one of the D3 Color Schemes for each of the horizon charts. It chooses the scheme with 6 colors to show 3 positive and 3 negative bands.
It puts data into each of the series charts based on the first element of the key. In this example, data is shifted lower to use the negative color bands.
The data in the example is the Michelson-Morley Experiment data used in the dc.js Series Chart Example, interpolated 20 points for every one:
const experiments2 = d3.range(experiments.length-1).flatMap(i => {
if(experiments[i].Expt !== experiments[i+1].Expt)
return [];
let {Expt, Run, Speed: Speed0} = experiments[i],
{Speed: Speed1} = experiments[i+1];
Expt = +Expt; Run = +Run; Speed0 = +Speed0; Speed1 = +Speed1;
const terp = d3.scaleLinear().range([Speed0, Speed1]);
return d3.range(mult).map(j => ({Expt, Run: Run + j/mult, Speed: terp(j/mult)}));
});
That's another difference between d3-horizon-chart and the built-in dc.js charts: it needs data for every pixel.
Upvotes: 1