我有一个水平图表 - 它具有特定的样式 - 它的两侧像条形一样弯曲 - 但它的边缘是直的 - 我有一些代码和示例 - 但我需要调整图表以确保其关闭到设计
https://jsfiddle.net/cjd3s6r7/31/
类似于剪辑路径的东西是一个好方法 - 但它不适用于所有浏览器
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 的形状?
路径解决方案(如果你真的不喜欢 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>