Chart featuring top corners smoothly rounded off for a unique doughnut design

I've been attempting to create a D3.js chart similar to the one shown in the first screenshot:

https://i.sstatic.net/GgP1d.png

While I can easily replicate a chart like the second screenshot using the default examples, the challenge arises when I try to round only the top corners of each segment.

The default cornerRadius method applies rounding to all corners, which is not what I'm aiming for.

Despite seeking advice from the D3.js Assistant (Chat GPT) and exploring various approaches, such as manually creating the SVG paths, I haven't been successful due to difficulty in calculating the correct values for the arcs.

Some methods I have tried include:

  • Removing existing rounded corners by customizing the path creation.
  • Manually creating each segment with a customized method.
  • Creating a layer with all corners rounded and then overlaying another layer with unrounded bottom corners (a workaround that feels somewhat hacky).
  • Resorting to hitting my head on the keyboard out of frustration.

Helpful resources I came across:

  • Vanilla SVG rounded corners (12 years old thread): This thread provided insights into creating arcs manually, but integrating these values with D3 proved challenging.

Answer №1

Upon careful examination of the images, I have determined that the top corners should be treated as outer corners. This method can be adjusted to accommodate various corner configurations.

To achieve the desired outcome, the most direct approach is to make modifications to the d3-arc source code and craft a personalized D3 module.

To integrate this functionality, it is essential to remove specific lines of code:

// Does the sector’s inner ring (or point) have rounded corners?
else if (rc0 > epsilon) {
  t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
  t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);

  context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);

  // Have the corners merged?
  if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);

  // Otherwise, draw the two corners and the ring.
  else {
    context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
    context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
    context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
  }

This specific code snippet deals with rounding the inner corners. By removing this segment, only the code responsible for rounding the outer corners will remain. The anticipated result should mirror the provided image.

I have constructed a basic example by integrating the customized source code into an existing module (for chevron arcs) without full compilation, thus avoiding unnecessary dependencies. While not suitable for production purposes, this demonstration effectively showcases the adjustments and allows you to assess the impact. The sample implementation is outlined below.

The new arc function can be accessed using `d3.customArc()` instead of `d3.arc()`, operating similarly to `d3.arc()` but excluding cornerRadius application to the interior corners.

let data = [
    {name: "A", value: 100 },
    {name: "B", value: 200 },
    {name: "C", value: 300 },
    {name: "D", value: 400 },
  ]
  
  const width = 400;
  const radius = 150;
  const height = 400;

  const arc = d3.customArc()
      .innerRadius(radius * 0.67)
      .outerRadius(radius - 1)
      .cornerRadius(20);

  const pie = d3.pie()
      .padAngle(1 / radius)
      .sort(null)
      .value(d => d.value);

  const color = d3.scaleOrdinal()
      .domain(data.map(d => d.name))
      .range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), data.length).reverse());

  const svg = d3.select("svg")
      .attr("width", width)
      .attr("height", height)
      .attr("viewBox", [-width / 2, -height / 2, width, height])
      .attr("style", "max-width: 100%; height: auto;");
      


  svg.append("g")
    .selectAll()
    .data(pie(data))
    .join("path")
      .attr("fill", d => color(d.data.name))
      .attr("d", arc)
    .append("title")
      .text(d => `${d.data.name}: ${d.data.value.toLocaleString()}`);

  svg.append("g")
      .attr("font-family", "sans-serif")
      .attr("font-size", 12)
      .attr("text-anchor", "middle")
    .selectAll()
    .data(pie(data))
    .join("text")
      .attr("transform", d => `translate(${arc.centroid(d)})`)
      .call(text => text.append("tspan")
          .attr("y", "-0.4em")
          .attr("font-weight", "bold")
          .text(d => d.data.name))
      .call(text => text.filter(d => (d.endAngle - d.startAngle) > 0.25).append("tspan")
          .attr("x", 0)
          .attr("y", "0.7em")
          .attr("fill-opacity", 0.7)
          .text(d => d.data.value.toLocaleString("en-US")));
<svg width="960" height="960"></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="d3customArc.js"></script>

...

(function (global, factory) {
  ...
}(this, (function (exports,d3Path) { 'use strict';

 ...

Object.defineProperty(exports, '__esModule', { value: true });

})));

</script>

The comprehensive source code for d3-arc referenced in this response can be accessed here.

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

The random number generator in TypeScript not functioning as expected

I have a simple question that I can't seem to find the answer to because I struggle with math. I have a formula for generating a random number. numRandomed: any; constructor(public navCtrl: NavController, public navParams: NavParams) { } p ...

Create a lockscreen feature in AngularJS that becomes active after a period of inactivity

I am looking to integrate a lockscreen feature into my app using Angular.js. This lockscreen will consist of a route and an HTML template containing a form that prompts the user to re-enter their password in order to keep their session active. The purpose ...

Guide to creating completely static HTML using `nuxt generate`?

Looking for a solution to ensure that Vue pages are fully generated as static HTML files in Nuxt even when they are simple and don't require any dynamic data fetching? Let's dig into the issue. <template> <div>hello world</div&g ...

No Access-Control-Allow-Origin or Parsing Error with jQuery

I am attempting to use ajax from a different server to request data from my own server as a web service. The response is a valid json generated by json_encode. {"reference":"","mobile":"","document":"","appointment":""} To avoid the 'Access Control ...

ReactJS: Checkbox status remains consistent through re-rendering of Component

I have developed a JSfiddle example Initially, this fiddle displays a list of checkboxes based on the passed props to the component. When you click the Re-render button, the same component is rendered with different props. Now, please follow these steps- ...

Trustpilot Authentication Error: Grant_type Not Recognized

I am attempting to utilize the Trustpilot API for sending email review invitations. Before making the call, I need to obtain an access token as per Trustpilot's documentation. However, when implementing the function below, I am encountering an error i ...

Having trouble getting Fullcalendar to show up on my cordova app

Currently, I am in the process of building a mobile application using ionic and cordova. My goal is to incorporate a timetable system utilizing Fullcalendar and a PHP page that showcases MySQL data in JSON format. Despite my efforts, I am encountering diff ...

To avoid TS2556 error in TypeScript, make sure that a spread argument is either in a tuple type or is passed to a rest parameter, especially when using

So I'm working with this function: export default function getObjectFromTwoArrays(keyArr: Array<any>, valueArr: Array<any>) { // Beginning point: // [key1,key2,key3], // [value1,value2,value3] // // End point: { // key1: val ...

The route in my Node.js Express application appears to be malfunctioning

I am facing an issue with my app.js and route file configuration. Here is my app.js file: const express = require('express'); const app = express(); const port = process.env.PORT || 8080; const userRoute = require('./routes/user.route' ...

Leveraging jquery's setInterval for automating tasks like a cronjob

I've been experimenting with Cronjobs and I've run into a roadblock. My goal is to have the cronjob execute every X minutes, containing a script with JavaScript that calls an ajax request every second for the next 60 seconds. The ajax call trigge ...

Appending a row to a table will not trigger events in Select2

Despite several attempts, I can't seem to get the select2:select event working on dynamically added rows in a table using select2. The event only works on the original row. Unfortunately, I don't have any additional details about this issue. COD ...

The JSON data script is not functioning properly

Is this JSON formatted correctly? Why is it not displaying in the element with #id? I found a similar code snippet on https://www.sitepoint.com/colors-json-example/, copied and replaced the values but it's not functioning. Can anyone shed some light o ...

Tips for stopping Vue.js automatic merging of CSS classes

Recently, I embarked on my journey with Vue.js and have been thoroughly enjoying the experience. However, I've stumbled upon a challenge that has me stumped. Despite searching high and low and studying the documentation, I haven't found a solutio ...

Error loading console due to JSON data on website

After creating a Json data file named question.json with the following content: "Endocrinology":[ { "title":"Endocrinology", "id": "001", "date":"08J", "question":"In adult men, anterior pituitary insufficiency does not caus ...

Building an Angular 4 universal application using @angular/cli and integrating third-party libraries/components for compilation

While attempting to incorporate server side rendering using angular universal, I referenced a post on implementing an angular-4-universal-app-with-angular-cli and also looked at the cli-universal-demo project. However, I ran into the following issue: Upon ...

Utilizing stored data to display multiple markers on Google Maps with Node, MongoDB, Express, and EJS

I've been working on a project that involves passing values from mongodb to the google maps script in order to display multiple markers. While I've been able to load the map successfully, I'm facing difficulties defining my customers and loo ...

When triggering the fireEvent.mouseOver event, it seems that document.createRange is not a valid

Having trouble using fireClick.mouseOver(tab) to test tooltip functionality on tab hover. Here's a snippet of the code: it('should handle change on hover of tab', () => { const {getByTestId, getByRole} = renderComponent('Dra ...

Automatically copy any chosen selection on the page to the clipboard

Is there a way to copy any selection from a webpage to the clipboard, regardless of where it is located on the page (div, text input, password input, span, etc.)? I have created a function that can retrieve the selected text, but I am struggling with sett ...

Saving JSON data into an HTML element using Handlebars templating

Is there a way to save the entire JSON object within an HTML element as a data attribute? let a = {name : "sample", age : "34"} $.find('#someDiv').data('adata', a); Is it possible to achieve the same result using Handlebars when creat ...

When the jquery loop fails to function

I need to dynamically add a class to a specific tag using jQuery depending on an if condition. Here's the code snippet: if ($(".asp:contains('Home')")) { $("ul.nav-pills a:contains('Home')"). parent().addClass('active ...