Qual è il modo migliore per rendere reattivo un layout di visualizzazione d3.js?


224

Supponiamo che io abbia uno script istogramma che costruisce un grafico svg 960 500. Come posso renderlo reattivo, così come ridimensionare le larghezze grafiche e le altezze sono dinamiche?

<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> 

L'istogramma completo dell'esempio è: https://gist.github.com/993912


5
Ho trovato il metodo in questa risposta più semplice: stackoverflow.com/a/11948988/511203
Mike Gorski,

il modo più semplice ho trovato è quello di rendere larghezza e altezza SVG: 100% e applicare dimensionamento sulla DOM contenitore (come div) stackoverflow.com/questions/47076163/...
MTX

Risposte:


316

C'è un altro modo per fare ciò che non richiede ridisegnare il grafico, e comporta la modifica del viewBox e preserveAspectRatio attributi sul <svg>elemento:

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

Aggiornamento 24/11/15 : la maggior parte dei browser moderni può dedurre le proporzioni degli elementi SVG da viewBox, quindi potrebbe non essere necessario mantenere aggiornate le dimensioni del grafico. Se devi supportare browser meno recenti, puoi ridimensionare l'elemento quando la finestra si ridimensiona in questo modo:

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

E i contenuti svg verranno ridimensionati automaticamente. Puoi vedere un esempio funzionante di questo (con alcune modifiche) qui : basta ridimensionare la finestra o il riquadro in basso a destra per vedere come reagisce.


1
Mi piace molto questo approccio shawn, 2 domande. 1 la viewbox funziona su più browser? 2. Nel tuo esempio i cerchi si ridimensionano ma non si adattano alla finestra. C'è qualcosa che non va nella finestra di visualizzazione svg o nelle proporzioni.
Matt Alcock,

4
Adoro questo approccio, non sono sicuro che quanto sopra funzioni correttamente. Come dice Matt, i cerchi si ridimensionano ma la distanza dalla sinistra del grafico al primo cerchio non si ridimensiona alla stessa velocità dei cerchi. Ho provato a giocherellare un po 'in jsfiddle ma non sono riuscito a farlo funzionare come previsto usando Google Chrome. Con quali browser sono compatibili viewBox e preservAspectRatio?
Chris Withers,

9
In realtà impostare solo viewBox e preservare Aspectio su un file SVG con altezza della larghezza impostata al 100% del padre e padre essendo una griglia bootstrap / flessibile funziona per me senza gestire l'evento di ridimensionamento
Deepu

2
Confermando che Deepu è corretto. Funziona con Bootstrap / griglia flessibile. Grazie @Deepu.
Mike O'Connor,

3
Nota: in diversi tutorial d3 1.) nell'html vengono impostate la larghezza e l'altezza svg e 2.) la visualizzazione viene effettuata tramite una onload chiamata di funzione. Se la larghezza e l'altezza svg sono impostate nell'html effettivo e si utilizza la tecnica sopra, l'immagine non sarà scalabile.
SumNeuron

34

Cerca 'SVG reattivo' è abbastanza semplice rendere reattivo un SVG e non devi più preoccuparti delle dimensioni.

Ecco come l'ho fatto:

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); 

Il codice 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;
}

Ulteriori informazioni / tutorial:

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

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


1
Nel mio caso, imbottitura inferiore: il 100% renderà il mio contenitore con imbottitura extra in basso, quindi l'ho cambiato al 50% per rimuoverlo. Usa il tuo codice e rendi lo svg reattivo, grazie.
V-SHY,

20

Ho scritto un piccolo riassunto per risolvere questo problema.

Il modello di soluzione generale è questo:

  1. Suddividere lo script in funzioni di calcolo e disegno.
  2. Assicurati che la funzione di disegno si disegna in modo dinamico e sia guidata dalle variabili di larghezza e altezza della visualizzazione (Il modo migliore per farlo è usare l'API d3.scale)
  3. Associa / incatena il disegno a un elemento di riferimento nel markup. (Ho usato jquery per questo, quindi l'ho importato).
  4. Ricorda di rimuoverlo se è già disegnato. Ottieni le dimensioni dall'elemento di riferimento usando jquery.
  5. Associa / incatena la funzione di disegno alla funzione di ridimensionamento della finestra. Introdurre un rinvio (timeout) a questa catena per garantire che ridisegniamo solo dopo un timeout.

Ho anche aggiunto lo script d3.js minimizzato per la velocità. L'essenza è qui: https://gist.github.com/2414111

codice back di riferimento jquery:

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

Codice di rimbalzo:

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);

Godere!


Questo non sembra funzionare; se ridimensiono la finestra del file html incluso nella sintesi corretta, l'istogramma viene comunque tagliato via via che la finestra diventa più piccola ...
Chris Withers

1
SVG è un formato vettoriale. È possibile impostare qualsiasi dimensione di viewbox virtuale e quindi ridimensionarla su dimensioni reali. Non è necessario utilizzare JS.
phil pirozhkov,

4

Senza usare ViewBox

Ecco un esempio di una soluzione che non si basa sull'uso di un viewBox:

La chiave sta nell'aggiornamento dell'intervallo delle scale utilizzate per posizionare i dati.

Innanzitutto, calcola le proporzioni originali:

var ratio = width / height;

Quindi, ad ogni ridimensionamento, aggiorna rangedi xe y:

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

Si noti che l'altezza si basa sulla larghezza e le proporzioni, in modo da mantenere le proporzioni originali.

Infine, "ridisegna" il grafico: aggiorna qualsiasi attributo che dipende da una delle scale xo 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); });
}

Si noti che nel ridimensionamento rectsè possibile utilizzare il limite superiore di rangeof y, anziché utilizzare esplicitamente l'altezza:

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


3

Se si utilizza d3.js tramite c3.js, la soluzione al problema della reattività è piuttosto semplice:

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

dove appare il codice HTML generato:

<div id="chart">
    <svg>...</svg>
</div>

2

La risposta di Shawn Allen è stata grandiosa. Ma potresti non voler farlo ogni volta. Se lo si ospita su vida.io , si ottiene una risposta automatica per la visualizzazione svg.

Puoi ottenere iframe reattivo con questo semplice codice di incorporamento:

<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/

Disclosure: costruisco questa funzione in vida.io .


2

Nel caso in cui si stia utilizzando un wrapper d3 come plottable.js , tenere presente che la soluzione più semplice potrebbe essere l' aggiunta di un listener di eventi e quindi la chiamata di una funzione di ridisegno ( redrawin plottable.js). Nel caso di plottable.js funzionerà in modo eccellente (questo approccio è scarsamente documentato):

    window.addEventListener("resize", function() {
      table.redraw();
    });

Probabilmente sarebbe meglio rimandare questa funzione per evitare di sparare il ridisegno il più velocemente possibile
Ian

1

Se le persone continuano a visitare questa domanda, ecco cosa ha funzionato per me:

  • Racchiudi l'iframe in un div e usa css per aggiungere una spaziatura, diciamo, del 40% a quel div (la percentuale dipende dalle proporzioni che desideri). Quindi imposta sia la larghezza che l'altezza dell'iframe stesso al 100%.

  • Nel documento html contenente il grafico da caricare nell'iframe, imposta la larghezza sulla larghezza del div a cui viene aggiunto lo svg (o sulla larghezza del corpo) e imposta le proporzioni altezza-larghezza *.

  • Scrivi una funzione che ricarica il contenuto di iframe al ridimensionamento della finestra, in modo da adattare la dimensione del grafico quando le persone ruotano il telefono.

Esempio qui sul mio sito Web: http://dirkmjk.nl/en/2016/05/embedding-d3js-charts-responsive-website

AGGIORNAMENTO 30 dic 2016

L'approccio che ho descritto sopra presenta alcuni inconvenienti, in particolare che non tiene conto dell'altezza di qualsiasi titolo e didascalia che non fanno parte dello svg creato da D3. Da allora mi sono imbattuto in quello che penso sia un approccio migliore:

  • Imposta la larghezza del grafico D3 sulla larghezza del div a cui è attaccato e usa le proporzioni per impostarne l'altezza di conseguenza;
  • Chiedi alla pagina incorporata di inviare l'altezza e l'URL alla pagina principale utilizzando postMessage di HTML5;
  • Nella pagina principale, utilizza l'URL per identificare l'iframe corrispondente (utile se hai più di un iframe nella tua pagina) e aggiorna la sua altezza all'altezza della pagina incorporata.

Esempio qui sul mio sito Web: http://dirkmjk.nl/en/2016/12/embedding-d3js-charts-responsive-website-better-solution


0

Uno dei principi di base del join di dati D3 è che è idempotente. In altre parole, se si valuta ripetutamente un'unione di dati con gli stessi dati, l'output di rendering è lo stesso. Pertanto, finché si esegue correttamente il rendering del grafico, avendo cura di inserire, aggiornare e uscire dalle selezioni - tutto ciò che è necessario fare quando le dimensioni cambiano, è nuovamente il rendering del grafico nella sua interezza.

Ci sono un paio di altre cose che dovresti fare, una è rimbalzare il gestore di ridimensionamento della finestra per rallentare. Inoltre, anziché larghezze / altezze di codifica rigida, ciò dovrebbe essere ottenuto misurando l'elemento di contenimento.

In alternativa, ecco il tuo grafico reso usando d3fc , che è un insieme di componenti D3 che gestiscono correttamente i join di dati. Ha anche un grafico cartesiano che lo misura contenente un elemento che semplifica la creazione di grafici "reattivi":

// 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);

Puoi vederlo in azione in questo codice:

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

dove è possibile ridimensionare la finestra e verificare che il grafico sia correttamente ridistribuito.

Si prega di notare che, come divulgazione completa, sono uno dei manutentori di d3fc.


Grazie per aver aggiornato le tue risposte! Ho visto i recenti aggiornamenti di alcuni dei tuoi post e apprezzo molto le persone che rivisitano i propri contenuti! Tuttavia, come avvertimento, questa revisione non si adatta più completamente alla domanda, poiché si modifica in D3 v4 , mentre OP si riferisce chiaramente alla v3 , che potrebbe confondere i lettori futuri. Personalmente, per le mie risposte, tendo ad aggiungere una sezione v4 mantenendo comunque la sezione v3 originale . Penso che v4 possa valere la pena di rispondere da solo aggiungendo un riferimento reciproco ad entrambi i post. Ma sono solo i miei due centesimi ...
Altocumulus,

Questo è davvero un buon punto! È così facile lasciarsi trasportare e concentrarsi solo sul futuro e sulle novità. A giudicare dalle più recenti domande d3, penso che molte persone stiano ancora usando la v3. È anche frustrante quanto siano sottili alcune differenze!
Rivisiterò le

0

Eviterei di ridimensionare / spuntare soluzioni come la peste poiché sono inefficienti e possono causare problemi nella tua app (ad esempio una descrizione dei comandi ricalcola la posizione che dovrebbe apparire sul ridimensionamento della finestra, quindi un attimo dopo anche il tuo grafico si ridimensiona e la pagina riappare layout e ora il tooltip è di nuovo sbagliato).

È possibile simulare questo comportamento in alcuni browser meno recenti che non lo supportano correttamente come anche IE11 utilizzando un <canvas>elemento che ne mantiene l'aspetto.

Dato 960x540 che è un aspetto di 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>

0

Molte risposte complesse qui.

Fondamentalmente tutto ciò che devi fare è abbandonare gli attributi widthe heighta favore viewBoxdell'attributo:

width = 500;
height = 500;

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

Se hai dei margini, puoi semplicemente aggiungerli lì in larghezza / altezza, quindi aggiungere gsuccessivamente e trasformarlo come faresti normalmente.


-1

Puoi anche utilizzare bootstrap 3 per adattare le dimensioni di una visualizzazione. Ad esempio, possiamo impostare il codice HTML come:

<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>

Ho impostato un'altezza fissa a causa delle mie esigenze, ma puoi anche lasciare la dimensione automatica. "Col-sm-6 col-md-4" rende il div reattivo per diversi dispositivi. Puoi saperne di più su http://getbootstrap.com/css/#grid-example-basic

Possiamo accedere al grafico con l'aiuto della visualizzazione mensile ID .

Non entrerò nei dettagli del codice d3, inserirò solo la parte necessaria per adattarsi alle diverse dimensioni dello schermo.

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

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

La larghezza viene impostata ottenendo la larghezza del div con la visualizzazione mensile ID.

L'altezza nel mio caso non dovrebbe includere l'intera area. Ho anche del testo sopra la barra, quindi devo calcolare anche quell'area. Ecco perché ho identificato l'area del testo con l'id responsivetext. Per calcolare l'altezza consentita della barra, ho sottratto l'altezza del testo dall'altezza del div.

Ciò ti consente di avere una barra che adotterà tutte le diverse dimensioni dello schermo / div. Potrebbe non essere il modo migliore per farlo, ma sicuramente funziona per le esigenze del mio progetto.


1
Ciao, ci ho provato, ma il contenuto svg non viene ridimensionato, come il calendario o i grafici.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.