Изменение размера SVG при изменении размера окна в d3.js

183

Я рисую диаграмму рассеяния с d3.js. С помощью этого вопроса:
получить размер экрана, текущей веб-страницы и окна браузера

Я использую этот ответ:

var w = window,
    d = document,
    e = d.documentElement,
    g = d.getElementsByTagName('body')[0],
    x = w.innerWidth || e.clientWidth || g.clientWidth,
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

Так что я могу разместить свой график в окне пользователя следующим образом:

var svg = d3.select("body").append("svg")
        .attr("width", x)
        .attr("height", y)
        .append("g");

Теперь я хотел бы, чтобы кто-то позаботился об изменении размера графика, когда пользователь изменяет размер окна.

PS: я не использую jQuery в своем коде.

mthpvg
источник

Ответы:

295

Ищите «отзывчивый SVG», сделать SVG отзывчивым довольно просто, и вам больше не нужно беспокоиться о размерах.

Вот как я это сделал:

d3.select("div#chartId")
   .append("div")
   // Container class to make it responsive.
   .classed("svg-container", true) 
   .append("svg")
   // Responsive SVG needs these 2 attributes and no width and height attr.
   .attr("preserveAspectRatio", "xMinYMin meet")
   .attr("viewBox", "0 0 600 400")
   // Class to make it responsive.
   .classed("svg-content-responsive", true)
   // Fill with a rectangle for visualization.
   .append("rect")
   .classed("rect", true)
   .attr("width", 600)
   .attr("height", 400);
.svg-container {
  display: inline-block;
  position: relative;
  width: 100%;
  padding-bottom: 100%; /* aspect ratio */
  vertical-align: top;
  overflow: hidden;
}
.svg-content-responsive {
  display: inline-block;
  position: absolute;
  top: 10px;
  left: 0;
}

svg .rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<div id="chartId"></div>

Примечание: все в изображении SVG будет масштабироваться с шириной окна. Это включает в себя ширину обводки и размеры шрифта (даже те, которые установлены с помощью CSS). Если это нежелательно, ниже приведены более сложные альтернативные решения.

Больше информации / учебники:

http://thenewcode.com/744/Make-SVG-Responsive

http://soqr.fr/testsvg/embed-svg-liquid-layout-responsive-web-design.php

cminatti
источник
8
Это гораздо более элегантно и также использует подход к масштабированию контейнеров, который должен присутствовать в каждом наборе инструментов фронт-энда разработчика (может использоваться при масштабировании чего-либо с определенным соотношением сторон). Должен быть главный ответ.
Контур
13
Просто чтобы добавить к этому: viewBoxатрибут должен быть установлен на соответствующую высоту и ширину вашего графика SVG: .attr("viewBox","0 0 " + width + " " + height) где widthи heightопределены в другом месте. То есть, если вы не хотите, чтобы ваш SVG обрезался в окне просмотра. Вы также можете обновить padding-bottomпараметр в вашем CSS, чтобы он соответствовал соотношению сторон вашего элемента SVG. Хотя отличный ответ - очень полезно, и работает удовольствие. Спасибо!
Эндрю Гай
13
Основываясь на той же идее, было бы интересно дать ответ, который не предполагает сохранения соотношения сторон. Вопрос был в том, чтобы иметь элемент svg, который покрывает все окно при изменении размера, что в большинстве случаев означает другое соотношение сторон.
Николя Ле Тьерри д'Аннекин
37
Просто примечание относительно UX: В некоторых случаях использование вашей диаграммы d3 на основе SVG как актива с фиксированным соотношением сторон не является идеальным. Диаграммы, которые отображают текст, или являются динамическими, или в целом больше похожи на правильный пользовательский интерфейс, чем на актив, могут получить большую выгоду за счет повторного рендеринга с обновленными измерениями. Например, viewBox заставляет оси шрифтов и тики масштабировать вверх / вниз, что может показаться неудобным по сравнению с HTML-текстом. Напротив, повторный рендеринг позволяет графику сохранять постоянный размер надписи, плюс он дает возможность добавлять или удалять тики.
Карим Эрнандес
1
Почему top: 10px;? Кажется неправильным для меня и обеспечивает смещение. Установка на 0px работает нормально.
ТимЗаман
43

Используйте window.onresize :

function updateWindow(){
    x = w.innerWidth || e.clientWidth || g.clientWidth;
    y = w.innerHeight|| e.clientHeight|| g.clientHeight;

    svg.attr("width", x).attr("height", y);
}
d3.select(window).on('resize.updatesvg', updateWindow);

http://jsfiddle.net/Zb85u/1/

Адам Пирс
источник
11
Это не очень хороший ответ, так как он предполагает привязку к событиям DOM ... Гораздо лучшим решением будет использование только d3 и css
alem0lars
@ alem0lars, как ни странно, версия css / d3 у меня не сработала, а эта сработала ...
Кристиан
32

ОБНОВЛЕНИЕ просто используйте новый способ от @cminatti


старый ответ для исторических целей

IMO, лучше использовать select () и on (), так как таким образом вы можете иметь несколько обработчиков событий изменения размера ... просто не сходите с ума

d3.select(window).on('resize', resize); 

function resize() {
    // update width
    width = parseInt(d3.select('#chart').style('width'), 10);
    width = width - margin.left - margin.right;

    // resize the chart
    x.range([0, width]);
    d3.select(chart.node().parentNode)
        .style('height', (y.rangeExtent()[1] + margin.top + margin.bottom) + 'px')
        .style('width', (width + margin.left + margin.right) + 'px');

    chart.selectAll('rect.background')
        .attr('width', width);

    chart.selectAll('rect.percent')
        .attr('width', function(d) { return x(d.percent); });

    // update median ticks
    var median = d3.median(chart.selectAll('.bar').data(), 
        function(d) { return d.percent; });

    chart.selectAll('line.median')
        .attr('x1', x(median))
        .attr('x2', x(median));


    // update axes
    chart.select('.x.axis.top').call(xAxis.orient('top'));
    chart.select('.x.axis.bottom').call(xAxis.orient('bottom'));

}

http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/

SLF
источник
Я не могу согласиться меньше. Метод cminatti будет работать со всеми файлами d3js, с которыми мы имеем дело. Этот метод преобразования с изменением размера на график является излишним. Кроме того, это должно быть переписано для каждого возможного графика, который мы могли придумать. Метод, упомянутый выше, работает для всего. Я пробовал 4 рецепта, включая тип перезагрузки, который вы упомянули, и ни один из них не работает для графиков с несколькими областями, таких как тип, используемый в кубизме.
Имонн Кенни
1
@EamonnKenny Я не могу с тобой согласиться. Другой ответ 7 месяцев в будущем лучше :)
SLF
Для этого метода: "d3.select (окно) .on" Я не смог найти "d3.select (окно) .off" или "d3.select (окно) .unbind"
морской кг
Этот ответ ни в коем случае не уступает. Их ответ будет масштабировать каждый аспект графика (включая галочки, оси, линии и текстовые аннотации), которые могут выглядеть неловко, на некоторых экранах, плохо на других и нечитаемыми в экстремальных размерах. Я считаю, что лучше изменить размер, используя этот (или, для более современных решений, @Angu Agarwal) метод.
Рунар Берг
12

Это немного уродливо, если код изменения размера почти такой же длинный, как и код для построения графика. Поэтому вместо изменения размера каждого элемента существующей диаграммы, почему бы просто не перезагрузить его? Вот как это работает для меня:

function data_display(data){
   e = document.getElementById('data-div');
   var w = e.clientWidth;
   // remove old svg if any -- otherwise resizing adds a second one
   d3.select('svg').remove();
   // create canvas
   var svg = d3.select('#data-div').append('svg')
                                   .attr('height', 100)
                                   .attr('width', w);
   // now add lots of beautiful elements to your graph
   // ...
}

data_display(my_data); // call on page load

window.addEventListener('resize', function(event){
    data_display(my_data); // just call it again...
}

Решающая линия d3.select('svg').remove();. В противном случае каждое изменение размера добавит еще один элемент SVG ниже предыдущего.

Райк
источник
Это абсолютно убьет производительность, если вы перерисовываете весь элемент в каждом событии изменения размера окна
sdemurjian
2
Но это правильно: во время рендеринга вы можете определить, должна ли у вас быть ось вставки, скрыть легенду, делать другие вещи на основе доступного контента. Используя чисто CSS / viewbox решение, все, что он делает, это делает визуализацию сжатой. Хорошо для изображений, плохо для данных.
Крис Нолл
8

В силовых макетах простая установка атрибутов 'height' и 'width' не поможет перецентрировать / переместить график в контейнер svg. Тем не менее, есть очень простой ответ , который работает для Force макетов нашел здесь . В итоге:

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

window.on('resize', resize);

Тогда, если у вас есть переменные svg & force:

var svg = /* D3 Code */;
var force = /* D3 Code */;    

function resize(e){
    // get width/height with container selector (body also works)
    // or use other method of calculating desired values
    var width = $('#myselector').width(); 
    var height = $('#myselector').height(); 

    // set attrs and 'resume' force 
    svg.attr('width', width);
    svg.attr('height', height);
    force.size([width, height]).resume();
}

Таким образом, вы не перерисовываете граф полностью, мы устанавливаем атрибуты, и d3 пересчитывает вещи по мере необходимости. По крайней мере, это работает, когда вы используете точку притяжения. Я не уверен, является ли это обязательным условием для этого решения. Кто-нибудь может подтвердить или опровергнуть?

Ура, г

Gavs
источник
Это отлично сработало на моем раскладе сил, который имеет около 100 соединений. Спасибо.
tribe84
1

Для тех, кто использует форсированные графы в D3 v4 / v5, этот sizeметод больше не существует. Что-то вроде следующего работало для меня (на основе этой проблемы github ):

simulation
    .force("center", d3.forceCenter(width / 2, height / 2))
    .force("x", d3.forceX(width / 2))
    .force("y", d3.forceY(height / 2))
    .alpha(0.1).restart();
Джастин льюис
источник
1

Если вы хотите привязать пользовательскую логику к событию изменения размера, в настоящее время вы можете начать использовать браузерный API ResizeObserver для ограничительной рамки SVGElement.
Это также будет обрабатывать случай, когда размер контейнера изменяется из-за изменения размера соседних элементов.
Для более широкой поддержки браузеров есть полифилл .

Вот как это может работать в компоненте пользовательского интерфейса:

function redrawGraph(container, { width, height }) {
  d3
    .select(container)
    .select('svg')
    .attr('height', height)
    .attr('width', width)
    .select('rect')
    .attr('height', height)
    .attr('width', width);
}

// Setup observer in constructor
const resizeObserver = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    // on resize logic specific to this component
    redrawGraph(entry.target, entry.contentRect);
  }
})

// Observe the container
const container = document.querySelector('.graph-container');
resizeObserver.observe(container)
.graph-container {
  height: 75vh;
  width: 75vw;
}

.graph-container svg rect {
  fill: gold;
  stroke: steelblue;
  stroke-width: 3px;
}
<script src="https://unpkg.com/resize-observer-polyfill@1.5.1/dist/ResizeObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<figure class="graph-container">
  <svg width="100" height="100">
    <rect x="0" y="0" width="100" height="100" />
  </svg>
</figure>

// unobserve in component destroy method
this.resizeObserver.disconnect()
Анбу Агарвал
источник