d3js 水平图表,带有圆角/硬边边界

问题描述 投票:0回答:1

我有一个水平图表 - 它具有特定的样式 - 它的两侧像条形一样弯曲 - 但它的边缘是直的 - 我有一些代码和示例 - 但我需要调整图表以确保其关闭到设计

https://jsfiddle.net/cjd3s6r7/31/

仅在 svg 的一侧有圆角

类似于剪辑路径的东西是一个好方法 - 但它不适用于所有浏览器

rect {
  clip-path: inset(0px 0px 0px 25px);
}
<svg>
  <rect width=200 height=100 rx=25 />
</svg>

但是这种使 svg 变得特定的方法怎么样

这可以适应图表代码吗?

function createRectanglePath(props) {
  const {
    width,
    height,
    topLeftRadius,
    topRightRadius,
    bottomRightRadius,
    bottomLeftRadius,
  } = props;

  return [
    // Move to the start point, considering top left radius
    `M ${topLeftRadius} 0`,
    // Draw a horizontal line to the top right corner, considering top right radius
    `H ${width - topRightRadius}`,
    // Draw an arc for top right corner if radius is greater than 0
    topRightRadius > 0
      ? `A ${topRightRadius} ${topRightRadius} 0 0 1 ${width} ${topRightRadius}`
      : null,
    // Draw a vertical line to the bottom right corner, considering bottom right radius
    `V ${height - bottomRightRadius}`,
    // Draw an arc for bottom right corner if radius is greater than 0
    bottomRightRadius > 0
      ? `A ${bottomRightRadius} ${bottomRightRadius} 0 0 1 ${
          width - bottomRightRadius
        } ${height}`
      : null,
    // Draw a horizontal line to the bottom left corner, considering bottom left radius
    `H ${bottomLeftRadius}`,
    // Draw an arc for bottom left corner if radius is greater than 0
    bottomLeftRadius > 0
      ? `A ${bottomLeftRadius} ${bottomLeftRadius} 0 0 1 0 ${
          height - bottomLeftRadius
        }`
      : null,
    // Draw a vertical line to the top left corner, considering top left radius
    `V ${topLeftRadius}`,
    // Draw an arc for top left corner if radius is greater than 0
    topLeftRadius > 0
      ? `A ${topLeftRadius} ${topLeftRadius} 0 0 1 ${topLeftRadius} 0`
      : null,
    // Close the path
    'Z',
  ]
    .filter((v) => v != null)
    .join(' ');
}


const node = {
  width: 512,
  height: 512,
  bottomLeftRadius: 10,
  bottomRightRadius: 50,
  topLeftRadius: 100,
  topRightRadius: 200
};

const svgPath = createRectanglePath(node);



document.getElementById('yourSVGPath').setAttribute('d', svgPath);

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  <path id="yourSVGPath" fill="#4EDFA5">
</svg>

<html>
    <head>
        <title>multibar d3</title>
        <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
        <style>
            
        </style>
    </head>
    <body>



    <script>
 
        
        d3.select("body")
            .selectAll('svg')
            .remove();

        var data = [{"label": "Toblerone", "value": 70}
    ];

        var w = parseInt(280, 10),
            h = parseInt(80, 10);


        var color = d3.scaleOrdinal().range(['#046395', '#d62828', '#f77f00', '#fcbf49', '#eae2b7']);


        data.forEach(function(d) {
            d.total = +d.value;
        });


        var margin = {top: 20, right: 10, bottom: 20, left: 20};

        var width = w - margin.left - margin.right;
        var height = h - margin.top - margin.bottom;

    
        var x = d3.scaleLinear()
                  .range([0, width]);
        var y = d3.scaleBand()
                  .range([height, 0])
                  .padding(0.1);

        x.domain([0, 100])
        y.domain(data.map(function(d) { return d.total; }));

        var svg = d3.select("body")
            .append("svg")
          .style('background','#5902f6')
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr('class', 'barchart')
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var border =4;
        var bars = svg.append('g').attr('class', 'bars');
   
      bars.selectAll(".bg")
            .data(data)
            .enter().append("rect")
            .attr("class", "bg")
      .attr('rx',15)
      .attr('ry',15)
            .attr('fill','#5902f6')
      .attr('stroke','white')
      .attr('stroke-width',border-1)
            .attr("width",x(100))
            .attr("y", function(d) { return y(d.value); })
            .attr("height", y.bandwidth());
      ;
        bars.selectAll(".bar")
            .data(data)
            .enter().append("rect")
            .attr("class", "bar")
            .attr('fill', '#2cc2d3')
      .attr('rx',15)
      .attr('ry',15)
            .attr("width", function(d) { return x(d.total); })
            .attr("y", function(d) { return y(d.value); })
            .attr("height", y.bandwidth());
   

        bars.selectAll(".label")
            .data(data)
            .enter().append("text")
            .attr("class", "label")
            .attr('text-anchor', function(d) {
              return d.total <= 20 ? 'start' : 'end';
            })
            .attr("x", function(d) {
              return d.total <= 20 ? x(d.total) + 20 : x(d.total) - 20;
            })
            .attr("y", function(d) {
              return y(d.value) + y.bandwidth() / 2;
            })
            .attr("dy", 5)
            .text(function(d) {
              return d.label;
            });

        bars.append("g")
            .attr("transform", "translate(0," + height + ")")
            .call(d3.axisBottom(x));

        bars.append("g")
            .call(d3.axisLeft(y));



    </script>



    </body>
</html>

我认为根据低于 100% 的值 - 右侧的边缘应保持平坦 - 但左侧的边缘为圆形 - 我们如何创建一个算法来正确处理图表的路径/svg 的形状?

javascript css d3.js
1个回答
0
投票

路径解决方案(如果你真的不喜欢 SVG 剪辑路径)

创建 SVG

path
d
属性的函数可能如下所示:

/** Creates the path with rounded corners

Takes the width and height of bar from scoped variables.
x() is the d3 linear scale.
*/
const createBarPath = function(percentage) {
    // this is the radius of the corners
    const halfHeight = barHeight / 2;

    // the x coordinate in which the bar ends (right coordinate)
    const endX = x(percentage);

    // move to initial position
    let path = `M0 ${halfHeight}`;

    // check if the bar width is below the left corner radius
    if (endX < halfHeight) {
        // use some good ole trig to get the end coordinate of the circle
        const xRelative = (halfHeight - endX) / halfHeight;
        const yRelative = Math.sin(Math.acos(xRelative));
        const yOffset = halfHeight * yRelative;

        // create the cut-off circle
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${endX} ${halfHeight - yOffset}`;
        path += `V ${halfHeight + yOffset}`;
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${0} ${halfHeight}`;

        path += 'Z';

        return path;
    }

    // if we get here, top and bottom LEFT corners of the bar are complete
    // complete top-left corner
    path += `A ${halfHeight} ${halfHeight} 0 0 1 ${halfHeight} 0`;

    // check if we even make it to the RIGHT corners
    if (endX < (barWidth - halfHeight)) {
        // top horizontal line
        path += `H ${endX}`;
        // straight vertical line
        path += `V ${barHeight}`;

    } else {
        // the full horizontal line
        path += `H ${barWidth - halfHeight}`;

        // check if the right corners are complete
        if (endX < barWidth) {
            // again trig
            const xRelative = (endX - (barWidth - halfHeight)) / halfHeight;
            const yRelative = Math.sin(Math.acos(xRelative));
            const yOffset = halfHeight * yRelative;

            // draw the partial right corners
            path += `A ${halfHeight} ${halfHeight} 0 0 1 ${endX} ${halfHeight - yOffset}`;
            path += `V ${halfHeight + yOffset}`;
            path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth - halfHeight} ${barHeight}`;

        } else {
            // draw full corners
            path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth} ${halfHeight}`;
            path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth - halfHeight} ${barHeight}`;
        }
    }

    // back to the left corners
    path += `H ${halfHeight}`;
    // complete bottom-left corner
    path += `A ${halfHeight} ${halfHeight} 0 0 1 0 ${halfHeight}`;

    path += 'Z';

    return path;
};

这将生成圆角条,其中拐角针对低百分比和高百分比被“切除”。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    svg path.value-bar {
      stroke: none;
      fill: #046395;
    }

  </style>
</head>
<body>
<div class="circles"></div>

<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script>

  d3.select("body")
    .selectAll('svg')
    .remove();

  const w = 280;
    h = 40;


  // margins for a single bar
  const barMargin = {top: 20, right: 10, bottom: 20, left: 20};
  // the width of a single bar
  const barWidth = w - barMargin.left - barMargin.right;
  // the height of the bar
  const barHeight = h;

  const svgTotalHeight = barMargin.top + 4 *(barHeight + barMargin.bottom);

  const x = d3.scaleLinear()
    .range([0, barWidth])
    .domain([0, 100]);

  const svg = d3.select("body")
    .append("svg")
    .attr("width", barWidth + barMargin.left + barMargin.right)
    .attr("height", svgTotalHeight);


  /** creates the path attributes for the given data item
   *
   * Takes the width and height of the bar from scoped variables.
   *
   * @param percentage [0, 100] percentage value to be drawn
   * @returns the path string
   */
  const createBarPath = function(percentage) {
    // this is the radius of the corners
    const halfHeight = barHeight / 2;

    // the x coordinate in which the bar ends (right coordinate)
    const endX = x(percentage);

    // move to initial position
    let path = `M0 ${halfHeight}`;

    // check if the bar width is below the left corner radius
    if (endX < halfHeight) {
      // use some good ole trig to get the end coordinate of the circle
      const xRelative = (halfHeight - endX) / halfHeight;
      const yRelative = Math.sin(Math.acos(xRelative));
      const yOffset = halfHeight * yRelative;

      // create the cut-off circle
      path += `A ${halfHeight} ${halfHeight} 0 0 1 ${endX} ${halfHeight - yOffset}`;
      path += `V ${halfHeight + yOffset}`;
      path += `A ${halfHeight} ${halfHeight} 0 0 1 ${0} ${halfHeight}`;

      path += 'Z';

      return path;
    }

    // if we get here, top and bottom LEFT corners of the bar are complete
    // complete top-left corner
    path += `A ${halfHeight} ${halfHeight} 0 0 1 ${halfHeight} 0`;

    // check if we even make it to the RIGHT corners
    if (endX < (barWidth - halfHeight)) {
      // top horizontal line
      path += `H ${endX}`;
      // straight vertical line
      path += `V ${barHeight}`;

    } else {
      // the full horizontal line
      path += `H ${barWidth - halfHeight}`;

      // check if the right corners are complete
      if (endX < barWidth) {
        // again trig
        const xRelative = (endX - (barWidth - halfHeight)) / halfHeight;
        const yRelative = Math.sin(Math.acos(xRelative));
        const yOffset = halfHeight * yRelative;

        // draw the partial right corners
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${endX} ${halfHeight - yOffset}`;
        path += `V ${halfHeight + yOffset}`;
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth - halfHeight} ${barHeight}`;

      } else {
        // draw full corners
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth} ${halfHeight}`;
        path += `A ${halfHeight} ${halfHeight} 0 0 1 ${barWidth - halfHeight} ${barHeight}`;
      }
    }

    // back to the left corners
    path += `H ${halfHeight}`;
    // complete bottom-left corner
    path += `A ${halfHeight} ${halfHeight} 0 0 1 0 ${halfHeight}`;

    path += 'Z';

    return path;
  };


  // draw the bar as a rect
  svg.append('path')
    .attr('class', 'value-bar')
    // start top corner
    .attr('d', createBarPath(50));

  svg.append('path')
    .attr('class', 'value-bar')
    .attr('transform' , `translate(0 ${1 * (barHeight + barMargin.top)})`)
    // start top corner
    .attr('d', createBarPath(97));

  svg.append('path')
    .attr('class', 'value-bar')
    .attr('transform' , `translate(0 ${2 * (barHeight + barMargin.top)})`)
    // start top corner
    .attr('d', createBarPath(3));

  const animatedBar = svg.append('path')
    .attr('class', 'value-bar')
    .attr('transform' , `translate(0 ${3 * (barHeight + barMargin.top)})`)
    // start top corner
    .attr('d', createBarPath(0));

  const dt = 1000/30;
  let t = 0;

  setInterval(function() {
    t += dt / 1000;
    // animate the percentage
    const percentage = 50 + 50 * Math.sin(t);
    animatedBar.attr('d', createBarPath(percentage));

  }, dt)


</script>
</body>
</html>

SVG 剪辑路径解决方案

如果您不喜欢 CSS

clip-paths
,您可以使用 SVG-native
clip-paths
来代替。

<svg>
    <defs>
        <clipPath id="rounded-bar">
            <rect x="0" y="0" width="250" height="40" rx="20"></rect> 
        </clipPath>
    </defs>
    <!-- all your bars -->
</svg>

我觉得这是最优雅的解决方案。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    body {
      font-family: 'Open Sans', sans-serif;
    }
    svg g.barchart g.bar rect.frame {
      fill: none;
      stroke: #ddd;
      stroke-width: 4;
    }
    svg g.barchart g.bar rect.value-bar {
      stroke: none;
      fill: #046395;
    }
    svg g.barchart g.bar text.label {
      font-weight: bold;
      fill: #444;
    }

  </style>
</head>
<body>
<div class="circles"></div>

<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script>

  d3.select("body")
    .selectAll('svg')
    .remove();

  var data = [
    {"label": "Toblerone", "value": 55},
    {"label": "Lindt", "value": 100},
    {"label": "Snickers", "value": 25},
    {"label": "Laderach", "value": 97},
    {"label": "Hersheys", "value": 2}
  ];

  const w = 280;
    h = 40;


  const color = d3.scaleOrdinal().range(['#046395', '#d62828', '#f77f00', '#fcbf49', '#eae2b7']);


  data.forEach(function(d) {
    d.total = +d.value;
  });


  // margins for a single bar
  const barMargin = {top: 20, right: 10, bottom: 20, left: 20};
  // the width of a single bar
  const barWidth = w - barMargin.left - barMargin.right;
  // the height of the bar
  const barHeight = h;

  const svgTotalHeight = barMargin.top + data.length * (barHeight + barMargin.bottom);

  const x = d3.scaleLinear()
    .range([0, barWidth])
    .domain([0, 100]);

  const svg = d3.select("body")
    .append("svg")
    .attr("width", barWidth + barMargin.left + barMargin.right)
    .attr("height", svgTotalHeight);

  // add the clip-path definition
  const defs = svg.append('defs');
  // the clip-path for the entire rect
  defs.append('clipPath')
    .attr('id', 'rounded-bar')
    .append('rect')
    .attr('x', 0).attr('y', 0)
    .attr('width', barWidth)
    .attr('height', barHeight)
    .attr('rx', barHeight / 2);

  const barChartGroup = svg.append("g")
    .attr('class', 'barchart')
    .attr("transform", "translate(" + barMargin.left + "," + barMargin.top + ")");

  const bars = barChartGroup.append('g').attr('class', 'bars');

  // create a group for each bar - this will make it easier to translate the drawings for multiple charts
  const barGroups = bars.selectAll("g.bar")
    .data(data)
    .enter()
    .append('g')
    .attr('class', 'bar')
    // translate the group for multiple charts
    .attr('transform', function(d,i) {
      return `translate(0 ${i * (barHeight + /*margin*/ 10)})`;
    })

  // draw the bar as a rect
  barGroups.append('rect')
    .attr('class', 'value-bar')
    // add the clip-path of this
    .attr('clip-path', 'url(#rounded-bar)')
    // start top corner
    .attr('x', 0).attr('y', 0)
    .attr('height', barHeight)
    .attr('width', function(d) {
      return x(d.value);
    });

  // add the visual frame on top of the bar
  barGroups.append('rect')
    .attr('class', 'frame')
    .attr('x', 0).attr('y', 0)
    .attr('width', barWidth)
    .attr('height', barHeight)
    .attr('rx', barHeight / 2);

  // the label
  barGroups.append('text')
    .attr('class', 'label')
    .attr('text-anchor', 'middle')
    .attr('x', 0).attr('y', 0)
    .attr('dx', barWidth / 2)
    .attr('dy', barHeight / 1.6)
    .text(function(d) {
      return d.label;
    });


</script>
</body>
</html>

您甚至可以做一些奇特的事情并混合标签的颜色。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    body {
      font-family: 'Open Sans', sans-serif;
    }
    svg g.barchart g.bar rect.frame {
      fill: none;
      stroke: #ddd;
      stroke-width: 4;
    }
    svg g.barchart g.bar rect.value-bar {
      stroke: none;
      fill: #046395;
    }
    svg g.barchart g.bar text.label {
      font-weight: bold;
    }
    svg g.barchart g.bar text.label-left {
      fill: #FFF;
    }
    svg g.barchart g.bar text.label-right {
      fill: #046395;
    }

  </style>
</head>
<body>
<div class="circles"></div>

<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script>

  d3.select("body")
    .selectAll('svg')
    .remove();

  var data = [
    {"label": "Toblerone", "value": 55},
    {"label": "Lindt", "value": 100},
    {"label": "Snickers", "value": 25},
    {"label": "Laderach", "value": 97},
    {"label": "Hersheys", "value": 2}
  ];

  const w = 280;
    h = 40;


  const color = d3.scaleOrdinal().range(['#046395', '#d62828', '#f77f00', '#fcbf49', '#eae2b7']);


  data.forEach(function(d) {
    d.total = +d.value;
  });


  // margins for a single bar
  const barMargin = {top: 20, right: 10, bottom: 20, left: 20};
  // the width of a single bar
  const barWidth = w - barMargin.left - barMargin.right;
  // the height of the bar
  const barHeight = h;

  const svgTotalHeight = barMargin.top + data.length * (barHeight + barMargin.bottom);

  const x = d3.scaleLinear()
    .range([0, barWidth])
    .domain([0, 100]);

  const svg = d3.select("body")
    .append("svg")
    .attr("width", barWidth + barMargin.left + barMargin.right)
    .attr("height", svgTotalHeight);

  // add the clip-path definition
  const defs = svg.append('defs');
  // the clip-path for the entire rect
  defs.append('clipPath')
    .attr('id', 'rounded-bar')
    .append('rect')
    .attr('x', 0).attr('y', 0)
    .attr('width', barWidth)
    .attr('height', barHeight)
    .attr('rx', barHeight / 2);


  // add clip paths for the left part of the labels
  defs
    .selectAll('clipPath.label-left')
    .data(data)
    .enter()
    .append('clipPath')
    .attr('class', 'label-left')
    .attr('id', function(d,i) {
      return `clip-path-left-${i}`;
    })
    .append('rect')
    .attr('x', 0).attr('y', 0)
    .attr('width', function(d) {
      return x(d.value);
    })
    .attr('height', barHeight);

  // add clip paths for the right part of the labels
  defs
    .selectAll('clipPath.label-right')
    .data(data)
    .enter()
    .append('clipPath')
    .attr('class', 'label-right')
    .attr('id', function(d,i) {
      return `clip-path-right-${i}`;
    })
    .append('rect')
    .attr('x', function(d) {
      return x(d.value);
    })
    .attr('y', 0)
    .attr('width', function(d) {
      return barWidth - x(d.value);
    })
    .attr('height', barHeight);

  const barChartGroup = svg.append("g")
    .attr('class', 'barchart')
    .attr("transform", "translate(" + barMargin.left + "," + barMargin.top + ")");

  const bars = barChartGroup.append('g').attr('class', 'bars');

  // create a group for each bar - this will make it easier to translate the drawings for multiple charts
  const barGroups = bars.selectAll("g.bar")
    .data(data)
    .enter()
    .append('g')
    .attr('class', 'bar')
    // translate the group for multiple charts
    .attr('transform', function(d,i) {
      return `translate(0 ${i * (barHeight + /*margin*/ 10)})`;
    })

  // draw the bar as a rect
  barGroups.append('rect')
    .attr('class', 'value-bar')
    // add the clip-path of this
    .attr('clip-path', 'url(#rounded-bar)')
    // start top corner
    .attr('x', 0).attr('y', 0)
    .attr('height', barHeight)
    .attr('width', function(d) {
      return x(d.value);
    });

  // add the visual frame on top of the bar
  barGroups.append('rect')
    .attr('class', 'frame')
    .attr('x', 0).attr('y', 0)
    .attr('width', barWidth)
    .attr('height', barHeight)
    .attr('rx', barHeight / 2);

  // we add two labels for two different clip paths
  // the white label for on top of the bar
  barGroups.append('text')
    .attr('class', 'label label-left')
    .attr('text-anchor', 'middle')
    .attr('x', 0).attr('y', 0)
    .attr('dx', barWidth / 2)
    .attr('dy', barHeight / 1.6)
    .text(function(d) {
      return d.label;
    })
    .attr('clip-path', function(d,i) {
      return `url(#clip-path-left-${i})`;
    });
  // the blue label for to the right of the bar
  barGroups.append('text')
    .attr('class', 'label label-right')
    .attr('text-anchor', 'middle')
    .attr('x', 0).attr('y', 0)
    .attr('dx', barWidth / 2)
    .attr('dy', barHeight / 1.6)
    .text(function(d) {
      return d.label;
    })
    .attr('clip-path', function(d,i) {
      return `url(#clip-path-right-${i})`;
    });



</script>
</body>
</html>

© www.soinside.com 2019 - 2024. All rights reserved.