D3. Создайте облако изображений с D3 - PullRequest
1 голос
/ 05 мая 2020

Я пытаюсь создать компонент Image Cloud, как на изображении. Example

Я пробовал использовать простой алгоритм d3 с прямоугольниками и усилием для центрирования изображений и их распределения с столкновениями. Проблема в том, что это выглядит так:

My attempt

Изображения выглядят слишком далеко друг от друга, и даже с меньшим радиусом столкновения облако не выглядит Не использовать все доступное пространство (помещать изображения меньшего размера между промежутками). Есть ли способ сделать этот компонент с макетами, предоставленными d3? я должен изменить какой-то другой макет? Я знаю, что могу реализовать квадратную силу столкновения, но результаты не выглядят так, как хотелось бы.

Любая помощь приветствуется. Большое спасибо.

РЕДАКТИРОВАТЬ: И снова здравствуйте. Я наконец-то сделал это похожим на желаемый результат. К сожалению, теперь у меня больше проблем. Первый и самый важный: большие изображения должны располагаться близко друг к другу и быть как можно более центрированными. вне файла (см. первый фрагмент кода) проблема в том, что когда я обновляю данные, изображение просто исчезает, и он больше не настраивает макет. Как мне заставить его обновить? Временный прием, который я сделал, - это удалить все изображения и прямоугольники из DOM перед рендерингом.

Опять же, спасибо за ваше время.

Редактировать: мне удалось получить все, что я хотел. Еще есть возможности для улучшения, но пока это работает. Мне удалось получить наблюдателя за div и неплохое управление состоянием с помощью React. Когда-нибудь я загружу компонент React в github

// specify svg width and height;
const width = 500, height = 500;
const listenTo = Math.min(width, height);
// create svg and g DOM elements;
let svg = d3.select('body')
  .append('svg')
  .attr('xmlns', 'http://www.w3.org/2000/svg')
  .attr('width', listenTo)
  .attr('height', listenTo)
  .append('g')
  // move 0,0 to the center
  .attr('transform', `translate(${width >>1}, ${height>>1})`);

var images = [], maxImages = 100, maxWeight = 50, minWeight = 1, padding=3;
for(let i = 0; i< maxImages -1; i++){
const weight = (Math.random() *(maxWeight - minWeight)) + minWeight;
  images.push({
    url:`https://via.placeholder.com/100?text=${Math.ceil(weight)}`,
    weight
  })
}
// make one image with a weight 3 times bigger for visualization testing propouses
images.push({
  url: `https://via.placeholder.com/100?text=${maxWeight * 3}`,
  weight: maxWeight * 3,
  fx: 0,
  fy: 0
})
images.sort((a, b) => b.weight - a.weight);

// make it so the biggest images is equal to 10% of canvas, and thre smallest one 1%
const scl = ((100 / maxImages) / 100);
console.log(scl);
const maxImageSize = listenTo * 0.1;
const minImageSize = listenTo * scl;

// function to scale the images
const scaleSize = d3.scaleLinear().domain([minWeight, maxWeight*3]).range([minImageSize, maxImageSize]).clamp(true);

// append the rects
let vizImages = svg.selectAll('.image-cloud-image')
  .data(images)
  .enter()
  .append('svg:image')
  .attr('class', '.image-cloud-image')
  .attr('height', d => scaleSize(d.weight))
  .attr('width', d => scaleSize(d.weight))
  .attr('id', d => d.url)
  .attr('xlink:href', d => d.url);
  vizImages.exit().remove();

// create the collection of forces
const simulation = d3.forceSimulation()
  // set the nodes for the simulation to be our images
  .nodes(images)
  // set the function that will update the view on each 'tick'
  .on('tick', ticked)
  .force('center', d3.forceCenter())
  .force('cramp', d3.forceManyBody().strength(listenTo / 100))
  // collition force for rects
  .force('collide', rectCollide().size(d=> {
  const s = scaleSize(d.weight);
  return [s + padding, s + padding];
  }));

// update the position to new x and y
function ticked() {
  vizImages.attr('x', d => d.x).attr('y', d=> d.y);
}

// Rect collition algorithm. i don't know exactly how it works
// https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
function rectCollide() {
    var nodes, sizes, masses
    var size = constant([0, 0])
    var strength = 1
    var iterations = 1

    function force() {
        var node, size, mass, xi, yi
        var i = -1
        while (++i < iterations) { iterate() }

        function iterate() {
            var j = -1
            var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)

            while (++j < nodes.length) {
                node = nodes[j]
                size = sizes[j]
                mass = masses[j]
                xi = xCenter(node)
                yi = yCenter(node)

                tree.visit(apply)
            }
        }

        function apply(quad, x0, y0, x1, y1) {
            var data = quad.data
            var xSize = (size[0] + quad.size[0]) / 2
            var ySize = (size[1] + quad.size[1]) / 2
            if (data) {
                if (data.index <= node.index) { return }

                var x = xi - xCenter(data)
                var y = yi - yCenter(data)
                var xd = Math.abs(x) - xSize
                var yd = Math.abs(y) - ySize

                if (xd < 0 && yd < 0) {
                    var l = Math.sqrt(x * x + y * y)
                    var m = masses[data.index] / (mass + masses[data.index])

                    if (Math.abs(xd) < Math.abs(yd)) {
                        node.vx -= (x *= xd / l * strength) * m
                        data.vx += x * (1 - m)
                    } else {
                        node.vy -= (y *= yd / l * strength) * m
                        data.vy += y * (1 - m)
                    }
                }
            }

            return x0 > xi + xSize || y0 > yi + ySize ||
                   x1 < xi - xSize || y1 < yi - ySize
        }

        function prepare(quad) {
            if (quad.data) {
                quad.size = sizes[quad.data.index]
            } else {
                quad.size = [0, 0]
                var i = -1
                while (++i < 4) {
                    if (quad[i] && quad[i].size) {
                        quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
                        quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
                    }
                }
            }
        }
    }

    function xCenter(d) { return d.x + d.vx + sizes[d.index][0] / 2 }
    function yCenter(d) { return d.y + d.vy + sizes[d.index][1] / 2 }

    force.initialize = function (_) {
        sizes = (nodes = _).map(size)
        masses = sizes.map(function (d) { return d[0] * d[1] })
    }

    force.size = function (_) {
        return (arguments.length
             ? (size = typeof _ === 'function' ? _ : constant(_), force)
             : size)
    }

    force.strength = function (_) {
        return (arguments.length ? (strength = +_, force) : strength)
    }

    force.iterations = function (_) {
        return (arguments.length ? (iterations = +_, force) : iterations)
    }

    return force
  }
  function constant(_) {
    return function () { return _ }
  }

Вот скрипка для будущих людей

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...