A dynamic d3 line chart showcasing various line colors during transitions

I have implemented a d3 line chart using this example as a reference: https://codepen.io/louisemoxy/pen/qMvmBM

My question is, is it possible to dynamically color the line in the chart differently based on the condition y > 0, even during the transition? The data includes values both above and below 0, so simply splitting the path into two pieces won't work.

Below is my update method, responsible for handling the transition:

update(data: Data[]): void {
    this.x.domain([new Date(data[0].date), new Date(data[this.data.length - 1].date)])
    this.chart.selectAll('.xaxis')
        .transition()
        .duration(1000)
        .call(this.xAxis);

    const minVal = d3.min(data, (d: Data) => {return d.val}) as number;
    const min = minVal < 0 ? minVal : 0;
    const max = d3.max(data, (d: Data) => { return d.val; }) as number;
    this.y = d3.scaleLinear()
      .domain([min, max])
      .range([ this.height, 0 ]);

    this.yAxis = d3.axisLeft(this.y);

    this.chart.selectAll('.yaxis')
        .transition()
        .duration(1000)
        .call(this.yAxis);

    // Create the u variable
    let u = this.grp.select("path")
        .interrupt()
        .datum(data)
        .attr('class', 'performance')
        .attr('d', d3.line()
            .x((d: Data) => { return this.x(new Date(d.date)); })
            .y((d: Data) => { return this.y(d.val); }));

    const pathLength = u.node().getTotalLength();
    const transitionPath = d3
        .transition()
        .ease(d3.easeSin)
        .duration(1500);
    u
        .attr("stroke-dashoffset", pathLength)
        .attr("stroke-dasharray", pathLength)
        .attr("stroke", "steelblue")
        .transition(transitionPath)
        .attr("stroke-dashoffset", 0);
  }

Answer №1

Utilizing a linearGradient is the most effective approach, as highlighted in the comments. While I initially considered marking this as a duplicate, I realized that existing solutions lack a comprehensive working example. Therefore, I have combined a practical demonstration with the code snippet from your provided link:

<!DOCTYPE html>

<html>
  <head>
    <style>
      #chart {
        text-align: center;
        margin-top: 40px;
      }
    </style>
  </head>

  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <button>Update Chart</button>
    <div id="chart"></div>
    <script>
      // Fake data designed to get many crossing points at zero
      let data = d3.range(20).map((d) => {
        return {
          year: 2000 + d,
          popularity:
            Math.random() < 0.5 ? Math.random() * -20 : Math.random() * 20,
        };
      });

      // Create SVG and padding for the chart
      const svg = d3
        .select('#chart')
        .append('svg')
        .attr('height', 300)
        .attr('width', 600);

      const defs = svg.append('defs');

      // build the gradient
      const gradient = defs
        .append('linearGradient')
        .attr('id', 'line-gradient')
        .attr('gradientUnits', 'userSpaceOnUse');

      const margin = { top: 0, bottom: 20, left: 30, right: 20 };
      const chart = svg
        .append('g')
        .attr('transform', `translate(${margin.left},0)`);
      const width = +svg.attr('width') - margin.left - margin.right;
      const height = +svg.attr('height') - margin.top - margin.bottom;
      const grp = chart
        .append('g')
        .attr('transform', `translate(-${margin.left},-${margin.top})`);

      // Add empty scales group for the scales to be attatched to on update
      chart.append('g').attr('class', 'x-axis');
      chart.append('g').attr('class', 'y-axis');

      // Add empty path
      const path = grp
        .append('path')
        .attr('transform', `translate(${margin.left},0)`)
        .attr('fill', 'none')
        .attr('stroke', 'url(#line-gradient)')
        .attr('stroke-linejoin', 'round')
        .attr('stroke-linecap', 'round')
        .attr('stroke-width', 1.5);

      function updateScales(data) {
        // Create scales
        const yScale = d3
          .scaleLinear()
          .range([height, 0])
          .domain(d3.extent(data, (dataPoint) => dataPoint.popularity));
        const xScale = d3
          .scaleLinear()
          .range([0, width])
          .domain(d3.extent(data, (dataPoint) => dataPoint.year));

        gradient
          .attr('x1', 0)
          .attr('y1', yScale(yScale.domain()[0]))
          .attr('x2', 0)
          .attr('y2', yScale(0))
          .selectAll('stop')
          .data([
            { offset: '100%', color: 'blue' },
            { offset: '100%', color: 'red' },
          ])
          .enter()
          .append('stop')
          .attr('offset', function (d) {
            return d.offset;
          })
          .attr('stop-color', function (d) {
            return d.color;
          });

        return { yScale, xScale };
      }

      function createLine(xScale, yScale) {
        return (line = d3
          .line()
          .x((dataPoint) => xScale(dataPoint.year))
          .y((dataPoint) => yScale(dataPoint.popularity)));
      }

      function updateAxes(data, chart, xScale, yScale) {
        chart
          .select('.x-axis')
          .attr('transform', `translate(0,${height})`)
          .call(d3.axisBottom(xScale).ticks(data.length));
        chart
          .select('.y-axis')
          .attr('transform', `translate(0, 0)`)
          .call(d3.axisLeft(yScale));
      }

      function updatePath(data, line) {
        const updatedPath = d3
          .select('path')
          .interrupt()
          .datum(data)
          .attr('d', line);

        const pathLength = updatedPath.node().getTotalLength();
        // D3 provides lots of transition options, have a play around here:
        // https://github.com/d3/d3-transition
        const transitionPath = d3.transition().ease(d3.easeSin).duration(2500);
        updatedPath
          .attr('stroke-dashoffset', pathLength)
          .attr('stroke-dasharray', pathLength)
          .transition(transitionPath)
          .attr('stroke-dashoffset', 0);
      }

      function updateChart(data) {
        const { yScale, xScale } = updateScales(data);
        const line = createLine(xScale, yScale);
        updateAxes(data, chart, xScale, yScale);
        updatePath(data, line);
      }

      updateChart(data);
      // Update chart when button is clicked
      d3.select('button').on('click', () => {
        // Create new fake data
        const newData = data.map((row) => {
          return { ...row, popularity: row.popularity * Math.random() };
        });
        updateChart(newData);
      });
    </script>
  </body>
</html>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can we track and record NaN values in JavaScript/TypeScript as they occur in real-time?

Is there a reliable method to identify and prevent NaN values during runtime, throughout all areas of the application where they might arise? A) Are there effective linting tools available to alert about possible occurrences of NaN values within specific ...

Error Detected: An unhandled error occurred, triggering a promise rejection: TypeError: The super expression must be either null or a function in Angular version 6

Upon deploying the application on AWS Elastic Beanstalk, an error has surfaced. While the build and deployment processes were successful, one module is giving a Super Expression error. The other modules are functioning correctly, and everything works fine ...

What's preventing me from drawing a line on this D3.js chart with my Angular code?

I am attempting to create a graph on my webpage using the D3.js library for JavaScript. I am able to draw a line on an SVG using HTML, but when I try to draw a line using JavaScript, I encounter the error message: ReferenceError: d3 is not defined. Even t ...

Automatically selecting the country phone code based on the country dropdown selection

When the country dropdown is changed, I want the corresponding phone code dropdown to be dynamically loaded. <div class="row"> <div class="col-sm-8 col-md-7 col-lg-6 col-xl-5 pr-0"> <div class="form-group py-2"> <l ...

Utilizing Angular signals to facilitate data sharing among child components

I have a main component X with two sub-components Y and Z. I am experimenting with signals in Angular 17. My query is how can I pass the value of a signal from sub-component Y to sub-component Z? I have attempted the following approach which works initial ...

Grouping elements of an array of objects in JavaScript

I've been struggling to categorize elements with similar values in the array for quite some time, but I seem to be stuck Array: list = [ {id: "0", created_at: "foo1", value: "35"}, {id: "1", created_at: "foo1", value: "26"}, {id: "2", cr ...

The best approach for setting a select value and managing state in React using TypeScript

Currently, I am in the process of familiarizing myself with TypeScript within my React projects. I have defined a type for the expected data structure (consisting of name and url). type PokedexType = { name: string; url: string; } The API respon ...

What is the best way to create a general getter function in Typescript that supports multiple variations?

My goal is to create a method that acts as a getter, with the option of taking a parameter. This getter should allow access to an object of type T, and return either the entire object or a specific property of that object. The issue I am facing is definin ...

What is the best way to create and manage multiple collapsible Material-UI nested lists populated from an array with individual state in React

In my SideMenu, I want each list item to be able to expand and collapse independently to show nested items. However, I am facing the issue of all list items expanding and collapsing at the same time. Here is what I've attempted: const authNavigation ...

TS1086: Attempting to declare an accessor within an ambient context is not allowed

While using Angular, I encountered the error TS1086: An accessor cannot be declared in an ambient context. when using Javascript getters and setters in this Abstract Typescript class. Here is the code snippet causing the issue: /** * The current id ...

Guide on storing geolocation information in an array on Google Firebase Realtime Database using Angular HttpClient

I have been working on developing an innovative Android geolocation tracking app using Ionic with the assistance of the Cordova Geolocation plugin. The tracking feature has been successfully implemented thus far. However, I am currently facing challenges w ...

Problem with Infragistics radio button not firing change event when value is set manually

After migrating from Angular 11 to 17, I encountered a strange issue with my application's Infragistics radio button. The change event for the radio button does not trigger manually for the first time, but it works fine when changed through the applic ...

Uncover the mystery behind the return value of a generic function in TypeScript

I can't seem to wrap my head around why TypeScript is behaving in the way described below. Snippet 01| const dictionary: { [key: string]: unknown} = {} 02| 03| function set<T>(key: string, value: T): void { 04| dictionary[key] = value; 05| } ...

What is the reason behind the warning about DOM element appearing when custom props are passed to a styled element in MUI?

Working on a project using mui v5 in React with Typescript. I am currently trying to style a div element but keep encountering this error message in the console: "Warning: React does not recognize the openFilterDrawer prop on a DOM element. If you in ...

Upon receiving the API response, my Angular webpage is failing to redirect to a different page

After executing my code in TypeScript, I encountered an issue with the updateProduct method calling the API to update a product based on form values. The update functionality works fine, but afterwards, I am receiving the following error: error: SyntaxErr ...

ngOnChanges will not be triggered if a property is set directly

I utilized the modal feature from ng-bootstrap library Within my parent component, I utilized modalService to trigger the modal, and data was passed to the modal using componentInstance. In the modal component, I attempted to retrieve the sent data using ...

Tips on providing validation for either " _ " or " . " (select one) in an Angular application

I need to verify the username based on the following criteria: Only accept alphanumeric characters Allow either "_" or "." (but not both) This is the code snippet I am currently using: <input type="text" class="form-control" [ ...

The build error TS1036 is thrown when a function with a return statement is moved to index.d.ts, even though it worked perfectly in a standard TS file

In my project using Node v14.x and StencilJS (React) v2.3.x, I encountered an issue with a test-helper file containing a function that converts string-arrays to number-arrays: export function parseNumericStringOrStringArrayToIntegers(value: string | (strin ...

What are the best practices for preventing risky assignments between Ref<string> and Ref<string | undefined>?

Is there a way in Typescript to prevent assigning a Ref<string> to Ref<string | undefined> when using Vue's ref function to create typed Ref objects? Example When trying to assign undefined to a Ref<string>, an error is expected: co ...

Error occurs in Angular Mat Table when attempting to display the same column twice, resulting in the message "Duplicate column definition name provided" being

What is the most efficient method to display a duplicated column with the same data side by side without altering the JSON or using separate matColumnDef keys? Data: const ELEMENT_DATA: PeriodicElement[] = [ {position: 1, name: 'Hydrogen', wei ...