Объединение родительских и вложенных данных с d3.js
У меня есть структура данных, подобная этой (предположим, что структура данных не подлежит обсуждению):
data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]};
Используя java-библиотеку d3.js, я бы хотел нарисовать четыре прямоугольника, по одному для каждого цвета в массивах colors
, Информация из каждой записи в массиве segments
используется для рисования прямоугольников, соответствующих каждому цвету в массиве color
. Например, красный и зеленый прямоугольники будут иметь ширину и высоту 10. Полученный html должен выглядеть следующим образом:
<div id="container">
<svg width="200" height="200">
<g>
<rect x="20" y="20" width="10" height="10" fill="#ff0000"></rect>
<rect x="30" y="30" width="10" height="10" fill="#00ff00"></rect>
</g>
<g>
<rect x="40" y="40" width="20" height="20" fill="#0000ff"></rect>
<rect x="60" y="60" width="20" height="20" fill="#000000"></rect>
</g>
</svg>
</div>
Я придумал какой-то код, который выполняет это, но я нашел часть об использовании данных из двух разных уровней вложенности в data
, чтобы запутать, и я чувствую, что может быть более идиоматический способ выполнения то же самое с d3.js. Здесь код (полный пример в http://jsbin.com/welcome/39650/edit):
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
// add the top-level svg element and size it
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter()
.append('g');
// Add a rectangle for each color
nested
.selectAll('rect')
.data(function(d) {
// **** ATTENTION ****
// Is there a more idiomatic, d3-ish way to approach this?
var expanded = [];
for(var i = 0; i < d.colors.length; i++) {
expanded.push({
color : d.colors[i],
x : d.x
size : d.size });
}
return expanded;
})
.enter()
.append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
Есть ли лучший и/или более идиоматический способ доступа к данным из двух разных уровней вложенности в структуре данных, используя d3.js?
Изменить
Вот решение, с которым я столкнулся, благодаря meetamit answer для идеи закрытия, и используя более идиоматический отступ d3.js благодаря nautat answer:
$(function() {
var
vis = null,
width = 200,
height = 200,
data = {
segments : [
{x : 20, y : 0, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, y : 0, size : 20, colors : ['#0000ff','#000000']}
]
};
// set the color
function f(d,i) {return d;}
// set the position
function pos(segment) {
return function(d,i) {
return segment.x + (i * segment.size);
};
}
// set the size
function size(segment) {
return function() {
return segment.size;
};
}
// add the top-level svg element and size it
vis = d3.select('#container').append('svg')
.attr('width',width)
.attr('height',height);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter().append('g');
// Add a rectangle for each color. Size of rectangles is determined
// by the "parent" data object.
nested
.each(function(segment, i) {
var
ps = pos(segment),
sz = size(segment);
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors)
.enter().append('rect')
.attr('x', ps)
.attr('y',ps)
.attr('width', sz)
.attr('height',sz)
.attr('fill', f);
});
});
Здесь полный рабочий пример: http://jsbin.com/welcome/42885/edit
Ответы
Ответ 1
Вы можете использовать закрытие
var nested = vis
.selectAll('g')
.data(data.segments);
nested.enter()
.append('g')
.each(function(segment, i) {
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors);
colors.enter()
.append('rect')
.attr('x', function(color, j) { return pos(segment, j); })
// OR: .attr('x', function(color, j) { return segment.x + (j * segment.size); })
.attr('width', function(color, j) { return size(segment); })
.attr('fill', String);
});
Ответ 2
Для реструктуризации данных вы можете сделать что-то вроде следующего:
newdata = data.segments.map(function(s) {
return s.colors.map(function(d) {
var o = this; // clone 'this' in some manner, for example:
o = ["x", "size"].reduce(function(obj, k) { return(obj[k] = o[k], obj); }, {});
return (o.color = d, o);
}, s);
});
Это преобразует ваши входные данные в:
// newdata:
[
[
{"size":10,"x":20,"color":"#ff0000"},
{"size":10,"x":20,"color":"#00ff00"}],
[
{"size":20,"x":40,"color":"#0000ff"},
{"size":20,"x":40,"color":"#000000"}
]
]
который затем может использоваться в стандартном шаблоне выбора вложенных данных:
var nested = vis.selectAll('g')
.data(newdata)
.enter().append('g');
nested.selectAll('rect')
.data(function(d) { return d; })
.enter().append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
Кстати, если вы хотите быть более d3-идиоматичным, я бы немного изменил стиль отступов для прикованных методов. Майк предложил использовать половину отступов каждый раз, когда выбор меняется. Это помогает четко понять, на какой выбор вы работаете. Например, в последнем коде; переменная nested
относится к выбору enter()
. См. Главу "выборы" в: http://bost.ocks.org/mike/d3/workshop/
Ответ 3
Я попытался бы сгладить colors
, прежде чем вы начнете создавать элементы. Если произойдут изменения в данных, я бы затем обновил эту сплющенную структуру данных и перерисовал. Сглаженные данные должны быть где-то сохранены, чтобы сделать реальные переходы d3 возможным.
Вот длинный пример, который работал у меня. Yon может видеть это в действии здесь.
Вот код:
var data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]
};
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
function flatten(data) {
// converts the .colors to a ._colors list
data.segments.forEach( function(s,i) {
var list = s._colors = s._colors || [];
s.colors.forEach( function(c,j) {
var obj = list[j] = list[j] || {}
obj.color = c
obj.x = s.x
obj.size = s.size
});
});
}
function changeRect(chain) {
return chain
.transition()
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f)
.style('fill-opacity', 0.5)
}
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the top-level svg element and size it
function update(){
flatten(data);
// add the nested svg elements
var all = vis.selectAll('g')
.data(data.segments)
all.enter().append('g');
all.exit().remove();
// Add a rectangle for each color
var rect = all.selectAll('rect')
.data(function (d) { return d._colors; }, function(d){return d.color;})
changeRect( rect.enter().append('rect') )
changeRect( rect )
rect.exit().remove()
}
function changeLater(time) {
setTimeout(function(){
var ds = data.segments
ds[0].x = 10 + Math.random() * 100;
ds[0].size = 10 + Math.random() * 100;
ds[1].x = 10 + Math.random() * 100;
ds[1].size = 10 + Math.random() * 100;
if(time == 500) ds[0].colors.push("orange")
if(time == 1000) ds[1].colors.push("purple")
if(time == 1500) ds[1].colors.push("yellow")
update()
}, time)
}
update()
changeLater(500)
changeLater(1000)
changeLater(1500)
Важно здесь функция flatten
, которая выполняет преобразование данных и сохраняет/повторно использует результат как свойство _colors
в родительском элементе данных. Другая важная строка:
.data(function (d) { return d._colors; }, function(d){return d.color;})
который определяет, где получить данные (первый параметр) И каков уникальный идентификатор для каждого элемента данных (второй параметр). Это помогает идентифицировать существующие цвета для переходов и т.д.