D3 v4: интервал и связывание элементов произвольно сложного массива с вложенными тегами g - PullRequest
0 голосов
/ 25 апреля 2018

TL; DR

Цель: иметь возможность размещать произвольные группы с переменным размером разделителя в зависимости от уровня группировки, где:

  • объекты соответственно сгруппированы в <g>теги
  • интервалы между объектами / группами имеют инкрементные переводы в соответствующих <g> тегах
  • привязка, ввод, обновление и выход из работы на правильных вложенных уровнях

в рекурсивной функции.

В настоящее время имеется: рекурсивная функция, которая:

  • корректно вкладывает объекты
  • , правильно размещает эти объекты
  • неправильно обновляеткогда даются новые данные

Хотя этот вопрос об интервале легко обобщить для других типов диаграмм (например, скрипка, рамп-юзер, даже ось рендеринга), для простоты давайте подойдемэта проблема в контексте создания простой гистограммы.

Наши данные будут представлены в массиве, где числовое значение соответствует высоте бара, а массив соответствует группе.

Например, если у нас было две парыиз двух точек данных мы могли бы создать сгруппированную гистограмму следующим образом:

[[1,4],[3,2]]

даст:

enter image description here

и

[2, [3,5], [[1,4,3], 2]]

будет производить:

enter image description here

Теперь первый пример более вероятен на практике, например, сравнивая значения 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]

может быть разнесен следующим образом

enter image description here

, где каждый уровень окрашен отдельно (например, первый уровень черный, и мы видим,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. проверить слой впереди и удалить любой элемент, который может не принадлежать (то, что я в настоящее время реализовал)

и, по общему признанию, первый (скорее всего, более сложный) приведет к более приятным переходам (на мой взгляд).

Разница заключается в том, что в 1 бары, которые уже существуют (на любом уровне группировки), повторно назначаются для баров в новых данных, а в 2 повторно используются только бары с тем же уровнем, если этот уровень существует для новые данные.

Ответы [ 2 ]

0 голосов
/ 04 мая 2018

Для записи ответ на мой вопрос был в моем оригинальном сообщении.

Я открыл щедрость, поскольку в то время я не был удовлетворен своим собственным решением, поскольку оно удалило слишком много элементов; а именно, элементы, которые должны быть заменены в одном месте (или вызваны с теми же данными), были удалены, что было очевидно, когда эти элементы имели переходы для их формы.

Более целенаправленным remove будет удаление строки:

selection.selectAll(':not([level="'+level+'"])').remove();

и добавьте:

currentSelection.each(function(d, i){
  ...
  if (Array.isArray(currentElement)) {
    move += makeNestedGroups(t, currentElement, horizontalQ, scale, moveby, objectClass, objectSize, spacerSize, level+1,transitionDuration, easeFunc)


    // BEGIN: add
    t.selectAll('g[level="'+(level)+'"] > g.'+objectClass).remove()
    // END: add


  }

  else {
  ...
  // BEGIN: add
  t.selectAll('g[level="'+(level+1)+'"]').remove()
  // END: add
  }
}
0 голосов
/ 04 мая 2018

После небольшой модификации ваш код работает. Изменил append ('g') для добавления ('rect'), добавил цвета и размеры и вызвал его из d3.interval (см. https://jsfiddle.net/w0wLgz3x/).

// snippet from makeNestedGroups, last iteration
if (obj.empty()) { // if empty, add
    obj = t.append('rect')
       .attr('class', objectClass)
       .attr('height', function(d) {
           return d * 20;
       })
       .attr('fill', function(d) {
           return colors[d%3];
       })
       .attr('width', function(d) {
           return 10;
       })
       .attr('parent-index', index);

Чтобы пояснить мое упоминание о более ориентированном на данные подходе, я также добавил предложение, которое - я должен признать - не генерирует все группы в SVG, поскольку они не нужны (вы можете изменить код, чтобы он соответствовал твои нужды).

Преимущество этого подхода заключается в четком разделении интересов. Модель данных изменяется в соответствии с вашей бизнес-логикой, а часть d3 имеет дело только с отображением данных (Model-View-Controller), что облегчает понимание каждой части.

Короче говоря, вы определяете модель данных

var a = { id: 1, height: 10, color: 'red', offset: 0};
var b = { id: 2, height: 20, color: 'green', offset: 0 };
var c = { id: 3, height: 50, color: 'orange', offset: 0 };
var d = { id: 4, height: 40, color: 'blue', offset: 0 };
var e = { id: 5, height: 50, color: 'black', offset: 0 };
var nodes = [ a, b, c, d ];

и возможные группировки

var datasets2 = [{
   order: [ a, [b, c], d ],
   visible: [ a, b, c, d ]
}, {
    order: [ a, d ],
    visible: [ a, d ]
}, {
    order: [ a, [ b, c ], d ],
    visible: [ a, b, c, d ]
}, {
    order: [ [a,  b], [d , c] ],
    visible: [ a, b, c, d ]
}]; 

Чтобы применить группировку, необходимо обновить смещения в модели данных. Это делается рекурсивно, в то время как рекурсия отслеживает текущее смещение. Я завернул все в функцию Bars с глобальными spacing и вспомогательными функциями first и rest (см. Скрипту).

function Bars() {}
// Update the offsets according to the dataset.order
Bars.prototype.update = function(data) {
    // recursive update
    function rec(head, tail, level, offset) {
        if (Array.isArray(head)) {
            // add larger spacing around groups
            offset += spacing/2;
            offset = rec(first(head), rest(head), level + 1, offset);
            offset += spacing/2;
            if(tail.length > 0) {
                    return rec(first(tail), rest(tail), level, offset);
            } else {
                return offset;
            }
        } else {
            offset = offset + spacing;
            head.offset = offset;
            head.level = level;
            if(tail.length > 0) {
                offset = rec(first(tail), rest(tail), level, offset);
            }
            return offset;
        }
    }
    rec(first(data), rest(data), 1, 20);
    return data;
}

Визуализация столбцов выполняется в пределах интервала d3.

d3.interval(function() {
    var mod = datasets2.length;
    var current = datasets2[(i++ % mod)];
    var data = update(current.order);
    console.log("-- ", i % mod, current);
    console.log(nodes.map(function(x) { return  x.id + ": " +  x.offset;  }));
    var bars = g.selectAll('rect')
            .data(current.visible);
    bars.exit().remove();
    bars.enter()
        .append('rect')
        .merge(bars)
        .attr('height', function(d) {
            return d.height;
        })
        .attr('width', function(d) {
            return d.width || 10;
        })
        .attr('fill', function(d) {
            return d.color;
        })
        //.style('opacity', 0.5)
        .transition()
        .duration(1000)
        .attr('transform', function(d, i) {
            var x = d.offset,
                y = 0;
            return 'translate('+ x +','+ 0 +')';
        });
}, 2000);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...