Как создать ‘facetplot’ в d3.js ?

#javascript #d3.js #data-visualization

#javascript #d3.js #визуализация данных

Вопрос:

Допустим, у меня есть набор данных разных спортсменов, которые выполняют один и тот же тест в разные дни. Каждый день у них будет несколько испытаний / запусков. Я хочу визуализировать развитие каждого спортсмена в эти дни, используя d3.js но я изо всех сил пытаюсь понять, как я могу выполнить эту задачу.

Используя seaborn в Python или ggplot2 в R, я бы использовал facetplot, где каждый день является аспектом. В рамках этих аспектов у меня были бы испытания по оси x и производительность по оси I. Но как я могу сделать это в d3.js ?

D3.group и group позволяют мне группировать набор данных по спортсменам, и я понимаю, как я могу перебирать значения каждого спортсмена. Но я не понимаю, как я могу перейти отсюда к фактическому созданию фасетной диаграммы в d3.js .

Я пытался найти соответствующие учебные пособия по observable, но без особой удачи. Одна интересная и связанная с этим визуализация — [Козыри в гольфах bt Босток]. Еще один — это диаграмма рассеяния.

Не мог бы кто-нибудь, пожалуйста, помочь мне в правильном направлении? Я создал простой набор данных и диаграмму рассеяния, которые могут служить в качестве начального

 const data = d3.range(10).map(i => ({
        bib: Math.floor(i / 5)   1,
        ratio: -1   Math.random() * 5,
        run: [1, 2, 3, 4, 5][i % 5],
        run: [1, 2, 3, 4, 5][i % 5],
        name: ['GIRL1', 'GIRL2', 'GIRL3', 'GIRL4'][Math.floor(i / 5)]
        }));

        const width = 250;
        const height = 150;
       
        const svg = d3.select('svg')
            .attr('width', width)
            .attr('height', height)

        const margin = {
            top: 20,
            right: 20,
            bottom: 20,
            left: 50
        }

        const innerWidth = width - margin.left - margin.right;
        const innerHeight = height - margin.top - margin.bottom;

        const xScale = d3.scaleLinear()
            .domain(d3.extent(data, d => d.run))
            .range([0, innerWidth])
     
        const yScale = d3.scaleLinear()
            .domain(d3.extent(data, d => d.ratio))
            .range([0, innerHeight])

        const g = svg.append('g')
            .attr('transform', `translate(${margin.left},${margin.top})`)

        g.selectAll('circle')
            .data(data)
            .join('circle')
            .attr('r', 3)
            .attr('cx', d => xScale(d.run))
            .attr('cy', d => yScale(d.ratio))

            g.append("g")
                .call(d3.axisBottom(xScale))
                .attr('transform', `translate(0,${innerHeight})`);;
            
            g.append("g")
                .call(d3.axisLeft(yScale)) 
 <script src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script>
<svg></svg> 

Пример построения графика из R:

введите описание изображения здесь

Ответ №1:

Настройка SVG

Сначала давайте установим соглашение о том, как мы будем размещать и структурировать страницу. Только с одним рядом диаграмм это немного проще.

введите описание изображения здесь

Нам нужно определить переменные на изображении выше. Тогда мы можем получить ширину и высоту каждого графика:

 const height = 500;
const width = 500;
const margin = {
   top: 50,
   left: 50,
   right: 50,
   bottom: 50
}
const padding = 10; // labelled `pad` in image due to space constraints

const plotWidth = (width-padding)/numberOfPlots - padding;
const plotHeight = height-padding*2;
 

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

Ниже я использую a g для удержания области, ограниченной полями, чтобы упростить последующее позиционирование.

Теперь у нас также есть два разных масштаба, и вы уже сделали это в своем коде, но теперь мы можем определить диапазоны с plotWidth помощью и plotHeight с использованием приведенного выше соглашения о интервалах.

Построение графика данных

Теперь мы готовы к созданию графиков. Для этого мы создадим вложенный набор данных: мы хотим сгруппировать данные по графику, и, как вы заметили, мы можем использовать d3.group:

  const grouped = d3.group(data,d=>d.bib);
 

Для каждой группировки мы создадим g позицию и, используя перевод, основанный на приведенной выше структуре:

  const plots = g.selectAll(".plot")
  .data(grouped)
  .enter()
  .append("g")
  .attr("transform", function(d,i) {
     return "translate(" [i*(padding plotWidth) padding,padding] ")";
   })
  
 

И мы готовы к построению графика с использованием вложенных данных:

    plots.selectAll(null)
     .data(d=>d[1])
     .enter()
     .append("circle")
     ... // and so forth.
 

Приведенный ниже фрагмент используется d=>d[1] как место, где находится массив элементов, принадлежащих этой группе. d[0] является идентификатором группы. Если бы мой массив данных был массивом массивов, я бы просто использовал d=>d; .

Теперь мы могли бы добавить ось x на каждый график:

  plots.append("g")
   .attr("transform","translate(" [0,plotHeight] ")")
   .call(d3.axisBottom(x));
 

И ось y:

  g.append("g")
   .attr("transform","translate(" [0,padding] ")");
   .call(d3.axisLeft(y));
 

Я не добавил родительскую ось x, в зависимости от вашего использования вы можете пометить ее или нет. Я также заметил, что я сгруппировал ваши данные по bib, а не по run, но принципы остаются прежними: группируйте данные, вводите и размещайте родительские графики, затем добавляйте точки данных к родительским.

Пример

 // Data and manipluation:
const data = d3.range(15).map(i => ({
        bib: Math.floor(i / 5)   1,
        ratio: -1   Math.random() * 5,
        run: [1, 2, 3, 4, 5][i % 5],
        run: [1, 2, 3, 4, 5][i % 5],
        name: ['GIRL1', 'GIRL2', 'GIRL3', 'GIRL4'][Math.floor(i / 5)]
        }));
        
const grouped = d3.group(data,d=>d.bib);


// Dimensions:
const height = 500;
const width = 500;
const margin = {
       top: 10,
       left: 50,
       right: 50,
       bottom: 50
    }
const padding = 30; // labelled `pad` in image due to space constraints

const plotWidth = (width-padding)/grouped.size - padding;
const plotHeight = height-padding*2;

const svg = d3.select("body")
  .append("svg")
  .attr("width", margin.left width margin.right)
  .attr("height", margin.top height margin.bottom);
  
const g = svg.append("g")
  .attr("transform","translate(" [margin.left,margin.top] ")");

//Scales:
 const xScale = d3.scaleLinear()
   .domain(d3.extent(data, d => d.run))
   .range([0, plotWidth]);
     
const yScale = d3.scaleLinear()
   .domain(d3.extent(data, d => d.ratio))
   .range([plotHeight, 0]);
   
// Place plots:
const plots = g.selectAll(null)
  .data(grouped)
  .enter()
  .append("g")
  .attr("transform", function(d,i) {
     return "translate(" [i*(padding plotWidth) padding,padding] ")";
   })
   
//Optional plot background:
plots.append("rect")
  .attr("width",plotWidth)
  .attr("height",plotHeight)
  .attr("fill","#ddd");
   

// Plot actual data
plots.selectAll(null)
     .data(d=>d[1])
     .enter()
     .append("circle")
     .attr("r", 4)
     .attr("cy", d=>yScale(d.ratio))
     .attr("cx", d=>xScale(d.run))

// Plot line if needed:
plots.append("path")
  .attr("d", function(d) {
    return d3.line()
               .x(d=>xScale(d.run))
               .y(d=>yScale(d.ratio))
               (d[1])
   })
   .attr("stroke", "#333")
   .attr("stroke-width", 1)
   .attr("fill","none")
   
// Plot names if needed:
plots.append("text")
  .attr("x", plotWidth/2)
  .attr("y", -10)
  .text(function(d) {
    return d[1][0].name;
  })
  .attr("text-anchor","middle");
   
// Plot axes     
 plots.append("g")
   .attr("transform","translate(" [0,plotHeight] ")")
   .call(d3.axisBottom(xScale).ticks(4));


 g.append("g")
   .attr("transform","translate(" [0,padding] ")")
   .call(d3.axisLeft(yScale)) 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.1.0/d3.min.js"></script> 

Что должно привести к:

введите описание изображения здесь

Комментарии:

1. Спасибо, что нашли время для создания этого пошагового руководства. Теперь я действительно понимаю, как это сделать. Однако одно: какова логика, лежащая в основе plots.selectAll(null)? Тот, который я не получил

2. selectAll(null) обеспечивает пустой выбор, так что для каждого элемента в массиве данных вводится элемент. Как plots и выбор пустых g элементов, мы могли бы использовать selectAll("circle") too или любой селектор. Поскольку в g элементах нечего выбирать, у нас всегда будет пустое выделение.

3. Если вам также нужно несколько строк. Как вы можете это сделать? 🙂

Ответ №2:

Ну, во второй раз сегодня @AndrewReid опередил меня, и, конечно, его ответ очень совершенен (мне нравится диаграмма).

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

 <!DOCTYPE html>

<html>
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <style>
       svg {
        font-family: arial
      }
      .tick line {
        stroke: white;
        stroke-opacity: 0.7;
        shape-rendering: crispEdges;
      }
    </style>
  </head>

  <body>
    <svg></svg>
    <script>
      let data = [],
        tests = ['PRETEST', 'TRENING1', 'TRENING2', 'TRENING3', 'POSTTEST'],
        courses = ['COURSE 1', 'COURSE 2', 'COURSE 3', 'STRAIGHT-GLIDING'];

      tests.forEach((i) => {
        courses.forEach((j) => {
          d3.range(5).map((k) => {
            data.push({
              test: i,
              course: j,
              run: k,
              ratio: -1   Math.random() * 5,
            });
          });
        });
      });

      const width = 1100;
      const height = 350;

      const margin = {
        top: 20,
        right: 10,
        bottom: 20,
        left: 30,
      };

      // place wrapper g with margins
      const svg = d3
        .select('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate('   margin.left   ','   margin.top   ')');

      // calculate the outer scale band for each line graph
      const outerXScale = d3
        .scaleBand()
        .domain(tests)
        .range([0, width - margin.left - margin.right]);

      // inner dimensions of chart based on bandwidth of outer scale
      const innerWidth = outerXScale.bandwidth() - margin.left - margin.right;
      const innerHeight = height - margin.top - margin.bottom;

      // g for each inner chart
      const testG = svg
        .selectAll('.outer')
        .data(d3.group(data, (d) => d.test))
        .enter()
        .append('g')
        .attr('class', 'outer')
        .attr('transform', function (d, i) {
          return 'translate('   outerXScale(d[0])   ','   0   ')';
        });

      // some styling
      testG
        .append('rect')
        .attr('width', innerWidth)
        .attr('height', innerHeight)
        .attr('fill', '#f2f2f2');

      testG
        .append('rect')
        .attr('width', innerWidth)
        .attr('height', 17)
        .attr('transform', 'translate('   0   ','   -17   ')')
        .attr('fill', '#e6e6e6');

      // header
      testG
        .append('text')
        .text(function (d) {
          return d[0];
        })
        .attr('text-anchor', 'middle')
        .attr('transform', 'translate('   innerWidth / 2   ','   -2   ')');

      // inner scales
      const innerXScale = d3
        .scaleLinear()
        .domain(d3.extent(data, (d) => d.run))
        .range([0, innerWidth]);

      const innerYScale = d3
        .scaleLinear()
        .domain(d3.extent(data, (d) => d.ratio))
        .range([innerHeight, 0]);

      testG
        .append('g')
        .call(d3.axisBottom(innerXScale).tickSize(-innerHeight))
        .attr('transform', `translate(0,${innerHeight})`);

      testG.append('g').call(d3.axisLeft(innerYScale).tickSize(-innerWidth));

      testG
        .selectAll('.line')
        .data(function (d) {
          return d3.group(d[1], (d) => d.course);
        })
        .enter()
        .append('path')
        .attr('d', function (d) {
          return d3
            .line()
            .x((d) => innerXScale(d.run))
            .y((d) => innerYScale(d.ratio))(d[1]);
        })
        .attr('fill', 'none')
        .attr('stroke', function (d, i) {
          return d3.schemeCategory10[i];
        })
        .attr('stroke-width', 1);
    </script>
  </body>
</html> 

Комментарии:

1. Ха, упс, увидел ваш комментарий и немного подождал. Думаю, недостаточно. Было бы неплохо, если бы ТАК сказали, кто начал ответ, я бы сэкономил немного времени на этой неделе. С положительной стороны, возможно, я смогу заработать значок спортивного мастерства, если мы продолжим задавать одни и те же вопросы.

2. @AndrewReid, не беспокойся и не извиняйся, если я наступил тебе на пятки. На самом деле мне нравится видеть наши слегка разные подходы.

3. На пальцы не наступают, всегда приятно видеть ваши ответы, и мне также нравится сравнивать наши разные подходы.