如何使用汇总为 d3 堆积条形图准备数据

How to prepare data for d3 stacked barchart using rollup

我是 JavaScript 和 D3.js 的新手,我正在尝试创建一个 Angular 应用程序,其中包含不同测试结果数量的堆叠条形图可视化 'OK' ,每天的 'NOK' 和 'Aborted' 作为 y 轴中的堆积条,日期作为 x 轴。

我的 data.csv 看起来像这样:

Date;Result
20-05-2021 17:54:02;Aborted
20-05-2021 17:55:24;OK
21-05-2021 21:48:45;NOK
22-05-2021 17:55:24;OK
22-05-2021 17:54:02;Aborted
22-05-2021 17:55:24;OK

因为我需要每天计算结果,所以我首先使用 timeParse 将日期解析为正确的格式,然后我使用 timeDay.floor 方法去掉时间:

let jsonObj = await d3.dsv(";", "/assets/data.csv", function (d) {

  let time = timeParse("%d-%m-%Y %-H:%M:%S")(d['Date'])
  let date = timeDay.floor(new Date(time));

  return {
    Date: date,
    Result: d.Result
  };
})

如果我没理解错的话,这给了我一个包含 < date | 的数组。 string > 每个测试结果。

现在总结一下我使用汇总的相同天数的测试结果计数:

let data_count = rollup(jsonObj, v => v.length, d => d.Date, d => d.Result)

现在我有一个嵌套的地图,其中日期(每天只有一个)作为键,值作为测试结果,每个值都带有当天的总和。

我发现了几个不需要使用汇总方法就可以继续的示例,例如 并尝试使其适应我的地图:

let processed_data = data_count.map( d => {
  let y0 = 0;
  let total = 0;
  return {
    date: d.key,
    //call second mapping function on every test-result
    values: (d.valules).map( d => {
      let return_object = {
        result: d.key,
        count: d.values,
        y0: y0,
        y1: y0 + d.values
      };
    //calculate the updated y0 for each new test-result on a given date
    y0 = y0 + d.values;
    //add the total for a given test-result to the sum total for that test-result
    total = total + d.values;
    return return_object;  
    }),
    total: total
  };
});

但我收到一个错误:

Property 'map' does not exist on type 'InternMap<Date, InternMap<string, number>>'.ts(2339)

我知道地图功能不能用在地图上,我猜。 我还尝试将这部分重写为单独的函数以不使用 map 函数,但它也不起作用。也许我有一些语法错误或其他什么,但我得到:

TypeError: Cannot read property 'key' of undefined

我可能需要对地图的值使用 get() 方法,但不确定如何实现它。

现在我不知道应该如何继续为堆积条形图准备数据。在这个来自 bl.ocks.org 的示例中,CSV 看起来有所不同。我正在考虑以某种方式操纵我的数据以适应该示例的形状:

Date;OK;NOK;Aborted
20-05-2021 17:54:02;1;0;1
21-05-2021 21:48:45;0;1;0
22-05-2021 17:55:24;2;0;1

但我不知道该怎么做。 欢迎任何有关如何准备我的数据的帮助。也许我不应该使用 rollup 方法,但我觉得它非常适合我的需求。

如果我们 运行 认为您最终想要利用一些现有的代码示例,因此需要这样的数据:

Date;OK;NOK;Aborted
20-05-2021 17:54:02;1;0;1
21-05-2021 21:48:45;0;1;0
22-05-2021 17:55:24;2;0;1

有几件事需要考虑:

  1. 您正在将数据从密集型数据转换为稀疏型数据,因为您需要为例如NOK20-05-2021 上,因为原始数据中不存在该数据点。

  2. 您需要 Result 值的不同作为转换数据中的行 header。您可以通过以下方式获得:const columns = [...new Set(data.map(d => d.Result))];

  3. 您发现您不能在 Map object 上使用 Array.protoype.map,因此您只需要考虑其他选项(下面提供两个)迭代 Map objects 例如使用 Map.prototype.entries() or Map.prototype.forEach.

d3.rollup 实现此 re-shaped 数据:

  • d3.rollup 语句包装在 Array.from(...) 中,这会得到 Map.prototype.entries(),您可以将其传递给 reduce 函数。

  • reduce 函数中,您可以访问外部 Map[key, value] 对,其中 value 本身就是一个 Map(由 d3.rollup 嵌套)

  • 然后迭代 columnsResult 的不同)以评估是否需要获取内部 Map 中的值(那几天 Result) 或插入一个 0 因为那天 Result 没有发生(每点 (1))。

    在示例中,这一行:

    resultColumns.map(col => row[col] = innerMap.has(col) ? innerMap.get(col) : 0);

    的意思是:对于一个列header,如果内Maphas那个列header作为key,然后 get 每个键 的值 ,否则为零。

工作示例:

// your data setup
const csv = `Date;Result
20-05-2021 17:54:02;Aborted
20-05-2021 17:55:24;OK
21-05-2021 21:48:45;NOK
22-05-2021 17:55:24;OK
22-05-2021 17:54:02;Aborted
22-05-2021 17:55:24;OK`;

// your data processing
const data = d3.dsvFormat(";").parse(csv, d => {
  const time = d3.timeParse("%d-%m-%Y %-H:%M:%S")(d.Date);
  const date = d3.timeDay.floor(new Date(time));
  return {
    Date: date,
    Result: d.Result
  }
});

// distinct Results for column headers per point (2)
const resultColumns = [...new Set(data.map(d => d.Result))];

// cast nested Maps to array of objects
const data_wide = Array.from( // get the Map.entries()
  d3.rollup(
    data,
    v => v.length,
    d => d.Date,
    d => d.Result
  )
).reduce((accumlator, [dateKey, innerMap]) => {
  // create a 'row' with a Date property
  let row = {Date: dateKey}
  // add further properties to the 'row' based on existence of keys in the inner Map per point (1)
  resultColumns.map(col => row[col] = innerMap.has(col) ? innerMap.get(col) : 0);
  // store and return the accumulated result
  accumlator.push(row);
  return accumlator;
}, []);

console.log(data_wide);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

如果您更喜欢一种更程序化(并且可能更具可读性)的方式来获得相同的结果,您可以避免使用 Array.from(...) 并使用 MapforEach method (which is different from Array.prototype.forEach) 进行迭代外部 Map 然后执行类似的操作来评估是否需要创建零数据点:

// your data setup
const csv = `Date;Result
20-05-2021 17:54:02;Aborted
20-05-2021 17:55:24;OK
21-05-2021 21:48:45;NOK
22-05-2021 17:55:24;OK
22-05-2021 17:54:02;Aborted
22-05-2021 17:55:24;OK`;

// your data processing
const data = d3.dsvFormat(";").parse(csv, d => {
  const time = d3.timeParse("%d-%m-%Y %-H:%M:%S")(d.Date);
  const date = d3.timeDay.floor(new Date(time));
  return {
    Date: date,
    Result: d.Result
  }
});

// distinct Results for column headers per point (2)
const resultColumns = [...new Set(data.map(d => d.Result))];

// rollup the data
const rolled = d3.rollup(
  data,
  v => v.length,
  d => d.Date,
  d => d.Result
);

// create an output array
const data_wide = [];

// populate the output array
rolled.forEach((innerMap, dateKey) => {
  // create a 'row' with the date property
  const row = {Date: dateKey}
  // add further properties to the 'row' based on existence of keys in the inner Map per point (1)
  resultColumns.map(col => row[col] = innerMap.has(col) ? innerMap.get(col) : 0);
  // store the result
  data_wide.push(row);
});

console.log(data_wide);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>