Каков лучший способ сделать макет визуализации d3.js отзывчивым?

224

Предположим, у меня есть скрипт гистограммы, который строит графику SVG 960 500. Как сделать так, чтобы изменение размера графической ширины и высоты было динамичным?

<script> 

var n = 10000, // number of trials
    m = 10,    // number of random variables
    data = [];

// Generate an Irwin-Hall distribution.
for (var i = 0; i < n; i++) {
  for (var s = 0, j = 0; j < m; j++) {
    s += Math.random();
  }
  data.push(s);
}

var histogram = d3.layout.histogram()
    (data);

var width = 960,
    height = 500;

var x = d3.scale.ordinal()
    .domain(histogram.map(function(d) { return d.x; }))
    .rangeRoundBands([0, width]);

var y = d3.scale.linear()
    .domain([0, d3.max(histogram.map(function(d) { return d.y; }))])
    .range([0, height]);

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

svg.selectAll("rect")
    .data(histogram)
  .enter().append("rect")
    .attr("width", x.rangeBand())
    .attr("x", function(d) { return x(d.x); })
    .attr("y", function(d) { return height - y(d.y); })
    .attr("height", function(d) { return y(d.y); });

svg.append("line")
    .attr("x1", 0)
    .attr("x2", width)
    .attr("y1", height)
    .attr("y2", height);

</script> 

Полный пример гистограммы: https://gist.github.com/993912

Мэтт Алкок
источник
5
Я нашел способ в этом ответе проще: stackoverflow.com/a/11948988/511203
Майк Горски
Самый простой способ, который я нашел, это сделать ширину и высоту SVG: 100% и применить размеры к контейнеру DOM (например, div) stackoverflow.com/questions/47076163/…
mtx

Ответы:

316

Есть еще один способ сделать это, который не требует перерисовки графика, и включает изменение атрибутов viewBox и preserveAspectRatio для <svg>элемента:

<svg id="chart" width="960" height="500"
  viewBox="0 0 960 500"
  preserveAspectRatio="xMidYMid meet">
</svg>

Обновление 24.11.15 : большинство современных браузеров могут определять соотношение сторон SVG-элементов viewBox, поэтому вам может не потребоваться обновлять размер диаграммы. Если вам нужно поддерживать старые браузеры, вы можете изменить размер вашего элемента, когда размер окна изменится следующим образом:

var aspect = width / height,
    chart = d3.select('#chart');
d3.select(window)
  .on("resize", function() {
    var targetWidth = chart.node().getBoundingClientRect().width;
    chart.attr("width", targetWidth);
    chart.attr("height", targetWidth / aspect);
  });

И содержимое SVG будет масштабироваться автоматически. Вы можете увидеть рабочий пример этого (с некоторыми изменениями) здесь : просто измените размер окна или нижней правой панели, чтобы увидеть, как оно реагирует.

Шон Аллен
источник
1
Мне очень нравится этот подход, Шон, 2 вопроса. 1 работает ли кросс браузер в окне просмотра? 2. В вашем примере круги меняются, но не помещаются в окне. Что-то не так с окном svg или соотношением сторон.
Мэтт Алкок
4
Люблю этот подход, не уверен, что выше работает правильно. Как говорит Мэтт, круги изменяют размер, но расстояние от левого графа до первого круга не изменяется с той же скоростью, что и круги. Я попытался немного поиграть в jsfiddle, но не смог заставить его работать, как и ожидалось, с помощью Google Chrome. С какими браузерами совместимы viewBox и preserveAspectRatio?
Крис Уизерс
9
На самом деле, установка только viewBox и preserveAspectRatio в SVG с высотой ширины, равной 100% от родительского, и родительской, являющейся загрузочной / гибкой сеткой, работает для меня без обработки события resize
Deepu
2
Подтверждая, что Дипу прав. Работает с Bootstrap / flex grid. Спасибо @Deepu.
Майк О'Коннор
3
Примечание: в нескольких руководствах по d3 1.) в html задаются ширина и высота svg и 2.) визуализация выполняется посредством onload вызова функции. Если ширина и высота svg установлены в фактическом html, и вы используете вышеупомянутую технику, изображение не будет масштабируемым.
SumNeuron
34

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

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

d3.select("div#chartId")
   .append("div")
   .classed("svg-container", true) //container class to make it responsive
   .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); 

Код CSS:

.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;
}

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

http://demosthenes.info/blog/744/Make-SVG-Responsive

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

cminatti
источник
1
В моем случае, padding-bottom: 100% сделает мой контейнер дополнительным отступом внизу, поэтому я изменил его на 50%, чтобы удалить его. Использовал ваш код и успешно сделал svg отзывчивым, спасибо.
V-SHY
20

Я решил небольшой вопрос, чтобы решить это.

Общий шаблон решения таков:

  1. Разбейте скрипт на функции вычисления и рисования.
  2. Убедитесь, что функция рисования отрисовывается динамически и управляется переменными ширины и высоты визуализации (лучший способ сделать это - использовать API d3.scale)
  3. Привязать / связать чертеж с ссылочным элементом в разметке. (Я использовал jquery для этого, поэтому импортировал его).
  4. Не забудьте удалить его, если он уже нарисован. Получите размеры из ссылочного элемента, используя jquery.
  5. Привязать / связать функцию рисования с функцией изменения размера окна. Введите debounce (timeout) в эту цепочку, чтобы гарантировать, что мы перерисовываем только после тайм-аута.

Я также добавил минимизированный сценарий d3.js для скорости. Суть здесь: https://gist.github.com/2414111

JQuery ссылка обратный код:

$(reference).empty()
var width = $(reference).width();

Отменить код:

var debounce = function(fn, timeout) 
{
  var timeoutID = -1;
  return function() {
     if (timeoutID > -1) {
        window.clearTimeout(timeoutID);
     }
   timeoutID = window.setTimeout(fn, timeout);
  }
};

var debounced_draw = debounce(function() {
    draw_histogram(div_name, pos_data, neg_data);
  }, 125);

 $(window).resize(debounced_draw);

Наслаждайтесь!

Мэтт Алкок
источник
Это не похоже на работу; если я изменю размер окна html-файла, включенного в исправленную суть, гистограмма все равно будет обрезана по мере уменьшения размера окна ...
Крис Уизерс
1
SVG - векторный формат. Вы можете установить любой виртуальный размер окна просмотра, а затем масштабировать его до реальных размеров. Нет необходимости использовать JS.
Фил Пирожков
4

Без использования ViewBox

Вот пример решения, которое не зависит от использования viewBox :

Ключ в обновлении диапазон от чешуи , которые используются для размещения данных.

Сначала рассчитайте исходное соотношение сторон:

var ratio = width / height;

Затем при каждом изменении размера обновите rangeof xи y:

function resize() {
  x.rangeRoundBands([0, window.innerWidth]);
  y.range([0, window.innerWidth / ratio]);
  svg.attr("height", window.innerHeight);
}

Обратите внимание, что высота основана на ширине и соотношении сторон, поэтому ваши исходные пропорции сохраняются.

Наконец, «перерисовать» диаграмму - обновите любой атрибут, который зависит от одного из xили yмасштабов:

function redraw() {
    rects.attr("width", x.rangeBand())
      .attr("x", function(d) { return x(d.x); })
      .attr("y", function(d) { return y.range()[1] - y(d.y); })
      .attr("height", function(d) { return y(d.y); });
}

Обратите внимание, что при изменении размера rectsвы можете использовать верхнюю границу rangeof y, а не явно использовать высоту:

.attr("y", function(d) { return y.range()[1] - y(d.y); })

Пол Мюррей
источник
3

Если вы используете d3.js через c3.js, решение проблемы с отзывчивостью довольно простое:

var chart = c3.generate({bindTo:"#chart",...});
chart.resize($("#chart").width(),$("#chart").height());

где сгенерированный HTML выглядит так:

<div id="chart">
    <svg>...</svg>
</div>
Vanna
источник
2

Ответ Шона Аллена был великолепен. Но вы не можете делать это каждый раз. Если вы разместите его на vida.io , вы получите автоматический ответ для вашей визуализации svg.

Вы можете получить отзывчивый iframe с помощью этого простого кода:

<div id="vida-embed">
<iframe src="http://embed.vida.io/documents/9Pst6wmB83BgRZXgx" width="auto" height="525" seamless frameBorder="0" scrolling="no"></iframe>
</div>

#vida-embed iframe {
  position: absolute;
  top:0;
  left: 0;
  width: 100%;
  height: 100%;
}

http://jsfiddle.net/dnprock/npxp3v9d/1/

Раскрытие информации: Я строй этой функции в vida.io .

Phuoc Do
источник
2

В случае, если вы используете оболочку d3, например plottable.js , имейте в виду , что самым простым решением может быть добавление прослушивателя событий, а затем вызов функции перерисовки ( redrawв plottable.js). В случае plottable.js это будет работать превосходно (этот подход плохо документирован):

    window.addEventListener("resize", function() {
      table.redraw();
    });
Райли Стил Парсонс
источник
Вероятно, было бы лучше отменить эту функцию, чтобы избежать перерисовки как можно быстрее
Ian
1

Если люди все еще посещают этот вопрос - вот что сработало для меня:

  • Заключите iframe в div и используйте css для добавления отступа, скажем, 40% к этому div (процент в зависимости от желаемого соотношения сторон). Затем установите ширину и высоту самого iframe на 100%.

  • В html-документе, содержащем диаграмму, которая будет загружена в iframe, задайте width равной ширине div, к которому добавляется svg (или ширине тела), и установите соотношение высоты к ширине *.

  • Напишите функцию, которая перезагружает содержимое iframe при изменении размера окна, чтобы адаптировать размер диаграммы, когда люди поворачивают свой телефон.

Пример здесь на моем сайте: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

ОБНОВЛЕНИЕ 30 декабря 2016

Подход, который я описал выше, имеет некоторые недостатки, особенно в том, что он не учитывает высоту любых заголовков и подписей, которые не являются частью svg, созданного D3. С тех пор я столкнулся с тем, что я считаю лучшим подходом:

  • Установите ширину диаграммы D3 равной ширине div, к которому она прикреплена, и используйте форматное соотношение, чтобы соответственно установить ее высоту;
  • Пусть встроенная страница отправит свою высоту и URL на родительскую страницу с помощью HTML5 postMessage;
  • На родительской странице используйте URL-адрес, чтобы определить соответствующий iframe (полезно, если на вашей странице более одного iframe) и обновите его высоту до высоты встроенной страницы.

Пример здесь на моем сайте: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution

dirkmjk
источник
0

Одним из основных принципов объединения данных D3 является то, что оно идемпотентно. Другими словами, если вы многократно оцениваете объединение данных с одними и теми же данными, результат вывода будет одинаковым. Поэтому, если вы правильно отображаете диаграмму, заботясь о выбранных параметрах ввода, обновления и выхода - все, что вам нужно делать при изменении размера, - повторно визуализировать диаграмму полностью.

Есть несколько других вещей, которые вы должны сделать, одна из них - отменить обработчик изменения размера окна, чтобы уменьшить его. Кроме того, вместо того, чтобы жестко кодировать ширину / высоту, это должно быть достигнуто путем измерения содержащего элемента.

В качестве альтернативы, ваша диаграмма отображается с использованием d3fc , который представляет собой набор компонентов D3, которые правильно обрабатывают соединения данных. Он также имеет декартову диаграмму, которая измеряет его, содержащий элемент, облегчающий создание «отзывчивых» диаграмм:

// create some test data
var data = d3.range(50).map(function(d) {
  return {
    x: d / 4,
    y: Math.sin(d / 4),
    z: Math.cos(d / 4) * 0.7
  };
});

var yExtent = fc.extentLinear()
  .accessors([
    function(d) { return d.y; },
    function(d) { return d.z; }
  ])
  .pad([0.4, 0.4])
  .padUnit('domain');

var xExtent = fc.extentLinear()
  .accessors([function(d) { return d.x; }]);

// create a chart
var chart = fc.chartSvgCartesian(
    d3.scaleLinear(),
    d3.scaleLinear())
  .yDomain(yExtent(data))
  .yLabel('Sine / Cosine')
  .yOrient('left')
  .xDomain(xExtent(data))
  .xLabel('Value')
  .chartLabel('Sine/Cosine Line/Area Chart');

// create a pair of series and some gridlines
var sinLine = fc.seriesSvgLine()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.y; })
  .decorate(function(selection) {
    selection.enter()
      .style('stroke', 'purple');
  });

var cosLine = fc.seriesSvgArea()
  .crossValue(function(d) { return d.x; })
  .mainValue(function(d) { return d.z; })
  .decorate(function(selection) {
    selection.enter()
      .style('fill', 'lightgreen')
      .style('fill-opacity', 0.5);
  });

var gridlines = fc.annotationSvgGridline();

// combine using a multi-series
var multi = fc.seriesSvgMulti()
  .series([gridlines, sinLine, cosLine]);

chart.plotArea(multi);

// render
d3.select('#simple-chart')
  .datum(data)
  .call(chart);

Вы можете увидеть это в действии в этом коде:

https://codepen.io/ColinEberhardt/pen/dOBvOy

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

Обратите внимание, что в качестве полного раскрытия я являюсь одним из сопровождающих d3fc.

Coline
источник
Спасибо за актуальность ваших ответов! Я видел последние обновления некоторых ваших постов, и я действительно ценю, что люди пересматривают свой собственный контент! Однако, как предостережение, эта редакция больше не полностью соответствует этому вопросу, потому что вы редактируете в D3 v4 , тогда как OP явно ссылается на v3 , что может запутать будущих читателей. Лично, для моих собственных ответов, я, как правило, добавляю раздел v4 , сохраняя при этом оригинальный раздел v3 . Я думаю, что v4 может стоить своего ответа, добавив взаимную ссылку на оба поста. Но это только мои два цента ...
altocumulus
Это действительно хороший момент! Так легко увлечься и просто сосредоточиться на будущем и на том, что нового. Судя по новейшим вопросам d3, я думаю, что многие все еще используют v3. Это также разочаровывает, насколько тонки некоторые различия! Я пересмотрю свои правки :-)
ColinE
0

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

Вы можете смоделировать это поведение в некоторых старых браузерах, которые не поддерживают его должным образом, например, в IE11, используя <canvas> элемент, поддерживающий его аспект.

Учитывая 960x540, который является аспектом 16: 9:

<div style="position: relative">
  <canvas width="16" height="9" style="width: 100%"></canvas>
  <svg viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; -webkit-tap-highlight-color: transparent;">
  </svg>
</div>
Dominic
источник
0

Здесь много сложных ответов.

В основном все , что вам нужно сделать , это канавы widthи heightатрибуты в пользу viewBoxатрибута:

width = 500;
height = 500;

const svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

Если у вас есть поля, вы можете просто добавить их туда в ширину / высоту, а затем просто добавить g и преобразовать, как обычно.

Джаред Уилбер
источник
-1

Вы также можете использовать загрузчик 3, чтобы адаптировать размер визуализации. Например, мы можем настроить HTML- код следующим образом:

<div class="container>
<div class="row">

<div class='col-sm-6 col-md-4' id="month-view" style="height:345px;">
<div id ="responsivetext">Something to write</div>
</div>

</div>
</div>

Я установил фиксированную высоту из-за своих потребностей, но вы также можете оставить размер авто. «Col-sm-6 col-md-4» делает div реагирующим на разные устройства. Вы можете узнать больше на http://getbootstrap.com/css/#grid-example-basic

Мы можем получить доступ к графику с помощью идентификатора месяца .

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

var width = document.getElementById('month-view').offsetWidth;

var height = document.getElementById('month-view').offsetHeight - document.getElementById('responsivetext2').offsetHeight;

Ширина устанавливается путем получения ширины div с идентификатором month-view.

Высота в моем случае не должна включать всю площадь. У меня также есть текст над панелью, поэтому мне нужно рассчитать и эту область. Вот почему я идентифицировал область текста с помощью идентификатора responseivetext. Для расчета допустимой высоты панели я вычел высоту текста из высоты элемента div.

Это позволяет вам иметь панель, которая будет принимать все различные размеры экрана / деления. Возможно, это не лучший способ сделать это, но он, безусловно, работает для нужд моего проекта.

arlindmusliu
источник
1
Привет, я попробовал это, но содержание svg не изменяется, как календарь или графики.