Как я могу правильно агрегировать/группировать несколько линейных графиков в один общий график с помощью d3.js, когда значения x не совпадают точно?

У меня есть несколько наборов данных, подобных этому:

const server1 = [
  { date: '2019-06-15T00:22:25.000Z', online: 451 },
  { date: '2019-06-15T01:08:58.000Z', online: 499 }
];

const server2 = [
  { date: '2019-06-15T00:14:18.000Z', online: 599 },
  { date: '2019-06-15T00:58:56.000Z', online: 554 }
];

const server3 = [
  { date: '2019-06-15T00:10:18.000Z', online: 321 },
  { date: '2019-06-15T00:54:56.000Z', online: 300 }
];

Мне удалось нарисовать линейный график для каждого из них, и диаграмма выглядит так: введите здесь описание изображения

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

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

Вот jsfiddle с кодом, который у меня есть


Обновление 1:

Вот функция, которую я собрал вместе с предложениями Coderino Javarino:

const mergeDatasets = (xPropName, yPropName, ...datasets) => {
  const makeScale = dataset => {
    return d3
      .scaleTime()
      .domain(dataset.map(obj => new Date(obj[xPropName])))
      .range(dataset.map(obj => obj[yPropName]));
  };
  const sumOtherDataSets = (scales, object, curDataSetIndex) => {
    let sum = object[yPropName];
    scales.forEach((_scale, scaleIndex) => {
      if (curDataSetIndex !== scaleIndex) {
        sum += scales[scaleIndex](new Date(object[xPropName]));
      }
    });
    return sum;
  };

  // make a scale for each dataset
  const scales = datasets.map(dataset => makeScale(dataset));

  /* make an new array where each datapoint is summed with the interpolated 
        /* counterparts from all other datasets */
  let merged = [];
  datasets.forEach((dataset, curDataSetIndex) => {
    const summedDataset = dataset.map(object => {
      const objCopy = { ...object };
      objCopy[yPropName] = sumOtherDataSets(scales, object, curDataSetIndex);
      return objCopy;
    });
    merged = [].concat(merged, summedDataset);
  });

  // restore the correct order by date
  merged = merged.sort((a, b) => new Date(a[xPropName]) - new Date(b[xPropName]));
  return merged;
};

Затем я могу легко использовать его так:

    const merged = mergeDatasets("date", "online", server1, server2, server3);

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

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

Вот обновленная скрипта, которая также показывает проблему


person christophrus    schedule 18.06.2019    source источник


Ответы (1)


Подход, который я бы выбрал:

  1. извлечь все значения x, где хотя бы в одной строке указано значение.
  2. создайте масштаб для каждой строки, где домен — это все точки x, а диапазон — все точки y
  3. перебрать список x, получить интерполированное значение y в каждой строке и сложить вместе.

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

const server1 = [
  { date: '2019-06-15T00:22:25.000Z', online: 451 },
  { date: '2019-06-15T01:08:58.000Z', online: 499 },
  { date: '2019-06-15T02:25:35.000Z', online: 464 },
  { date: '2019-06-15T13:25:42.000Z', online: 252 },
  { date: '2019-06-15T15:45:24.000Z', online: 247 },
  { date: '2019-06-15T17:02:09.000Z', online: 254 },
  { date: '2019-06-15T20:11:00.000Z', online: 300 },
  { date: '2019-06-15T21:22:33.000Z', online: 296 },
  { date: '2019-06-15T22:24:58.000Z', online: 298 },
  { date: '2019-06-15T22:58:18.000Z', online: 270 }
];

const server2 = [
  { date: '2019-06-15T00:14:18.000Z', online: 599 },
  { date: '2019-06-15T00:58:56.000Z', online: 554 },
  { date: '2019-06-15T02:15:13.000Z', online: 505 },
  { date: '2019-06-15T13:32:19.000Z', online: 221 },
  { date: '2019-06-15T15:19:08.000Z', online: 265 },
  { date: '2019-06-15T16:04:55.000Z', online: 277 },
  { date: '2019-06-15T17:31:16.000Z', online: 275 },
  { date: '2019-06-15T18:41:16.000Z', online: 303 },
  { date: '2019-06-15T20:05:41.000Z', online: 333 },
  { date: '2019-06-15T21:39:44.000Z', online: 288 },
  { date: '2019-06-15T22:46:01.000Z', online: 277 },
  { date: '2019-06-15T23:29:16.000Z', online: 264 }
];

const server3 = [
  { date: '2019-06-15T00:10:18.000Z', online: 321 },
  { date: '2019-06-15T00:54:56.000Z', online: 300 },
  { date: '2019-06-15T02:11:13.000Z', online: 280 },
  { date: '2019-06-15T13:28:19.000Z', online: 110 },
  { date: '2019-06-15T15:15:08.000Z', online: 130 },
  { date: '2019-06-15T16:01:55.000Z', online: 133 },
  { date: '2019-06-15T17:25:16.000Z', online: 140 },
  { date: '2019-06-15T18:37:16.000Z', online: 172 },
  { date: '2019-06-15T20:01:41.000Z', online: 180 },
  { date: '2019-06-15T21:35:44.000Z', online: 201 },
  { date: '2019-06-15T22:41:01.000Z', online: 210 },
  { date: '2019-06-15T23:23:16.000Z', online: 197 }
];

var x_list = [].concat(server1, server2, server3)
  .map(d => new Date(d.date))
  .sort(d3.ascending);
  
var servers_scales = [server1, server2, server3].map(s => {
  return d3.scaleLinear()
    .domain(s.map(d => new Date(d.date)))
    .range(s.map(d => d.online));
});

var combinedData = x_list.map(x => {
  var val = 0;
  for (var i = 0; i < servers_scales.length; i++)
    val += servers_scales[i](x);
  return {
    date: x,
    online: val
  };
});


const combined = [].concat(server1, server2, server3);

const margin = {top: 50, right: 50, bottom: 50, left: 50};
const width = window.innerWidth - margin.left - margin.right;
const height = window.innerHeight - margin.top - margin.bottom;

const dateMin = d3.min(combined, d => new Date(d.date));
const dateMax = d3.max(combined, d => new Date(d.date));
const onlineMin = d3.min(combined, d => d.online);
const onlineMax = d3.max(combined, d => d.online);

const xScale = d3
  .scaleTime()
  .domain([dateMin, dateMax])
  .range([0, width]);

const yScale = d3
  .scaleLinear()
  .domain([0, onlineMax * 3])
  .range([height, 0]);
  
const line = d3
  .line()
  .x(d => xScale(new Date(d.date)))
  .y(d => yScale(d.online))
  .curve(d3.curveMonotoneX);
  
const svg = d3
  .select('body')
  .append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
  
svg
  .append('path')
  .datum(server1)
  .attr('class', 'line')
  .attr('fill', 'none')
  .attr('stroke', 'blue')
  .attr('d', line);
  
svg
  .append('path')
  .datum(server2)
  .attr('class', 'line')
  .attr('fill', 'none')
  .attr('stroke', 'red')
  .attr('d', line);
  
svg
  .append('path')
  .datum(server3)
  .attr('class', 'line')
  .attr('fill', 'none')
  .attr('stroke', 'green')
  .attr('d', line);

svg
  .append('path')
  .datum(combinedData)
  .attr('class', 'line')
  .attr('fill', 'none')
  .attr('stroke', 'orange')
  .attr('d', line);

svg
	.append("g")
  .attr("class", "y axis")
  .call(d3.axisLeft(yScale));
  
svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(xScale));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

person Coderino Javarino    schedule 18.06.2019
comment
К сожалению, это запутывается, когда в одном из наборов данных в конце или начале нет реального представления данных: i.ibb.co/KqFd8Dw/messed-up-d3-chart.jpg - person christophrus; 30.06.2019
comment
потому что масштаб линейно экстраполируется из последних двух точек, если вы передаете его значение за пределы его домена. Вам нужно будет добавить дополнительную проверку, если значение находится за пределами домена масштаба, и вернуть ноль, если оно - person Coderino Javarino; 30.06.2019