简体   繁体   English

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

[英]How to create line chart with JSON data using D3

I am working with D3.js and react-hooks to create charts, So I tried creating one Line chart by searching around and got one.我正在使用 D3.js 和 react-hooks 来创建图表,所以我尝试通过四处搜索并得到一个Line图来创建一个Line图。

  • But the one I am working with is using Sample data, here in my case I have JSON data.但是我正在使用的是示例数据,在我的情况下,我有 JSON 数据。
  • I have made the charts responsive as well using resize-observer-polyfill this library.我还使用resize-observer-polyfill这个库使图表具有响应性。
  • Now I am struggling to implement it with JSON data, to renders it with dynamic data.现在我正在努力用 JSON 数据实现它,用动态数据呈现它。

What I did我做了什么

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>

Here I am not able to pass the data, my data is below这里我无法传递数据,我的数据在下面

[
  { 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" }
]

Here in my data I have color for each data, which I want to show in each dot generated.在我的数据中,每个数据都有颜色,我想在生成的每个点中显示颜色。

working code sandbox of line chart 折线图的工作代码沙箱

Similarly I tried doing bar chart and it is working fine同样,我尝试制作条形图,效果很好

I did some dynamic rendering to labels, when we resize the window the labels gets adjusted automatically.我对标签做了一些动态渲染,当我们调整窗口大小时,标签会自动调整。

Here is the full working bar chart what I am trying to implement to line chart 这是我试图对折线图实施的完整工作条形图

I have also commented the lines where I am doing what.我还评论了我正在做什么的行。

Edit / Update编辑/更新

I ahve been following @MGO 's answer and it helped me Alot, but still I am facing issue to align labels and filling the color to dots.我一直在关注@MGO 的回答,它对我帮助很大,但我仍然面临对齐标签和将颜色填充为点的问题。

如您所见,标签彼此重叠

actually it is obvious that it will overlap because of the text size, but just to overcome that In bar chart I have used below code实际上很明显它会因为文本大小而重叠,但只是为了克服在条形图中我使用了下面的代码

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

what it is doing is when the device size is small it will filter some labels and will not show labels.它所做的是当设备尺寸较小时,它会过滤一些标签并且不会显示标签。

And also I am using below code to give color而且我正在使用下面的代码来提供颜色

.attr("fill", ({ color }) => color) 

But is is not taking any color, but it is taking by default black color.但是它不采用任何颜色,而是默认采用黑色。

I have data to show as label as July.9.2021 11:18:28 but I only want to show time so what I am doing in my bar chart code is below我有数据作为标签显示为July.9.2021 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);

Same I am trying to do with Line chart, In a simple way, I do not want to change this to any time and all.同样,我正在尝试使用折线图,以简单的方式,我不想将其更改为任何时候。

This is the second data, so basically the Answer I got is only working for 1st data not for second, I want that to be dynamic if data comes in any of these format I want to show.这是第二个数据,所以基本上我得到的答案只适用于第一个数据而不适用于第二个数据,如果数据以我想显示的任何这些格式出现,我希望它是动态的。

sensorValueAddedTime I want to show on x-axis sensorValue On y-axis sensorValueAddedTime我想在 x 轴上显示sensorValue On y 轴

I have already added my bar chart full working code, Same I want to do with line chart.我已经添加了我的条形图完整的工作代码,我想用折线图做同样的事情。

    [
    {
      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"
    }
  ]

In the original example from Muri's D3 with React Hooks video series data is a flat single-dimensional array: [10, 25, 30, 40, 25, 60] .带有 React Hooks 视频系列data Muri 的D3的原始示例中,是一个扁平的一维数组: [10, 25, 30, 40, 25, 60]

In the new array you're providing ( data1 ), we're asking D3 to render a chart based on an array of objects .在您提供的新数组 ( data1 ) 中,我们要求 D3 基于对象数组呈现图表。 So when we pass data1 in as a prop to our chart function, we need to do a little more work to access the values inside of our array of objects.所以当我们将data1作为 prop 传递给我们的图表函数时,我们需要做更多的工作来访问我们对象数组中的值。

We'll need to scale the data values into the appropriate ranges.我们需要将数据值缩放到适当的范围内。 The original example looked up the values by their index in the array.原始示例按数组中的索引查找值。 We're going to need to access the values in the objects contained in the data1 array based on those objects' keys.我们将需要根据这些对象的键访问包含在data1数组中的对象中的值。 Like this, to create our scales:像这样,创建我们的秤:

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

And like this, in our lineGenerator function:就像这样,在我们的lineGenerator函数中:

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

You'll notice above that if we're going to use scaleTime() , we'll have to convert sensorValueAddedTime into a value that D3 can make sense of -- right now there's an extra space in the string in your object, but if you strip that space you can convert to a Date object and use d3.scaleTime() .您会在上面注意到,如果我们要使用scaleTime() ,我们必须将sensorValueAddedTime转换为 D3 可以理解的值 - 现在您的对象中的字符串中有一个额外的空间,但是如果你d3.scaleTime()可以转换为 Date 对象并使用d3.scaleTime()

There's an updated, working Sandbox that's rendering this data to a line chart with additional comments here. 有一个更新的、有效的 Sandbox,可将这些数据呈现为带有附加注释的折线图。

Update: OP was asking to plot a different set of data and asked that the points "appear in that order".更新:OP 要求绘制一组不同的数据,并要求这些点“按该顺序出现”。

There's a different working Sandbox rendering this different set of data to a line chart correctly here. 这里有一个不同的工作沙箱,可以将这组不同的数据正确呈现为折线图。

It seems like what you're asking for -- the "line graph version of the bar chart" may not produce what you're after.这似乎是您所要求的 - “条形图的折线图版本”可能不会产生您想要的东西。 Just talking it through:简单说一下:

If you use the index position of each object on your xScale, like this:如果您使用 xScale 上每个对象的索引位置,如下所示:

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

that will produce a pretty boring chart -- because you're saying "put a dot at every whole number representing the index of the array".这将产生一个非常无聊的图表——因为你说“在每个代表数组索引的整数上放一个点”。 In other words, a dot at 1, at 2, at 3, and so on.换句话说,在 1、2、3 处的一个点,依此类推。

For this chart, we should choose meaningful values in our data -- like data1.anno -- and use those to communicate.对于这个图表,我们应该在我们的数据中选择有意义的值——比如data1.anno并使用这些值进行通信。

For example:例如:

Scaling the values into the range according to the index positions of the array and plotting them according to their index position, like this:根据数组的索引位置将值缩放到范围内,并根据它们的索引位置绘制它们,如下所示:

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

....chart code....

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

Produces this plot:产生这个情节:

按数组中索引位置绘制点

Solution解决方案

Scaling the values of the data into the range and plotting the points according to their values.将数据的值缩放到范围内并根据它们的值绘制点。 We can use d3's extent method , like this:我们可以使用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));

Produces this plot:产生这个情节:

在此处输入图片说明

data1.anno is clearly a year. data1.anno显然是一年。 The option to parse the years as date obects is a good one.将年份解析为日期对象的选项是一个不错的选择。 var parse = d3.timeParse("%Y") , then use this to set the domain of your time scale: .domain([parse(minDate), parse(maxDate)]) , as shown here . var parse = d3.timeParse("%Y") ,然后使用它来设置您的时间刻度的域: .domain([parse(minDate), parse(maxDate)])如下所示

Otherwise, if you'd like to preserve that value on the x-axis and not treat it as a Date object, you can use Javascript's toFixed() method to remove the decimal places, remove the comma delimiter with d3.format and change the number of ticks that appear with axis.ticks() .否则,如果您想在 x 轴上保留该值而不将其视为 Date 对象,则可以使用 Javascript 的toFixed()方法删除小数位,使用d3.format删除逗号分隔符并更改随axis.ticks()出现的刻度数。

That produces this chart:这产生了这个图表:

在此处输入图片说明

To generate a line with these points, use the values in the data, not the indexes.要使用这些点生成一条线,请使用数据中的值,而不是索引。

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

That produces this chart:这产生了这个图表:

在此处输入图片说明

Finally, if your goal is to assign the color value in each object to a point, access those values and assign their values to the fill, like this:最后,如果您的目标是将每个对象中的颜色值分配给一个点,请访问这些值并将它们的值分配给填充,如下所示:

.attr("fill", (value) => value.color)

That produces this chart:这产生了这个图表:

在此处输入图片说明

Here's the working sandbox. 这是工作沙箱。

Hope this helps!希望这可以帮助! ✌️ ✌️

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM