TL; DR
Цель: иметь возможность размещать произвольные группы с переменным размером разделителя в зависимости от уровня группировки, где:
- объекты соответственно сгруппированы в
<g>
теги - интервалы между объектами / группами имеют инкрементные переводы в соответствующих
<g>
тегах - привязка, ввод, обновление и выход из работы на правильных вложенных уровнях
в рекурсивной функции.
В настоящее время имеется: рекурсивная функция, которая:
- корректно вкладывает объекты
- , правильно размещает эти объекты
- неправильно обновляеткогда даются новые данные
Хотя этот вопрос об интервале легко обобщить для других типов диаграмм (например, скрипка, рамп-юзер, даже ось рендеринга), для простоты давайте подойдемэта проблема в контексте создания простой гистограммы.
Наши данные будут представлены в массиве, где числовое значение соответствует высоте бара, а массив соответствует группе.
Например, если у нас было две парыиз двух точек данных мы могли бы создать сгруппированную гистограмму следующим образом:
[[1,4],[3,2]]
даст:
и
[2, [3,5], [[1,4,3], 2]]
будет производить:
Теперь первый пример более вероятен на практике, например, сравнивая значения a и valueб между группой х и группой у.Последний труднее увидеть конкретный пример, но на практике и функция интервалов стоит того, чтобы ее соль стоила обрабатывать произвольно сложные группировки.Скорее всего, у вас будут одинаково вложенные группировки, чем неравные, как показано здесь.
Тем не менее, в примере 2 ясно, что каждый элемент на «уровне 0» (2
, [3,5]
и [[1,4,3],2]
) имеют одинаковые по размеру и большие проставки между ними.Для элементов на "уровне 1" (3
и 5
, а также [1,4,3]
и 2
) предусмотрена прокладка меньшего размера, а для элементов на "уровне 2" (1
, 4
и 3
) там самая маленькая проставка.
Некоторые вещи, на которые следует обратить внимание:
каждое из этих изображений создается с помощью кода в этом примере (поэтому я близок к желаемому ответу)
Я использую термин «уровень» для обозначения глубины вложения значения, при котором JS имеет индекс 0, элементы в основном массиве находятся на уровне 0 и т. Д.
в этом примере моя функция раскраски по умолчанию окрашивает каждый столбец по индексу по отношению к его родительскому элементу (опять же - каждый столбец встречается во вложении тегов <g>
, соответствующих вложенности, видимой в переданном массиве).
"произвольная сложность" - здесь - относится к массиву, состоящему только из числовых значений и массивов только числовых значений (например, вложенных массивов)
Итак, чтобы подвести итог:
ЦЕЛЬ:
При заданном массиве произвольной сложности создайте эквивалентную группировку в SVG, разнесенную по уровню, на котором происходит значение с соответствующей происходящей трансляциейв соответствующей группе(Тег <g>
).Кроме того, привязка данных не требует полного удаления существующих элементов, когда функция вызывается с другими данными.
Путь к тому месту, где мы сейчас находимся
(см. Изображения выше)
В настоящее время у нас есть рекурсия, вложенные группы и правильный перевод.Осталось исправить функции привязки, ввода, обновления, выхода.
Итерация 1 - просто уменьшить интервал
Пример:
Учитывая следующий демонстрационный массив
demo = [1,1,1,1,[2,2,2],[[3,3],[[4,4,4]]], 1]
может быть разнесен следующим образом
, где каждый уровень окрашен отдельно (например, первый уровень черный, и мы видим,5 черных квадратов для первых четырех 1 в массиве и последней 1 в массиве).
Как воспроизвести пример
Изображение выше можно вычислить путем рекурсивного перемещения каждого элемента всех уровней с помощью:
- количество не элементов массива на любом уровне перед ним * ширина отображаемых элементов
- совокупное количество «спейсеров» между каждым элементом (не включающим массив)
Проставка на любом уровне может быть определена как:
1 / (level + 1) // assuming zero indexed e.g. the lowest level is level 0
// where the 1s are in the demo array
Чтобы получить массив общего количества спейсера, который идет перед элементом non -array с индексом i , я написал эту рекурсивную функцию:
function cumulativeSpacer(array, level, cSpacerData, cSpacer) {
level = level == undefined ? 0 : level
cSpacerData = cSpacerData == undefined ? [] : d
cSpacer = cSpacer == undefined ? 0 : cSpacer
a.map(function(arr, ind){
if ( ind == 0 ) { cSpacer }
else {cSpacer += (1 / (level+1))}
if (Array.isArray(arr)) {cumulativeSpacer(arr, level+1, cSpacerData, cSpacer); cSpacer = cSpacerData.last()}
else {cSpacerData.push(cSpacer)}
})
return cSpacerData
}
Что если мы запустим демонстрационный массив, то получим:
Array [ 0, 1, 2, 3, 4, 4.5, 5, 6, 6.333333333333333, 6.833333333333333, 7.083333333333333, 7.333333333333333, 8.333333333333332 ]
Первый элемент уровня - это , а не , смещенный на спейсер, т.е. на заданном уровне с длиной n имеется n -1 спейсеров.
Итерация 2 - переместить интервал на <g>
Пока работает вышеуказанная функция, я бы хотел изменить рекурсию.
Для тех, кто знаком с SVG, им известен тег <g>
группы. Предположим, что я поместил каждый элемент во вложенные группы (по одному на каждый уровень элемента). Затем я хотел бы рассчитать перевод для каждого тега группы, а не для самих элементов.
например.
<g level=0>
<rect...> (element index: 0, value: 1)
</g>
<g level=0>
<rect...> (element index: 1, value: 1)
</g>
<g level=0>
<rect...> (element index: 2, value: 1)
</g>
<g level=0>
<rect...> (element index: 3, value: 1)
</g>
<g level=0>
<g level=1>
<rect...> (element index: 4, value: 2)
</g>
<g level=1>
<rect...> (element index: 5, value: 2)
</g>
<g level=1>
<rect...> (element index: 6, value: 2)
</g>
<rect...>
</g>
...
Функция для создания вложенных групп
function createNestedGroups(selection, data, objClass, level, index) {
var currentSelection = selection.selectAll('g[level="'+level+'"]')
currentSelection = currentSelection.data(data)
currentSelection.exit().remove()
index = index == undefined ? 0 : index
currentSelection = currentSelection.enter().append('g').attr('level', level)
currentSelection.each(function(currentData, index) {
var t = d3.select(this)
if (Array.isArray(currentData)) { createNestedGroups(t, currentData, objClass, level+1, index)}
else { t.append('g').attr('class', objClass) }
})
return level
}
Таким образом, если мы имеем:
createNestedGroups (d3.select ('g.test'), демонстрация, 'item-container')
создаст вышеуказанную вложенную группировку
тогда мы можем добиться раскраски:
d3.select('g.test').selectAll('g.item-container').each(function(d, i) {
var t = d3.select(this)
var b = t.select('rect').empty() ? t.append('rect') : t.select('rect')
b.attr('width', 10)
b.attr('height', 10)
.attr('fill', function(dd, k) {
var l = d3.select(t.node().parentNode).attr('level')
if (l == 0) {return "black"}
if (l == 1) {return "blue"}
if (l == 2) {return "red"}
if (l == 3) {return "purple"}
})
})
Если мы обновим makeNestedGroups
, чтобы попытаться включить движение:
function makeNestedGroups(selection, data, objectClass, objectSize, spacerSize, level, cumulativeIndex, cumulativeSpacer, m) {
if (cumulativeSpacer == undefined) {console.log('type\tlevel\tindex\tcumI\tcumSL\tcumS');}
cumulativeIndex = cumulativeIndex == undefined ? 0 : cumulativeIndex
cumulativeSpacer = cumulativeSpacer == undefined ? 0 : cumulativeSpacer
m = m == undefined ? 0:m
// bind, remove, update, and append new groups for the current level
var currentSelection = selection.selectAll('g[level="'+level+'"]')
currentSelection = currentSelection.data(data)
currentSelection.exit().remove()
currentSelection = currentSelection.enter().append('g').attr('level', level)
var cumulativeSpacerAtLevel = 0
currentSelection.each(function(currentElement, index) {
var t = d3.select(this)
if (index) {cumulativeSpacerAtLevel += spacerSize * 1 / (level+1)}
// if (level == 1) {console.log(m, cumulativeSpacer, cumulativeSpacerAtLevel)}
// if (index) { move += baseSpacerSize * 1 / (level+1) * index}
if (Array.isArray(currentElement)) {
// console.log("HERE")
// console.log(level, index, cumulativeSpacer)
[cumulativeIndex, cumulativeSpacer, m] = makeNestedGroups(t, currentElement, objectClass, objectSize, spacerSize, level+1, cumulativeIndex, cumulativeSpacerAtLevel,m)
m += cumulativeSpacer
t.attr('transform', 'translate('+m+',0)')
console.log("ARR", level, index, cumulativeIndex, cumulativeSpacerAtLevel, cumulativeSpacer)
}
else {
// m = cumulativeIndex * objectSize
// m += cumulativeSpacerAtLevel
console.log()
// t.attr('transform', 'translate('+m+',0)')
t.append('g').attr('class', objectClass);
// currentElement is not an array, update collectiveIndex
console.log("NOT", level, index, cumulativeIndex, cumulativeSpacerAtLevel, cumulativeSpacer)
cumulativeIndex += 1
m += objectSize
}
})
return [cumulativeIndex, cumulativeSpacer + cumulativeSpacerAtLevel, m]
}
затем (с закомментированными строками) мы видим, что большинство элементов перемещено правильно для количества исходящих элементов.
Однако заставить его работать как прежде почему-то стало для меня неясным.
например. последний элемент в массиве demo
(a 1) необходимо переместить на 6 * level_0 spacer
+ 3 * level_1 spacer
+ 1 * level_2 spacer
+ 2 *level_3 spacer
как есть:
- 6 элементов, продолжающих его на уровне 0
- 5 предметов на уровне 1 (
2,2,2, [3,3], [[4,4,4]]
), но для них нужно 3 проставки
- 3 предмета на уровне 2 (
3,3, [4,4,4]
), но между ними требуется 1 проставка
- 3 предмета на уровне 3 (
4,4,4
), но между ними 2 проставки
Итерация 3 - исправление итерации 2
Эта функция производит желаемую групповую вложенность, а также соответствующие трансляции на каждом уровне группы:
function makeNestedGroups(
selection, // container where levels are to be added
data, // list of aribtrary number of mixed numbers and lists
horizontalQ, // whether it should be spaced horizontallly or vertically
scale, // scale for the values in the list
objectClass, // what the container for the object be
objectSize, // object size
spacerSize, // base size to move object over by
level, // current level (nesting)
transitionDuration, // how long transitions should take
easeFunc // transition easing function
) {
// default value for level
if ( horizontalQ == undefined ) { horizontalQ = true; }
if ( level == undefined ) { level = 0; }
if ( transitionDuration == undefined ) { transitionDuration=1000; }
if ( easeFunc == undefined ) { easeFunc = d3.easeExp; }
/*
* NEED HELP HERE
*/
// select all current level groups
var currentSelection = selection.selectAll('g[level="'+level+'"]')
// bind data
currentSelection = currentSelection.data(data)
// add new group for all sub elements
var enter = currentSelection.enter().append('g').attr('level', level)
// remove excess
var exit = currentSelection.exit().remove()
currentSelection = currentSelection.merge(enter)
// removes too much
selection.selectAll(':not([level="'+level+'"])').remove()
// spacer for current level
var levelSpacer = spacerSize / (level+1)
// movement for current level
var move = 0
currentSelection.each(function(currentElement, index) {
// this selection
var t = d3.select(this)
// move container
t.transition().duration(transitionDuration).ease(easeFunc)
.attr('transform', function(d, i) {
var
x = horizontalQ ? move : 0,
y = !horizontalQ ? move: 0,
t = 'translate('+x+','+y+')'
return t
})
// If currentElement is an array ---> recurse
if (Array.isArray(currentElement)) {
move += makeNestedGroups(t, currentElement, horizontalQ, scale, objectClass, objectSize, spacerSize, level+1,transitionDuration, easeFunc)
}
else {
// move over by object size
move += objectSize
// grab object
var obj = t.select('g[level="'+level+'"] > g.'+objectClass).attr('parent-index', index).attr('data', currentElement)
if (obj.empty()) { // if empty, add
obj = t.append('g')
.attr('class', objectClass)
.attr('parent-index', index)
}
} // end else for if cur is array
// move over by a spacer if not last element
move += (index == currentSelection.size()-1) ? 0 : levelSpacer
})
return move
}
но если я вызываю с другой структурой массива, то иногда получаю дубликаты ящиков, или он удаляет все ящики и воссоздает их.
Проблема в итерации 2 заключалась в том, что недостаточно подсчитать количество элементов на уровне, замеченном до сих пор, и сделать выводы о количестве необходимых прокладок. Если на уровне 1 есть два элемента, и каждый из них находится в своем собственном массиве, то между ними необходимы 0 разделителей на уровне 1.
Что осталось?
Исправление привязки, ввода, обновления и выхода
В итерации 3 я указываю:
/*
* NEED HELP HERE
*/
// select all current level groups
var currentSelection = selection.selectAll('g[level="'+level+'"]')
// bind data
currentSelection = currentSelection.data(data)
// add new group for all sub elements
var enter = currentSelection.enter().append('g').attr('level', level)
// remove excess
var exit = currentSelection.exit().remove()
currentSelection = currentSelection.merge(enter)
// removes too much
selection.selectAll(':not([level="'+level+'"])').remove()
Мне понятно, почему это ошибка.
Для тех, кто может быть немного озадачен, проблема связана с желанием обработать произвольную сложность (например, смешанные значения и вложенные массивы).
Кроме того, это связано с моим собственным желанием вкладывать данные в теги <g>
.
Выбор всех тегов <g level="currentLevel">
включает сочетание тегов <g class=objectClass>
и <g level="currentLevel+1">
.
Таким образом, если data1
имеет в общей сложности 5 баров, а data2
имеет в общей сложности 7 баров (каждый в своей произвольной вложенности), то тогда 5 баров из data1
преобразуются в местоположение первых 5 баров data2
не так просто, если они не соответствуют одному и тому же вложению.
Таким образом, я думаю, что нужно сделать более тщательный отбор или элементы должны быть связаны, введены, обновлены и завершены по отдельности.
Несмотря на то, что я вижу эту ошибку, мне неясно, какое наиболее эффективное решение заключается в получении нужных вложенных элементов и удалении элементов без создания дубликатов с другой вложенной структурой.
Примечание для @Christoph
Прежде всего, я ценю ваш ответ, советы и время, потраченное на то, чтобы помочь мне найти решение. Я думаю, однако, есть некоторое недоразумение.
Предоставленная мною функция была обобщением для разнесения любой сложной группировки. Функция могла бы расставлять сгруппированные участки скрипки, сгруппированные коробки и усы и другие объекты, которым требуется более одного элемента, потому что она добавляет <objectClass>
к вложенному g
, так что затем можно вызвать <container-selection>.selectAll(g.<objectClass>)
, а затем .each(function(d, i) {... /*make whatever crazy data-driven shape here.*/})
делать то, что они хотят. Кроме того, этот рекурсивный интервал также является обобщением, поскольку многие bl.ocks с сгруппированными барами / скрипками будут иметь такие переменные, как innerSpacer
и outerSpacer
; Я считаю такой подход довольно неуклюжим и не надежным.
Я согласен, что функции построения графиков должны просто «строить» данные; Соответственно, ни в одной точке эта функция не выходит за рамки предварительной обработки данных. Код, который я предоставил здесь, является M.W.E. чтобы создать элементы распорок и создать неправильную привязку ... он просто не показывает какую-либо форму, поскольку это другая функция (проверка, является ли выбор пустым, а затем добавление, если необходимо, прямолинейно) и мои замыкания для этого (весь гистограмма) слишком велики, чтобы соответствовать этому и без того длинному вопросу.
В моих замыканиях есть данные в удобной для использования "плоской" форме и переменная grouping
, указывающая, как данные должны быть упорядочены / сгруппированы.
Дело в том, что предоставленная мною функция была достаточно абстрагированной, чтобы вы могли построить на ней не менее гибкую функцию, цель которой - повторное использование (как часто подчеркивает Босток). Я надеюсь, вам понятно, как вместо добавления rect
(как вы выбрали), g.<objectClass>
означает, что эту функцию можно повторно использовать на графике баров, графиках нарушения и т. Д.
Что касается моделей данных, я думаю, что мы оба можем согласиться с тем, что те, что мы здесь предоставили, являются лишь краткими упрощениями для демонстрации / отладки.
Что касается обновления интервалов, то функция, как я ее предоставил, делает это уже при повторном вызове. Это не было проблемой. Проблема и этот вопрос о вложенных g
тегах.
Вы заявляете, что они совершенно необходимы. Это вроде как правда. Просто чтобы получить правильный интервал, абсолютно. Гораздо проще (и более естественно) вычислить рекурсивный интервал без вложенности, чем с помощью (следовательно, это более поздняя итерация); тем не менее, нетрудно представить пример использования, в котором вы хотели бы выделить, перетащить, переместить или применить любое другое событие для вложенной группы . В каком случае, какую реализацию легче применить? Сглаженная версия (как вы реализовали и я сделал изначально) или вложенная версия? Это последнее (если вам не нравится работать с parentNodes, что все еще несколько неуклюже в d3
).
Итак, кратко
- благодарю вас. Я ценю ваше время, усилия, вклад и ответ. +1.
- это не ответ на заданный вопрос. Даже для вложенного ответа вы просто добавили столбики, но они по-прежнему удаляют слишком много.
Два способа попытаться найти ответ:
- отметьте слой впереди и при необходимости переместите детей к новым родителям
- проверить слой впереди и удалить любой элемент, который может не принадлежать (то, что я в настоящее время реализовал)
и, по общему признанию, первый (скорее всего, более сложный) приведет к более приятным переходам (на мой взгляд).
Разница заключается в том, что в 1
бары, которые уже существуют (на любом уровне группировки), повторно назначаются для баров в новых данных, а в 2
повторно используются только бары с тем же уровнем, если этот уровень существует для новые данные.