Is there a way to adjust the starting and ending points of a bezier curve in D3 by utilizing the link generator?

I'm currently working on developing a horizontal hierarchical tree Power BI custom visual using TypeScript and D3. I am utilizing d3's treeLayout for this purpose, and my task is to create a link generator that can plot bezier, step, and diagonal links based on the user's choice.

The challenge I face is related to the nodes being rectangles with specific widths, which require the links to go from (source.x + width/2, y) to (target.x - width/2, y). While I have successfully implemented this for diagonal and step options using the d3.line() function, I have encountered difficulties with the bezier option and the linkHorizontal function. Despite reading through the documentation and examining the d3 code extensively, I haven't been able to utilize the source, target, and context functions effectively.

Below is a simplified version of my current code:

  • this.settings.links.style determines the selected link style: "bezier", "curve", or "step"
  • this.settings.nodes.width specifies the node width
  • this.orientation.x maps x and y functions as demonstrated in Bostock's https://bl.ocks.org/mbostock/3184089
const linkH = d3.linkHorizontal().x(d => this.orientation.x(d)).y(d => this.orientation.y(d));

let linkGenerator = this.settings.links.style == "bezier" ? linkH 
            :
            (this.settings.links.style == "step" ?
                d => d3.line().curve(d3.curveStep)([[this.orientation.x(d.source) + this.settings.nodes.width / 2, this.orientation.y(d.source)],
                [this.orientation.x(d.target) - this.settings.nodes.width / 2, this.orientation.y(d.target)]])
                :
                d => d3.line()([[this.orientation.x(d.source) + this.settings.nodes.width / 2, this.orientation.y(d.source)],
                [this.orientation.x(d.target) - this.settings.nodes.width, this.orientation.y(d.target)]])
            )

var links = linkGroup.selectAll("path")
              .data(this.viewModel.hierarchy.links())
              .enter()
              .append("path")
              .attr("d", linkGenerator);

Answer №1

Check out this alternative Bezier curve function that can be used in place of D3's:

const calculateBezierPath = (start, end) => {
    if (Math.abs(start.x - end.x) > Math.abs(start.y - end.y)) {
    const midpointX = (end.x + start.x) / 2;
    return `M ${start.x},${start.y} C ${midpointX},${start.y} ${midpointX},${end.y} ${end.x},${end.y}`;
  } else {
    const midpointY = (end.y + start.y) / 2;
    return `M ${start.x},${start.y} C ${start.x},${midpointY} ${end.x},${midpointY} ${end.x},${end.y}`;
  }
};

Take a look at the demo on JSFiddle

Answer №2

After some experimentation, I managed to create a function that achieves the goal of combining d3.line() and d3.linkHorizontal(). The key discovery was utilizing an implementation of d3.DefaultLinkObject to leverage the original source and target passed by attr("d", f(d)).

For anyone looking for a solution:

class CustomLink implements d3.DefaultLinkObject {
    public source: [number, number];
    public target: [number, number];
    constructor(s: [number, number], t: [number, number]) {
        this.source = s;
        this.target = t;
    }
}

function generateLinkPath(d) {

   var deltaX = self.settings.nodes.width / 2;

   var pSource: [number, number] = [self.orientation.x(d.source) + deltaX, self.orientation.y(d.source)];
   var pTarget: [number, number] = [self.orientation.x(d.target) - deltaX, self.orientation.y(d.target)];

   var points = [pSource, pTarget];
   var linkObject: CustomLink = new CustomLink(pSource, pTarget);
   var path = "";

   if (self.settings.links.style == "step") {

      var lineGenerator = d3.line().curve(d3.curveStep);
      path = lineGenerator(points);

   } else if (self.settings.links.style == "diagonal") {

      var lineGenerator = d3.line();
      path = lineGenerator(points);

   } else {  // bezier

      var linkGen = d3.linkHorizontal().x(d => d[0]).y(d => d[1]);
      path = linkGen(linkObject);
   }
            
   return path;
}

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

Encountering TS1204 error on version 1.5.0-beta with ES6 target, yet all functionalities are running smoothly

After successfully compiling everything from Typescript to ES6 to ES5, I encountered an error that has me stumped. The error message reads as follows: Error TS1204: Cannot compile external modules into amd or commonjs when targeting es6 or higher. Here i ...

Managing nested request bodies in NestJS for POST operations

A client submits the following data to a REST endpoint: { "name":"Harry potter", "address":{ "street":"ABC Street", "pincode":"123", "geo":{ &q ...

What is the proper way to include type annotation in a destructured object literal when using the rest operator?

When working with objects, I utilize the spread/rest operator to destructure an object literal. Is there a way to add type annotation specifically to the rest part? I attempted to accomplish this task, but encountered an error when running tsc. const { ...

The issue persists with react-hook-form and Material UI RadioGroup as the selected value remains null

Having trouble with RadioGroup from Material UI when using react-hook-form Controller. I keep getting a null selected value, even though my code seems right. Can you help me figure out what's missing? import * as React from "react"; import { ...

The intricate field name of a TypeScript class

I have a TypeScript class that looks like this - export class News { title: string; snapshot: string; headerImage: string; } In my Angular service, I have a method that retrieves a list of news in the following way - private searchNews(sor ...

Angular - A simple way to conceal a specific row in a mat-table using Angular

I am looking to dynamically hide or show a specific column in a table by clicking on a button. The goal is to hide or delete the weight column when the "hide weight" button is clicked, and then show the weight column when the "show weight" button is clicke ...

Issues with TypeScript: outFile in tsconfig not functioning as expected

Currently, I am utilizing Atom as my primary development environment for a project involving AngularJs 2 and typescript. To support typescript, I have integrated the atom-typescript plugin into Atom. However, I noticed that Atom is generating separate .js ...

Creating mandatory reactive form fields in Angular 11's HTML code based on conditions

I am facing an issue with two select/dropdown fields in my form. The second dropdown field should render based on a condition *ngIf="selectedStdntList?.packages". However, the problem is that the submit form function stops working even when the c ...

Error: AppModule requires an array of arguments in order to function properly

Upon successfully compiling my Angular application and running ng serve, I encountered the following error in the browser console. AppComponent_Host.ngfactory.js? [sm]:1 ERROR Error: Arguments array must have arguments. at injectArgs (core.js:1412) at c ...

Why am I encountering this rendering issue when passing data to the ReactTable component?

The following code snippet represents the parent component containing an array of columns and data. const TransactionTable = () => { const columns = useMemo( () => [ { Header: 'DATE/TIME', accessor: &apos ...

Designing personalized plugins with Typescript in Nuxt

In my Nuxt project, I have implemented a custom plugin file that contains an object with settings called /helpers/settings: export const settings = { baseURL: 'https://my-site.com', ... }; This file is then imported and registered in /plugi ...

Adding TypeScript definition file to an npm package: A step-by-step guide

Is it possible to include typescript definitions (.d.ts files) in a pure javascript project without using TypeScript itself? I'm struggling to find any information on how to do this directly in the package.json. ...

Adding an event listener to the DOM through a service

In the current method of adding a DOM event handler, it is common to utilize Renderer2.listen() for cases where it needs to be done outside of a template. This technique seamlessly integrates with Directives and Components. However, if this same process i ...

Check to see if the upcoming birthday falls within the next week

I'm trying to decide whether or not to display a tag for an upcoming birthday using this boolean logic, but I'm a bit confused. const birthDayDate = new Date('1997-09-20'); const now = new Date(); const today = new Date(now.getFullYear( ...

Issue with exclude not functioning in tsconfig.json for Angular Typescript deployment

I am encountering an issue with a module within the node_modules directory while compiling my Angular 4 project. The error messages I'm receiving are as follows, even after attempting to exclude the problematic module in the tsconfig.json file. Can an ...

Using Angular 4 to retrieve a dynamic array from Firebase

I have a dilemma while creating reviews for the products in my shop. I am facing an issue with the button and click event that is supposed to save the review on the database. Later, when I try to read those reviews and calculate the rating for the product, ...

Issue with Typescript Conditional Type not being functional in a function parameter

For a specific use-case, I am looking to conditionally add a key to an interface. In attempting to achieve this, I used the following code: key: a extends b ? keyValue : never However, this approach breaks when a is generic and also necessitates explicit ...

Vue 3 with Typescript - encountering a property that does not exist on the specified type error

I'm currently working on populating a component with leads fetched from an API. In my setup, I have a LeadService file and a Vue template file. The challenge I'm encountering is related to using an async call in my template file. Although the cal ...

I'm having trouble getting Remix.run and Chart.js to cooperate, can anyone offer some guidance?

I've encountered a challenge with Remix.run and chart.js (react-chartjs-2) when attempting to display the chart. I followed the documentation and installed the necessary dependencies: react-chartjs-2 and chart.js. Here's the snippet from my pac ...

React error: The DatePickerProps generic type must have one type argument specified

Date Selection Component: import React from "react" import AdapterDateFns from '@mui/lab/AdapterDateFns'; import { LocalizationProvider } from '@mui/lab'; import { DatePicker, DatePickerProps } from '@mui/lab'; cons ...