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

Processing a list in Angular using Observables

In my Angular12 application, I am fetching data from a Firebase Realtime DB using AngularFire. To streamline my code and ensure consistency, I have implemented a DAO service to preprocess the retrieved data (e.g., converting string dates to Date objects). ...

Include a query parameter each time a page is added to bookmarks

Is there a way to automatically add a query parameter to a page URL when bookmarked using Chrome? For example, if I bookmark https://www.example.com, can it be saved as https://www.example.com/?bookmarked? I'm thinking I might need to use JavaScript ...

A solution for resolving the RuntimeException in genuitec's TypeScript Editor for Eclipse Oxygen

After setting up a fresh Eclipse Oxygen and installing the Angular IDE from Genuitec, I encountered an issue on the second day when I opened a project and accessed a *.ts File. The error message displayed was: java.lang.RuntimeException: java.lang.Illegal ...

What is the best way to trigger a Redux Toolkit action within a React Router DOM action?

My router setup looks like this: const router = createBrowserRouter([ { path: "/", element: <MainLayout />, errorElement: <Error />, children: [ { path: "/inventory", element: <Inve ...

Can anyone tell me the best way to access the name attribute of an HTML element in TypeScript?

Currently, my code is utilizing the name attribute to verify whether the user has entered information in a specific field and validating the input. However, I am facing an issue where the submit button remains active even if there are empty fields presen ...

Type verification not functioning properly in a standalone TypeScript function

I am trying to validate the type of a parameter in a separate function, but I keep getting this error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable ...

How to prevent unnecessary new instances from being created by the Inject() function in Angular

Can someone please clarify if the inject() function provides different instances of a service? I suspect this might be why my code is not functioning as expected. Let's examine the code snippet below: { path: 'recipes', comp ...

Unable to inject basic service into component

Despite all my efforts, I am struggling to inject a simple service into an Angular2 component. Everything is transpiling correctly, but I keep encountering this error: EXCEPTION: TypeError: Cannot read property 'getSurveyItem' of undefined Even ...

Error: Unexpected input detected in `ts.resolveTypeReferenceDirective`. This issue may lead to a debug failure

I'm encountering the error below: { "name": "Angular", "version": "1.0.0", ... } If anyone has insights on what steps to take next or the potential cause of the problem, your input would be greatly a ...

Identifying Classifications Based on Input Parameters

I'm encountering some difficulty in typing a function that calls an external API and returns data based on the parameters sent. Here is what I have so far... import axios from 'axios'; interface IGetContactArgs { properties?: string[]; } ...

What is the process for configuring vue.config.js with typescript?

Just starting out with typescript and running into an issue while configuring vue.config.js const webpack = require("webpack"); module.exports = { plugins: [ new webpack.DefinePlugin({ __VUE_I18N_FULL_INSTALL__: true, __ ...

Eslint is unable to locate file paths when it comes to Solid.js tsx extensions

Whenever I attempt to import TSX components (without the extension), Eslint raises an error stating that it's unable to resolve the path: However, if I add the TSX extension, it then prompts me to remove it: This represents my configuration in .esli ...

What is the best way to send props (or a comparable value) to the {children} component within RootLayout when using the app router in Next.js

As I work on updating an e-commerce website's shopping cart to Next.js 13+, I refer back to an old tutorial for guidance. In the previous version of the tutorial, the _app.ts file utilized page routing with the following code snippet: return ( < ...

Path for WebStorm file watcher's output

I'm in the process of setting up a Typescript project in WebStorm, where I want the files to be transpiled into a dist folder. This is my current project folder structure: projectroot/src/subfolder/subfolder/index.ts What I aim for is to have the f ...

Creating a unique theme export from a custom UI library with Material-UI

Currently, I am in the process of developing a unique UI library at my workplace which utilizes Material-UI. This UI library features a custom theme where I have integrated custom company colors into the palette object. While these custom colors work perfe ...

My component is displaying a warning message that advises to provide a unique "key" prop for each child in a list during rendering

I need help resolving a warning in my react app: Warning: Each child in a list should have a unique "key" prop. Check the render method of `SettingRadioButtonGroup`. See https://reactjs.org/link/warning-keys for more information. at div ...

Array of options with specified data types in Props interface

Trying to implement options as props for styling a button component in Astro. Still learning TypeScript. Encountering the error message: Generic type 'Props<style>' requires 1 type argument(s). Below is the code snippet: --- import type { H ...

It appears that React Native's absolute paths are not functioning as expected

I have been attempting to set up React Native with absolute paths for easier imports, but I am having trouble getting it to work. Here is my tsconfig.json: { "compilerOptions": { "allowJs": true, "allowSynthetic ...

"Send the selected radio button options chosen by the user, with the values specified in a JSON format

My current task involves inserting radio button values into a MySql database using Angular. The form consists of radio buttons with predefined values stored in a json file. Below is an example of how the json file is structured: //data.json [{ "surve ...

The TypeScript error "Issue with Type Assertion: 'This expression is not callable Type'...' has no call signatures" occurs when there is a missing semicolon

Here's a simplified version of our original code: const start: number = 10 const end: number = 20 (someElement as HTMLInputElement).setSelectionRange(start, end) We encountered an error with the 20, where a red squiggly line appeared indicating ...