如何使用D3将JSON数据创建成折线图

6
我正在使用D3.js和react-hooks创建图表,因此我尝试通过搜索来创建一个Line图表。
  • 但是我正在使用的图表使用示例数据,在我的情况下是JSON数据。
  • 我还使用resize-observer-polyfill库使图表具有响应性。
  • 现在我正在努力将其与JSON数据实现,以便使用动态数据呈现它。

我所做的

const svgRef = useRef();
const wrapperRef = useRef();
const dimensions = useResizeObserver(wrapperRef); // for responsive

// will be called initially and on every data change
useEffect(() => {
    const svg = select(svgRef.current);
    const { width, height } =
        dimensions || wrapperRef.current.getBoundingClientRect();

    // scales + line generator
    const xScale = scaleLinear()
        .domain([0, data.length - 1]) // here I need to pass the data
        .range([0, width]);

    const yScale = scaleLinear()
        .domain([0, max(data)])
        .range([height, 0]);

    const lineGenerator = line()
        .x((d, index) => xScale(index))
        .y((d) => yScale(d))
        .curve(curveCardinal);

    // render the line
    svg
        .selectAll('.myLine')
        .data([data])
        .join('path')
        .attr('class', 'myLine')
        .attr('stroke', 'black')
        .attr('fill', 'none')
        .attr('d', lineGenerator);

    svg
        .selectAll('.myDot')
        .data(data)
        .join('circle')
        .attr('class', 'myDot')
        .attr('stroke', 'black')
        .attr('r', (value, index) => 4)
        .attr('fill', (value, index) => 'red')
        .attr('cx', (value, index) => xScale(index))
        .attr('cy', yScale);

    // axes
    const xAxis = axisBottom(xScale);
    svg
        .select('.x-axis')
        .attr('transform', `translate(0, ${height})`)
        .call(xAxis);

    const yAxis = axisLeft(yScale);
    svg.select('.y-axis').call(yAxis);
}, [data, dimensions]);

    <React.Fragment>
        <div ref={wrapperRef} style={{ marginBottom: '2rem' }}>
            <svg ref={svgRef}>
                <g className="x-axis" />
                <g className="y-axis" />
            </svg>
        </div>
    </React.Fragment>

我无法传递数据,以下是我的数据

[
  { anno: 2014, consumo: 300, color: "#ff99e6" },
  { anno: 2015, consumo: 290, color: "blue" },
  { anno: 2016, consumo: 295, color: "green" },
  { anno: 2017, consumo: 287, color: "yellow" },
  { anno: 2018, consumo: 282, color: "red" },
  { anno: 2019, consumo: 195, color: "white" }
]

我的数据中每个数据都有颜色,我想在生成的每个点中显示它。

线状图的工作代码沙盒

同样地,我尝试制作柱状图,效果很好

当我们调整窗口大小时,我对标签进行了一些动态渲染,标签会自动调整。

这是完全可行的柱状图,我正在尝试将其实现为线状图

我还注释了我正在进行操作的行。

编辑/更新

我一直在遵循@MGO的答案,它对我有很大帮助,但我仍然面临着对齐标签和填充颜色到点的问题。

as you can see the labels are overlapping to each other

实际上,由于文字大小的原因,它很明显会重叠,但为了克服这一点,在条形图中我使用了以下代码

 const tickWidth = 40;
const width = xScaleLabels.range()[1];
const tickN = Math.floor(width / tickWidth);
const keepEveryNth = Math.ceil(xScaleLabels.domain().length / tickN);

const xScaleLabelDomain = xScaleLabels
  .domain()
  .filter((_, i) => i % keepEveryNth === 0);
xScaleLabels.domain(xScaleLabelDomain);

当设备尺寸较小时,它将过滤一些标签并且不会显示这些标签。同时我使用以下代码来添加颜色。
.attr("fill", ({ color }) => color) 

但它没有显示任何颜色,而是默认显示黑色。
我有一些数据要显示为标签,例如2021年7月9日11:18:28,但我只想显示时间,所以我在我的条形图代码中所做的如下。
const xScaleLabels = scaleBand()
        .domain(
            data.map(
                ({ sensorValueAddedTime }) => sensorValueAddedTime.split(' ')[2]  // this I am using to show only time
            )
        )
        .range([0, dimensions.width])
        .padding(padding);

我试图使用折线图实现相同的功能,简单来说,我不希望将其更改为任何时间和全部时间。

这是第二个数据,所以基本上我得到的答案只适用于第一个数据,而不是第二个数据,如果数据以任何这些格式出现,我希望它是动态的,我想要显示sensorValueAddedTime在x轴上,sensorValue在y轴上。

我已经添加了我的条形图完整工作代码,我希望使用相同的方法制作折线图。

    [
    {
      sensorValue: 32,
      sensorValueAddedTime: "July.9.2021 10:56:22",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 32,
      sensorValueAddedTime: "July.9.2021 10:56:23",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 35,
      sensorValueAddedTime: "July.9.2021 11:17:51",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 35,
      sensorValueAddedTime: "July.9.2021 11:17:52",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 36,
      sensorValueAddedTime: "July.9.2021 11:18:08",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 36,
      sensorValueAddedTime: "July.9.2021 11:18:09",
      color_code: null,
      condition: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 38,
      sensorValueAddedTime: "July.9.2021 11:18:27",
      condition: null,
      color_code: null,
      __typename: "sensorData"
    },
    {
      sensorValue: 38,
      sensorValueAddedTime: "July.9.2021 11:18:28",
      condition: null,
      color_code: null,
      __typename: "sensorData"
    }
  ]

请澄清一下,您是想用数组 {anno: 2014, consumo: 300, color: "#ff99e6" } 生成折线图,还是使用 Codesandbox 中的数组?我已经根据 Codesandbox 中提供的数据回答了您的问题。这符合您的要求吗? - MGO
1个回答

6
在Muri的D3 with React Hooks视频系列中的原始示例中,data是一个平面的单维数组:[10, 25, 30, 40, 25, 60]
在您提供的新数组(data1)中,我们要求D3基于一个对象数组呈现图表。因此,当我们将data1作为一个prop传递给我们的图表函数时,我们需要做更多的工作来访问我们对象数组中的值。
我们需要将数据值缩放到适当的范围内。原始示例通过其在数组中的索引查找值。我们将需要根据这些对象的键访问data1数组中包含的对象中的值。像这样创建我们的比例尺:
const yScale = scaleLinear()
                  .domain([0, max(data, (d) => d.sensorValue)])
                  .range([height, 0]);

// scale the values of sensorValueAddedTime into the range for our x axis values
// since it's a date you'll need to convert the strings to Date objects:

const xScale = scaleTime()
                 .domain([
                   new Date(min(data, (d) => d.sensorValueAddedTime)),
                   new Date(max(data, (d) => d.sensorValueAddedTime))
                  ])
                 .range([0, width]);

就像这样,在我们的 lineGenerator 函数中:

const lineGenerator = line()
      .x((d) => xScale(new Date(d.sensorValueAddedTime))
      .y((d) => yScale(d.sensorValue));

你会注意到上面提到,如果我们要使用 scaleTime(),我们必须将 sensorValueAddedTime 转换为 D3 可以理解的值 -- 目前在你的对象字符串中有一个额外的空格,但是如果你去掉那个空格 你就可以将其转换为 Date 对象并使用 d3.scaleTime() 这里有一个更新的、可工作的沙盒,它将这些数据呈现为一条线图,并附有额外的注释。 更新:OP 希望绘制不同的数据集,并要求点“按照那个顺序出现”。 这里有一个不同的可工作的沙箱,可以正确地将这个不同的数据集呈现为一条线图。 似乎你所要求的 - “柱状图的折线图版本” 可能并不能得到你想要的结果。让我们说一下:
如果你使用 xScale 上每个对象的索引位置,像这样:
const xScale = scaleLinear()
      .domain([0, data.length - 1])
      .range([0, width]);

那样会生成一个相当无聊的图表--因为你是在说“在表示数组索引的每个整数处放置一个点”。换句话说,在1、2、3等处放置一个点。

对于这个图表,我们应该选择数据中有意义的值--比如data1.anno--并用它们来传达信息。

例如:

根据数组的索引位置将值缩放到范围内,并根据其索引位置绘制它们,就像这样:

const xScale = scaleLinear()
      .domain([0, data.length - 1])
      .range([0, width]);
    

....chart code....

.attr("cx", (value, index) => xScale(index))

产生这个图表:

plot of points by index position in array

解决方案

将数据的值缩放到特定范围内,并根据其值绘制点。我们可以使用 d3 的 extent 方法,如下所示:

const xScale = scaleLinear()
      .domain(extent(data, (d) => d.anno))
      .range([0, width]);

    const yScale = scaleLinear()
      .domain([0, max(data, (d) => d.consumo)])
      .range([height, 0]);

....chart code...

.attr("cx", (value) => xScale(value.anno))
.attr("cy", (value) => yScale(value.consumo));

生成这个图表:

enter image description here

data1.anno 显然是年份。将年份解析为日期对象的选项是一个不错的选择。var parse = d3.timeParse("%Y"),然后使用它来设置您的时间刻度的域:.domain([parse(minDate), parse(maxDate)])如图所示

否则,如果您希望在 x 轴上保留该值而不将其视为 Date 对象,则可以使用 Javascript 的 toFixed() 方法去除小数点,使用 d3.format 移除逗号分隔符,并使用 axis.ticks() 更改出现的刻度数。

这将产生以下图表:

enter image description here

使用数据中的值而不是索引来生成包含这些点的线。
const lineGenerator = line()
      // .x((d, index) => xScale(index)) // old
      // .y((d) => yScale(d)) // old
      .x((d) => xScale(d.anno)) // new
      .y((d) => yScale(d.consumo)) // new
      .curve(curveCardinal);

这将生成以下图表:

enter image description here

最后,如果您的目标是将每个对象中的颜色值分配给一个点,请访问这些值并将它们分配给填充,就像这样:
.attr("fill", (value) => value.color)

这将产生以下图表:

enter image description here

这里是工作沙盒。

希望对您有所帮助!✌️


我已经用正确的数据更新了 Code Sandbox,请检查一下。 - vivek singh
实际上,我不想将其更改为日期对象,我希望它显示数据以那种顺序进入。 - vivek singh
明白了,答案已经更新。简而言之,您仍然需要访问数组中每个对象中的值。如果这回答了您的问题,请标记一下。这是带有不同数据集的工作沙盒 https://codesandbox.io/s/eloquent-vaughan-pg99v?file=/src/BrushChart.js:1550-1594 - MGO
嘿,我试了你的代码,现在如果我尝试使用以前的数据,即 2021年7月9日10:56:22 的日期以这种形式出现,我想以相同的格式显示,所以它不起作用,实际上我不想做任何其他事情,只是无论我得到什么数据,我都想按照那个顺序显示,因此每种情况下的数据都不相同,因为我的数据是动态的,所以在一个地方我会像上面那样获取,所以 tofixed 会导致问题,当我删除它时,图表没有清晰地显示。 - vivek singh
这是预期的,因为sensorValueAddedTime是一个字符串。另一个对象(anno)中的值是一个数字。上面的答案正确地描述了如何使用D3绘制这些值,符合原始问题。将该值读取为日期对象将解决两种情况下的问题。看起来你的问题实际上可能与处理和规范化来自不同来源的数据有更多关系 -- "给定具有不同类型值的对象,如何处理JSON以使这些值可以表示在相同的维度中?" - MGO
这是一个沙盒示例,展示了如何将要在x轴上绘制的值转换为日期对象。我会将其添加到原始答案中。https://codesandbox.io/s/lucid-haslett-7vobh?file=/src/BrushChart.js - MGO

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接