Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

  • But the one I am working with is using Sample data, here in my case I have JSON data.
  • I have made the charts responsive as well using resize-observer-polyfill this library.
  • Now I am struggling to implement it with JSON data, to renders it with dynamic data.

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.

as you can see the labels are overlapping to each other

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

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

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"
    }
  ]
like image 587
vivek singh Avatar asked Jul 12 '21 05:07

vivek singh


People also ask

What is D3 JSON?

d3.json() Sends http request to the specified url to load . json file or data and executes callback function with parsed json data objects. d3.tsv() Sends http request to the specified url to load a .

What does D3 JSON return?

The function d3. json() is an asynchronous function that directly returns (with an undefined value I assume). Only when the data is received from the backend, the callback function you passed to it will be called.

Is D3 js a visualization library?

D3. js is a JavaScript library for creating visualizations like charts, maps, and more on the web. Unlike many other data visualization libraries that provide ready made charts, D3 gives you lots of creative freedom as you have total control over the visualizations you create.

How to create a line chart in angular with D3?

Install D3.js as a dependency and as we’re working with Typescript, install the d3 types as well: Next, create a component for your chart via Angular CLI: Within the Typescript file of this component, we’re going to import d3: Add the above JSON to your app.component.ts: ... We want to pass it as an input to our line chart component.

How to create a bar chart in D3 using D3 JS?

Step 1: Integrate the D3.js file. First of all, for drawing a bar chart in d3.js you required to add js to the HTML page. You can directly integrate through CDN URL. Alternatively, you can download the latest version from their official URL.

How to read a JSON file in D3?

You will read the JSON using d3.json () method. Continue the above code with the following code. The methods x.domain () and y.domain () methods map the data for the x and y-axis. There is two grouping done first on the x-axis and other on the y-axis.

What is D3 JS data visualization?

And, here is a demo of the above code in action. D3.js is an awesome JavaScript libray for data visualization. In this tutorial, we focused on creating fairly simple bar and line charts. If you’re interested in experimenting more, try adding additional visualization techniques from the D3 library to the charts in this article.


Video Answer


1 Answers

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].

In the new array you're providing (data1), we're asking D3 to render a chart based on an array of objects. 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.

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. 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:

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().

There's an updated, working Sandbox that's rendering this data to a line chart with additional comments here.

Update: OP was asking to plot a different set of data and asked that the points "appear in that order".

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:

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.

For this chart, we should choose meaningful values in our data -- like data1.anno -- and use those to communicate.

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:

plot of points by index position in array

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:

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:

enter image description here

data1.anno is clearly a year. 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.

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().

That produces this chart:

enter image description here

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:

enter image description here

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:

enter image description here

Here's the working sandbox.

Hope this helps! ✌️

like image 147
MGO Avatar answered Oct 18 '22 04:10

MGO