I have data nested into 3 levels, which I need to dynamically update. The kicker is that the elements for the mid-level need to actually display on TOP of the elements for the low-level due to some hover behavior I need, so I'm having trouble with what the enter/update/exit/merge pattern should look like. (There don't need to be any elements displayed for the high-level).
The code I have right now updates the data successfully but is not rendering the rectangles at all, instead giving me an error, Uncaught TypeError: this.setAttribute is not a function.
How do I fix this problem, please?
Here's what it should look like before updating:
And here's what it should look like after updating:
Here's a CodePen with the code Below
```
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
update(data1);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
barsAll.exit().remove();
let barsEnter = barsAll
.enter()
.append('g')
.attr('class', 'bars');
barsEnter = barsEnter.merge(barsAll);
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
let bitsEnter = bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none');
bitsEnter = bitsEnter.merge(bitsAll);
bitsEnter
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter.append('rect')
.attr('stroke', 'black')
.attr('class', 'segments')
.style('fill-opacity', 0.2);
segmentsEnter = segmentsEnter.merge(segmentsAll);
segmentsEnter
.attr('fill', function(d) { return color(d.key) })
.attr('height', y.bandwidth()*0.75)
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
```
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Well, by going around the “merge” step on the mid-level segments and low-level bits (but not on the top-level bars), I was able to hack a fix, finally. Working pen
Still open to help from others because I feel like I should really get the hang of the whole flow - update, enter, exit, merge - at some point.
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
// update
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
// exit
barsAll.exit().remove();
// enter
let barsEnter = barsAll
.enter();
barsEnter = barsEnter.merge(barsAll).append('g');
barsEnter.selectAll('.segments').remove();
d3.selectAll('.segments').remove();
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll
.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none')
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
// bitsEnter = bitsEnter.merge(bitsAll);
bitsAll
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter
.append('rect')
.attr('class', 'segments')
.attr('stroke', 'black')
.style('fill-opacity', 0.2)
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
segmentsAll
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
//segmentsAll = segmentsEnter.merge(segmentsAll);
// segmentsEnter
// .attr('fill', function(d) { return color(d.key) })
// .attr('height', function() { return y.bandwidth()*0.75 })
// .attr('x', function(d) { return x(d.start) - width*0.05 })
// .attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
// .attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Related
I try move a grouped element but after click and drag, the elements jumped away.
demo()
function demo() {
var tooltip = d3.select('body')
.append('div')
.attr('id','tooltip')
.style('position','absolute')
.style('opacity',0)
.style('background','lightsteelblue')
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200)
.style("background", "#ececec")
add_grid(svg);
var data = [
{
text: "O",
x: 50,
y: 50
},
];
var g = svg.append('g')
var fontsize = 20;
var box = g.selectAll(".box")
.data(data)
.join('g')
.attr('class','box')
.attr("pointer-events", "all")
box.call(
d3.drag()
.on("start",function(event,d) {
d3.select(this).raise().classed("active", true);
d3.select('#tooltip')
.transition().duration(100)
.style('opacity', 1)
})
.on("drag",function(event,d) {
d.x = event.x
d.y = event.y
d3.select(this).attr('transform',`translate(${d.x},${d.y})`)
var desc = "(" + d.x.toFixed(1) +"," + d.y.toFixed(1) + ")"
d3.select('#tooltip')
.style('left', (event.x+2) + 'px')
.style('top', (event.y-2) + 'px')
.text(desc)
})
.on("end", function dragEnd(event,d) {
d3.select(this).classed("active", false);
d3.select('#tooltip').style('opacity', 0)}
))
.on('mouseover', function(event,d) {
})
.on('mouseout', function(event,d) {
})
.on('mousemove', function(event,d) {
})
.on("mousedown", function(){
})
.on("mouseup", function(){
});
var txt = box.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline",'text-before-edge')//'central')//text-bottom
.attr("font-size", fontsize)
.attr("x", (d) => d.x)
.attr("y", (d) => d.y)
var tspan = txt.selectAll(".tspan")
.data((d) => d.text.split("\n"))
.join("tspan")
.attr("class", "tspan")
.attr("x", function (d) {
let x = +d3.select(this.parentNode).attr("x");
return x;
})
.attr("y", function (d,i) {
let y = +d3.select(this.parentNode).attr("y");
return y + i*fontsize * .9;
})
.text((d) => d);
box.each((d,i,n) => {
var bbox = d3.select(n[i]).node().getBBox()
var padding = 2
bbox.x -= padding
bbox.y -= padding
bbox.width += 2*padding
bbox.height += 2*padding
d.bbox = bbox
})
.attr('transform',d => `translate(${0},${-d.bbox.height/2})`)
.append('rect')
.attr('x',d => d.bbox.x)
.attr('y',d => d.bbox.y)
.attr('width', d => d.bbox.width)
.attr('height',d => d.bbox.height)
.attr('stroke','red')
.attr('fill','none')
add_dots(svg,data)
function add_dots(svg,data) {
svg.selectAll('.dots')
.data(data)
.join('circle')
.attr('class','dots')
.attr('r',2)
.attr('cx',d => d.x)
.attr('cy',d => d.y)
.attr('fill','red')
}
function add_grid(svg) {
var w = +svg.attr("width");
var step = 10;
var mygrid = function (d) {
return `M 0,${d} l ${w},0 M ${d},0 l 0,${w}`;
};
var grid = [];
for (var i = 0; i < w; i += step) {
grid.push(i);
}
svg
.append("g")
.selectAll(null)
.data(grid)
.enter()
.append("path")
.attr("d", (d) => mygrid(d))
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 0.5);
}
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>
I am trying to create a background texture rectangle block behind the charts blue rectangles. I need to create a pattern block and apply it to the fill for the grey rectangles
https://jsfiddle.net/aLh9d51t/
I currently have a solid block
but need to add a texture like this
https://iros.github.io/patternfills/sample_d3.html
var def = svg.append('defs').attr("height", 10).attr("width", 10).append('pattern')
I have to create this def
<svg height="10" width="10" xmlns="http://www.w3.org/2000/svg" version="1.1"> <defs> <pattern id="diagonal-stripe-1" patternUnits="userSpaceOnUse" width="10" height="10"> <image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+CiAgPHJlY3Qgd2lkdGg9JzEwJyBoZWlnaHQ9JzEwJyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsMTAgbDEwLC0xMAogICAgICAgICAgIE05LDExIGwyLC0yJyBzdHJva2U9J2JsYWNrJyBzdHJva2Utd2lkdGg9JzEnLz4KPC9zdmc+Cg==" x="0" y="0" width="10" height="10"> </image> </pattern> </defs> </svg>
var itemRects2 = main.append('g').attr('clip-path', 'url(#clip)');
var itemRects = main.append('g').attr('clip-path', 'url(#clip)');
var currentLine = main
.append('line')
.attr('class', 'currentLine')
.attr('clip-path', 'url(#clip)');
var brush = d3
.brushX()
.extent([
[0, 0],
[w, miniHeight],
])
.on('brush', brushed);
mini.append('g')
.attr('class', 'x brush')
.call(brush)
.call(brush.move, x1.range());
function getXAxisTop(tTick, tFormat) {
return d3
.axisBottom(xTop)
.ticks(d3[tTick])
.tickFormat(d => d3.timeFormat(tFormat)(d));
}
function toDays(d) {
d = d || 0;
return d / 24 / 60 / 60 / 1000;
}
function toUTC(d) {
if (!d || !d.getFullYear) return 0;
return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
}
function daysBetween(d1, d2) {
return toDays(toUTC(d2) - toUTC(d1));
}
function drawBrush(minExtent, maxExtent) {
var visItems = items.filter(function(d) {
return d.starting_time < maxExtent && d.ending_time > minExtent;
});
var days = daysBetween(minExtent, maxExtent);
console.log('days', days);
/*
function timeFormat(formats) {
return function(date) {
var i = formats.length - 1, f = formats[i];
while (!f[1](date)) f = formats[--i];
return d3.functor(f[0])(date);
};
}*/
/*
var customTimeFormat = timeFormat([
["00:00", function () { return true; }],
["06:00", function (d) { return 3 <= d.getHours() && d.getHours() < 9; }],
["12:00", function (d) { return 9 <= d.getHours() && d.getHours() < 15; }],
["18:00", function (d) { return 15 <= d.getHours() && d.getHours() < 21; }]
]);
*/
var tFormat1 = '%Y-%m';
var tTick1 = 'timeMonth';
//var tFormat2 = '%H%M';
//var tTick2 = 'timeHour';
if (days < 40) {
tFormat1 = '%Y-%m-%d';
tTick1 = 'timeWeek';
}
if (days <= 7) {
tFormat1 = '%Y-%m-%d';
tTick1 = 'timeDay';
}
if (days <= 1) {
tFormat1 = '%H%M';
tTick1 = 'timeHour';
//tFormat2 = '%H%M';
//tTick2 = customTimeFormat;
//console.log("CUSTOM FORMAT", customTimeFormat)
}
var toolTipDateFormat = d3.timeFormat('%Y-%m');
x1.domain([minExtent, maxExtent]);
xTop.domain([minExtent, maxExtent]);
gXTop.call(getXAxisTop(tTick1, tFormat1));
gXTop.selectAll('.tick text');
//gXTop2.call(getXAxisTop(tTick2, tFormat2));
//gXTop2.selectAll('.tick text');
currentLine
.attr('x1', x1(now))
.attr('x2', x1(now))
.attr('y1', 0)
.attr('y2', mainHeight);
//update main item rects
var rects = itemRects
.selectAll('rect')
.data(visItems, function(d) {
return d.id;
})
.attr('x', function(d) {
return x1(d.starting_time);
})
.attr('width', function(d) {
return x1(d.ending_time) - x1(d.starting_time);
});
rects
.enter()
.append('rect')
.attr('class', function(d) {
return 'miniItem';
})
.attr('x', function(d) {
return x1(d.starting_time);
})
.attr('y', function(d) {
return y1(d.lane + gutter);
})
.attr('width', function(d) {
return x1(d.ending_time) - x1(d.starting_time);
})
.attr('height', function(d) {
return y1(1 - 2 * gutter);
})
.on('mouseover', function(d) {
tooltipTimeline
.transition()
.duration(200)
.style('opacity', 0.9);
tooltipTimeline
.html(
'Start Time ' +
toolTipDateFormat(d.starting_time) +
'<br>' +
'End Time ' +
toolTipDateFormat(d.ending_time),
)
.style('left', d3.event.pageX + 'px')
.style('top', d3.event.pageY - 28 + 'px');
})
.on('mouseout', function(d) {
tooltipTimeline
.transition()
.duration(500)
.style('opacity', 0);
});
rects.exit().remove();
//textured blocks
console.log("w", w);
//update main item rects
var backRects = itemRects2
.selectAll('rect')
.data(visItems, function(d) {
return d.id;
})
.attr('x', function(d) {
return 0;
})
.attr('width', function(d) {
return w;
})
.attr('fill', function(d) {
return "grey";
});
backRects
.enter()
.append('rect')
.attr('class', function(d) {
return 'miniItem2';
})
.attr('x', function(d) {
return 0;
})
.attr('y', function(d) {
return y1(d.lane + gutter);
})
.attr('width', function(d) {
return w;
})
.attr('height', function(d) {
return y1(1 - 2 * gutter);
})
.attr('fill', function(d) {
return "grey";
});
// backRects.exit().remove();
//textured block
}
something like this.
chart.append("defs").append("pattern")
.attr('id','diagonal-stripe-1')
.attr("width", 10)
.attr("height", 10)
.attr('patternUnits',"userSpaceOnUse")
.append('image')
.attr('xlink:href','data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+CiAgPHJlY3Qgd2lkdGg9JzEwJyBoZWlnaHQ9JzEwJyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsMTAgbDEwLC0xMAogICAgICAgICAgIE05LDExIGwyLC0yJyBzdHJva2U9J2JsYWNrJyBzdHJva2Utd2lkdGg9JzEnLz4KPC9zdmc+Cg==')
.attr('x',0)
.attr('y',0)
.attr("width", 10)
.attr("height", 10);
but how do I make it closer to the design -- custom image?
//update main item rects
var backRects = itemRects2
.selectAll('rect')
.data(visItems, function(d) {
return d.id;
})
.attr('x', function(d) {
return 0;
})
.attr('width', function(d) {
return w;
})
.attr('fill', function(d) {
//return "grey";//'diagonal-stripe-1'
return "url(#diagonal-stripe-1)";
});
I am new to d3.js and please forgive me if this sounds like a naive question. I have plotted a line (d3 v4) which can be draggable by its end points. The end points are rectangle.
The current output looks as below :
This is how it looks
The challenge that i am facing is - when i start dragging the point, the line seems to take its origin from the top left corner. When i drag the second point of the same line, the line drags / moves as expected.
The sample data looks as below :
The sample data
Requesting your suggestions / inputs on how to fix the above issue.
Below is the attached code that i am using :
var margin = { top: 0, right: 0, bottom: 0, left: 0 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// Creating the colour Category
var color = d3.scaleOrdinal(d3.schemeCategory10);
var y = d3.scaleLinear().range([390, 0]);
// Scale the range of the data
y.domain([0, d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice)*1.2; })]).nice();
// Line for the 1st Block
var lines = svg.selectAll("line")
.data(data)
.enter()
.append('line')// attach a line
.style("stroke", "#E6EAEE")
.style("stroke-width", 8) // colour the line
.attr("x1", function (d) { return d.x_value; }) // x position of the first end of the line
.attr("y1", function (d) { return y(d.nonpromotedprice); }) // y position of the first end of the line
.attr("x2", function (d) { return d.x_value; }) // x position of the second end of the line
.attr("y2", function (d) { return y(d.promotedprice); });
// Add the Y Axis
svg.append("g")
.attr("class", "grid")
.attr("fill", "lightgrey")
.attr("stroke-width", 0.7)
.attr("stroke-opacity", 0.2)
.call(d3.axisLeft(y)
.tickSize(-400)
.tickFormat(""));
var topEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.nonpromotedprice,
'marker': 'marker-start',
'lineIndex': i
};
});
var bottomEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.promotedprice,
'marker': 'marker-end',
'lineIndex': i
};
});
var MiddleEndPoints = data.map(function (line, i) {
return {
'x': line.x_value,
'y': line.avgprice,
'marker': 'marker-middle',
'lineIndex': i
};
});
var endPointsData = topEndPoints.concat(bottomEndPoints, MiddleEndPoints);
// Pointer to d3 rectangles
var endPoints = svg
.selectAll('rect')
.data(endPointsData)
.enter()
.append('rect')
.attr("width", 12)
.attr("height", 8)
.attr("x", function (d) { return d.x - 6; })
.attr("y", function (d) { return y(d.y); })
//.attr("cx", function (d) { return d.x; })
//.attr("cy", function (d) { return d.y; })
//.attr('r',7)
.attr("fill", function (d) { return color(d.x); })
.call(d3.drag()
//.origin(function(d) { return y(d.y); })
.subject(function() {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// draw the logo
svg.selectAll("image")
.data(data)
.enter()
.append("svg:image")
.attr("xlink:href", function (d) { return d.logo; })
//.append("rect")
.attr("x", function (d) { return d.x_value - 13; })
.attr("y", function (d) { return y(d.nonpromotedprice + 35); })
.attr("height", 25)
.attr("width", 25);
function dragstarted() {
d3.select(this).classed("active", true).attr('y', d.y = y(d3.event.y));
}
function dragged(d, i) {
var marker = d3.select(this);
// Update the marker properties
marker
//.attr('cx', d.x = d3.event.x)
.attr('y', d.y = d3.event.y);
// Update the line properties
lines
.filter(function (lineData, lineIndex) {
return lineIndex === d.lineIndex;
})
.attr('x1', function (lineData) {
return d.marker === 'marker-start' ? lineData.x1 = d.x : lineData.x1;
})
.attr('y1', function (lineData) {
return d.marker === 'marker-start' ? lineData.y1 = d.y : lineData.y1;
})
.attr('x2', function (lineData) {
return d.marker === 'marker-end' ? lineData.x2 = d.x : lineData.x2;
})
.attr('y2', function (lineData) {
return d.marker === 'marker-end' ? lineData.y2 = d.y : lineData.y2;
});
}
function dragended() {
d3.select(this).classed("active", false);
Shiny.setInputValue("pricechanged",
{price: (d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice); }) -(d3.event.y / 390)* d3.max(data, function (d) { return Math.max(d.nonpromotedprice, d.promotedprice); }))*1.19},
{priority: "event"}
);
}
I have a vertical bar chart in my Ember application and I am struggling to attach text labels to the top of the bars.
The chart is broken up into the following functions:
Drawing the static elements of the chart:
didInsertElement() {
let svg = select(this.$('svg')[0]);
this.set('svg', svg);
let height = 325
let width = 800
let padding = {
top: 10,
bottom: 30,
left: 40,
right: 0
};
this.set('barsContainer', svg.append('g')
.attr('class', 'bars')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
let barsHeight = height - padding.top - padding.bottom;
this.set('barsHeight', barsHeight);
let barsWidth = width - padding.left - padding.right;
// Y scale & axes
let yScale = scaleLinear().range([barsHeight, 0]);
this.set('yScale', yScale);
this.set('yAxis', axisLeft(yScale));
this.set('yAxisContainer', svg.append('g')
.attr('class', 'axis axis--y axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
// X scale & axes
let xScale = scaleBand().range([0, barsWidth]).paddingInner(0.15);
this.set('xScale', xScale);
this.set('xAxis', axisBottom(xScale));
this.set('xAxisContainer', svg.append('g')
.attr('class', 'axis axis--x axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top + barsHeight})`)
);
// Color scale
this.set('colorScale', scaleLinear().range(COLORS[this.get('color')]));
this.renderChart();
this.set('didRenderChart', true);
},
This re-draws the chart when the model changes:
didUpdateAttrs() {
this.renderChart();
},
This handles the drawing of the chart:
renderChart() {
let data = this.get('data');
let counts = data.map(data => data.count);
// Update the scales
this.get('yScale').domain([0, Math.max(...counts)]);
this.get('colorScale').domain([0, Math.max(...counts)]);
this.get('xScale').domain(data.map(data => data.label));
// Update the axes
this.get('xAxis').scale(this.get('xScale'));
this.get('xAxisContainer').call(this.get('xAxis')).selectAll('text').attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(40)")
.style("text-anchor", "start");
this.get('yAxis').scale(this.get('yScale'));
this.get('yAxisContainer').call(this.get('yAxis'));
let barsUpdate = this.get('barsContainer').selectAll('rect').data(data, data => data.label);
// Enter
let barsEnter = barsUpdate.enter()
.append('rect')
.attr('opacity', 0);
let barsExit = barsUpdate.exit();
let div = select('body')
.append("div")
.attr("class", "vert-tooltip");
// Update
let rafId;
barsEnter
.merge(barsUpdate)
.transition()
.attr('width', `${this.get('xScale').bandwidth()}px`)
.attr('height', data => `${this.get('barsHeight') - this.get('yScale')(data.count)}px`)
.attr('x', data => `${this.get('xScale')(data.label)}px`)
.attr('y', data => `${this.get('yScale')(data.count)}px`)
.attr('fill', data => this.get('colorScale')(data.count))
.attr('opacity', data => {
let selected = this.get('selectedLabel');
return (selected && data.label !== selected) ? '0.5' : '1.0';
})
.on('start', (data, index) => {
if (index === 0) {
(function updateTether() {
Tether.position()
rafId = requestAnimationFrame(updateTether);
})();
}
})
.on('end interrupt', (data, index) => {
if (index === 0) {
cancelAnimationFrame(rafId);
}
});
// Exit
barsExit
.transition()
.attr('opacity', 0)
.remove();
}
I have stripped some tooltip and click events to maintain clarity.
To add the labels I have tried to add the following in the renderChart() function:
barsEnter.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function (d) { return d.count; })
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
.style("fill", "white");
with the above code I receive an error to say that xScale and yScale are not found because they are not within this functions scope. If I use:
.attr("x", function (d) { return this.get('xScale')(d.label) + this.get('xScale').bandwidth() / 2; })
.attr("y", function (d) { return this.get('yScale')(d.count) + 12; })
I generate 'this.get' is not a function errors and the context of 'this' becomes the an object with the value of (d).
If I add the X and Y scales as variables to this function like:
let xScale = this.get('xScale')
let yScale = this.get('ySCale')
...
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
Then the x and y attrs are returned as undefined. Please let me know if I have missed anything out.
Converting the function() {} syntax into arrow functions will allow you to maintain the this.
So:
function(d) { return this.get('xScale'); }
becomes
(d) => this.get('xScale')
or
(d) => {
return this.get('xScale');
}
For more information on arrow functions:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- https://hackernoon.com/javascript-es6-arrow-functions-and-lexical-this-f2a3e2a5e8c4
Continuing to try and master the enter-update-exit pattern...
I've got a relatively simple reusable d3.js chart, and I want to be able to update the chart between two data sets. I'm getting close, but the chart is not updating properly.
You can see a fiddle here: http://jsfiddle.net/rolfsf/vba6n4sh/2/
Where did I mess up the enter-update-exit pattern?
The chart code looks like this:
function relativeSizeChart() {
var width = 1200,
margin = 0,
padding = 16,
r = d3.scale.linear(),
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null,
val = function(d){return d;};
totalFormat = function(d){return d;};
clusterFormat = function(d){return d;};
clusterFormat2 = function(d){return d;};
function chart(selection) {
selection.each(function(data) {
//console.log(data);
var clusterCount = data.Clusters.length,
totalColWidth = 0.3*width,
colWidth = (width - totalColWidth)/clusterCount,
height = colWidth + 2*padding,
maxRadius = (colWidth - 10)/2;
var svg = d3.select(this).selectAll("svg")
.data([data]);
var svgEnter = svg
.enter().append("svg")
.attr('class', function(d){
if( onTotalMouseOver !== null || onTotalClick !== null ||onClusterMouseOver !== null || onClusterClick !== null){
return 'clickable';
}else{
return 'static';
}
})
.attr("width", width)
.attr("height", height);
var background, clusterLines;
background = svgEnter.append("g")
.attr('class', 'background');
var headers = svgEnter.append("g")
.attr('class', 'headers')
.selectAll("text.header")
.data(data.Headers, function(d){return d;});
var total = svgEnter.append("g")
.attr('class', 'total');
var cluster = svgEnter.selectAll('g.cluster')
.data(data.Clusters,function(d){ return d;});
var clusterEnter = cluster
.enter().append("g")
.attr('class', 'cluster')
.attr('transform', function (d, i) {
return 'translate(' + (totalColWidth + i*colWidth) + ',0)';
});
var clusters = svg.selectAll('g.cluster');
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([40, maxRadius]);
background .append("rect")
.attr("class", "chart-bg")
.attr('x', 0)
.attr('y', padding)
.attr('height', (height-padding))
.attr('width', width)
.attr('class', 'chart-bg');
background .append("g")
.attr('class', 'cluster-lines');
background .append("line")
.attr("class", "centerline")
.attr('x1', (totalColWidth - padding))
.attr('x2', width - (colWidth/2))
.attr('y1', (height+padding)/2)
.attr('y2', (height+padding)/2);
clusterLines = background.select('g.cluster-lines')
.selectAll("line")
.data(data.Clusters,function(d){ return d;})
.enter().append('line')
.attr('class', 'cluster-line');
headers .enter().append('text')
.attr('class', 'header');
total .append("rect")
.attr("class", "total-cluster")
.attr('x', padding)
.attr('y', 0.2*(height+(4*padding)))
.attr('height', 0.5*(height))
.attr('width', totalColWidth-(2*padding))
.attr('rx', 4)
.attr('ry', 4)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
total .append("text")
.attr("class", "total-name")
.attr('x', totalColWidth/2 )
.attr('y', function(d, i) { return ((height+padding)/2) + (padding + 10); });
total .append("text")
.attr("class", "total-value")
.attr('x', totalColWidth/2 )
.attr('y', function(d, i) { return ((height+padding)/2); })
.text(totalFormat(0));
clusterEnter.append('circle')
.attr('class', 'bubble')
.attr('cx', function(d, i) { return colWidth/2; })
.attr('cy', function(d, i) { return (height+padding)/2;})
.attr("r", "50")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){
onClusterClick(this, d, i);
});
clusterEnter.append('text')
.attr('class', 'cluster-value')
.attr('x', function(d, i) { return colWidth/2; })
.attr('y', function(d, i) { return ((height+padding)/2); })
.text(clusterFormat(0));
clusterEnter.append('text')
.attr('class', 'cluster-value-2')
.attr('x', function(d, i) { return colWidth/2; })
.attr('y', function(d, i) { return ((height+padding)/2) + (padding + 10); })
.text(clusterFormat2(0));
//update attributes
clusterLines.attr('x1', function(d, i) { return totalColWidth + i*colWidth })
.attr('x2', function(d, i) { return totalColWidth + i*colWidth })
.attr('y1', function(d, i) { return padding })
.attr('y2', function(d, i) { return (height); });
headers .attr('x', function(d, i) {
if(i === 0){
return (totalColWidth/2);
}else{
return (totalColWidth + (i*colWidth) - (colWidth/2))
}
})
.attr('y', 12);
//clean up old
svg .exit().remove();
cluster .exit().selectAll('circle.bubble')
.style("opacity", 1)
.style("fill", "#DDD")
.style("stroke", "#DDD")
.transition()
.duration(500)
.style("opacity", 0);
cluster .exit().remove();
headers .exit().remove();
function update(data) {
//update with data
svg .selectAll('text.total-value')
.transition()
.delay(400)
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d.Total[1] );
return function( t ) {
this.textContent = totalFormat(interpolator(t));
};
});
svg .selectAll('text.total-name')
.text(val(data.Total[0]));
svg .selectAll('circle')
.attr('class', function(d, i) {
if(d[1] === 0){ return 'bubble empty';}
else {return 'bubble';}
})
.transition()
.duration(1000)
.delay(function(d, i) { return 500 + (i * 100); })
.ease('elastic')
.attr("r", function (d, i) { return r(d[1]); });
svg .selectAll('text.cluster-value')
.transition()
.delay(function(d, i) { return 500 + (i * 100); })
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d[1] );
return function( t ) {
this.textContent = clusterFormat(interpolator(t));
};
});
svg .selectAll('text.cluster-value-2')
.transition()
.delay(function(d, i) { return 500 + (i * 100); })
.duration(1000)
.tween( 'text', function(d, i) {
var currentValue = +this.textContent.replace(/\D/g,'');
var interpolator = d3.interpolateRound( currentValue, d[0] );
return function( t ) {
this.textContent = clusterFormat2(interpolator(t));
};
});
headers .text(function(d, i){return d});
}
update(data);
});
}
chart.totalFormat = function(_) {
if (!arguments.length) return totalFormat;
totalFormat = _;
return chart;
};
chart.clusterFormat = function(_) {
if (!arguments.length) return clusterFormat;
clusterFormat = _;
return chart;
};
chart.clusterFormat2 = function(_) {
if (!arguments.length) return clusterFormat2;
clusterFormat2 = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onTotalClick = function(_) {
if (!arguments.length) return onTotalClick;
onTotalClick = _;
return chart;
};
chart.onTotalMouseOver = function(_) {
if (!arguments.length) return onTotalMouseOver;
onTotalMouseOver = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
chart.onClusterMouseOver = function(_) {
if (!arguments.length) return onClusterMouseOver;
onClusterMouseOver = _;
return chart;
};
return chart;
}
my sample data looks like this
var data = {
"data1": {
Headers: ["Total", "Col 1A", "Col 2A", "Col 3A", "Col 4A"],
Total: ["Total # of Widgets", 1200],
Clusters: [
[100, 1200],
[67, 800],
[42, 500],
[17, 198]
]
},
"data2": {
Headers: ["Total", "Col 1B", "Col 2B", "Col 3B", "Col 4B"],
Total: ["Total # of Widgets", 1200],
Clusters: [
[20, 245],
[31, 371],
[32, 386],
[12, 145]
]
}
}
Thanks!!
There are a couple of issues with your enter/update/exit pattern:
background = svgEnter.append("g")
.attr('class', 'background');
var headers = svgEnter.append("g")
.attr('class', 'headers')
.selectAll("text.header")
.data(data.Headers, function(d){return d;});
var total = svgEnter.append("g")
.attr('class', 'total');
This code is referencing svgEnter, which is OK the first time through, as svgEnter has a non-empty selection (it contains the svg you create a little earlier).
On subsequent calls to this function, svgEnter will contain an empty selection, as the svg element already exists. So, I have modified this part of your code to:
svgEnter.append('g')
.attr('class', 'background');
var background = svg.selectAll('g.background');
svgEnter.append('g')
.attr('class', 'headers')
var headers = svg.selectAll('g.headers').selectAll('text.header')
.data(data.Headers, function(d) { return d; });
svgEnter.append('g')
.attr('class', 'total');
var total = svg.selectAll('g.total');
This will create the g elements if we also have to create the svg element. It will then create the variables similar to your existing code using selections from the svg element.
I think that they're the only changes I made, the rest of your code works as expected.
An updated fiddle is at http://jsfiddle.net/vba6n4sh/9/