I work with d3js (v4) and I'd like to fill a specific area defined by multiple paths.
Here is my shape:
And here is my code:
var width = 400,
height = 350;
var svg = d3.select('#svgContainer')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', 'white');
var shape = {
leftEdge: [],
topCurve: [],
supportJunction: [],
bottomCurve: [],
centralSupport: [],
width: 0
};
/* Set path data */
var startX = 200,
startY = 80,
centralSupportW = 10,
centralSupportH = 25;
shape.centralSupport.push({
x: startX - centralSupportW,
y: startY - centralSupportH
});
shape.centralSupport.push({
x: startX + centralSupportW,
y: startY - centralSupportH
});
shape.centralSupport.push({
x: startX + centralSupportW,
y: startY + centralSupportH
});
shape.centralSupport.push({
x: startX - centralSupportW,
y: startY + centralSupportH
});
var shapeW = 80,
shapeH = 60,
curve = 40,
intensity = 6;
shape.leftEdge.push({
x: startX - shapeW + curve,
y: startY + shapeH,
id: 1
});
shape.leftEdge.push({
x: startX - shapeW + curve / intensity,
y: startY + shapeH,
id: 2
});
shape.leftEdge.push({
x: startX - shapeW,
y: startY,
id: 3
});
shape.leftEdge.push({
x: startX - shapeW + curve / intensity,
y: startY - shapeH,
id: 4
});
shape.leftEdge.push({
x: startX - shapeW + curve,
y: startY - shapeH,
id: 5
});
var topCurveIntensity = 10;
var centralPosition = 5;
shape.topCurve.push({
x: shape.leftEdge[shape.leftEdge.length - 1].x,
y: shape.leftEdge[shape.leftEdge.length - 1].y,
id: 6
});
shape.topCurve.push({
x: shape.leftEdge[shape.leftEdge.length - 1].x - topCurveIntensity,
y: shape.centralSupport[0].y + centralPosition,
id: 7
});
shape.topCurve.push({
x: shape.centralSupport[0].x,
y: shape.centralSupport[0].y + centralPosition,
id: 8
});
shape.supportJunction.push({
x: shape.centralSupport[0].x,
y: shape.centralSupport[0].y + centralPosition,
id: 9
});
shape.supportJunction.push({
x: shape.centralSupport[shape.centralSupport.length - 1].x,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 10
});
shape.bottomCurve.push({
x: shape.centralSupport[shape.centralSupport.length - 1].x,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 11
});
shape.bottomCurve.push({
x: shape.leftEdge[0].x - topCurveIntensity,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 12
});
shape.bottomCurve.push({
x: shape.leftEdge[0].x,
y: shape.leftEdge[0].y,
id: 13
});
/* */
/* draw paths */
var regularLine = d3.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
var curvedLine = d3.line()
.curve(d3.curveBundle)
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
var closedLine = d3.line()
.curve(d3.curveLinearClosed)
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
/*
svg.append('path')
.datum(shape.centralSupport)
.attr('d', closedLine)
.style('fill', 'none')
.style('stroke', 'black')
.style('stroke-width', '3px');
*/
svg.append('path')
.datum(shape.leftEdge)
.attr('d', curvedLine)
.style('fill', 'none')
.style('stroke', '#A0A3A5')
.style('stroke-width', '3px');
svg.append('path')
.datum(shape.topCurve)
.attr('d', curvedLine)
.style('fill', 'none')
.style('stroke', '#A0A3A5')
.style('stroke-width', '3px');
svg.append('path')
.datum(shape.bottomCurve)
.attr('d', curvedLine)
.style('fill', 'none')
.style('stroke', '#A0A3A5')
.style('stroke-width', '3px');
svg.append('path')
.datum(shape.supportJunction)
.attr('d', regularLine)
.style('fill', 'none')
.style('stroke', '#A0A3A5')
.style('stroke-width', '3px');
var data = '';
svg.selectAll('path')
.each(function() {
data += d3.select(this).attr('d');
});
svg.append('path')
.attr('d', data)
.style('fill', 'lightgray')
.style('stroke', 'black')
.style('stroke-width', '1px')
.style('fill-rule', 'evenodd');
/* */
/* draw points used */
svg.selectAll('circles')
.data(shape.leftEdge)
.enter()
.append('circle')
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('r', 3)
.style('fill', 'black');
svg.selectAll('circles')
.data(shape.topCurve)
.enter()
.append('circle')
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('r', 3)
.style('fill', 'blue');
svg.selectAll('circles')
.data(shape.bottomCurve)
.enter()
.append('circle')
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('r', 3)
.style('fill', 'red');
/* */
/* draw labels */
svg.selectAll('circles')
.data(shape.leftEdge)
.enter()
.append('text')
.attr('x', function(d) {
return d.x - 16;
})
.attr('y', function(d) {
return d.y + 10;
})
.attr('dy', 1)
.text(function(d) {
return d.id;
});
svg.selectAll('circles')
.data(shape.topCurve)
.enter()
.append('text')
.attr('x', function(d) {
return d.x;
})
.attr('y', function(d) {
return d.y - 8;
})
.attr('dy', 1)
.text(function(d) {
return d.id;
})
.style('fill', 'blue');
svg.selectAll('circles')
.data(shape.supportJunction)
.enter()
.append('text')
.attr('x', function(d) {
return d.x + 16;
})
.attr('y', function(d) {
return d.y - 8;
})
.attr('dy', 1)
.text(function(d) {
return d.id;
})
.style('fill', 'purple');
svg.selectAll('circles')
.data(shape.bottomCurve)
.enter()
.append('text')
.attr('x', function(d) {
return d.x - 16;
})
.attr('y', function(d) {
return d.y - 8;
})
.attr('dy', 1)
.text(function(d) {
return d.id;
})
.style('fill', 'red');
/* */
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="svgContainer"></div>
Note: this snippet uses some random values to generate the shape (to reflect what I really need).
Each path is defined by an array of coordinates. Each of them used different curved or linear lines.
This is the code for a single path:
g.append('path')
.datum(foobar)
.attr('d', d3.line()
.curve(d3.curveBundle)
.x(function (d) { return d.x; })
.y(function (d) { return d.y; }))
.style('fill', 'none')
.style('stroke', 'purple')
.style('stroke-width', '3px');
I tried to concatenate the data of each path but the filling isn't properly done:
var data = '';
svg.selectAll('path')
.each(function () { data += d3.select(this).attr('d'); });
svg.append('path')
.attr('d', data)
.style('fill', 'red')
.style('stroke', 'black')
.style('stroke-width', '3px');
Below the shape with all the points used to draw the paths (one color per path). Some of them are shared between paths (1 13, 5 6, 8 9, 10 11)
Do you have any idea how I could fill the contain (i. e. inner section) of these paths?
Open the SVG file in Inkscape (Free software, cross platform https://inkscape.org) Select the paths to merge. From the Path menu, choose Union. Save the file.
The fill-rule attribute is a presentation attribute defining the algorithm to use to determine the inside part of a shape. Note: As a presentation attribute, fill-rule can be used as a CSS property. You can use this attribute with the following SVG elements: <altGlyph> <path>
A path is defined in SVG using the 'path' element. The basic shapes are all described in terms of what their equivalent path is, which is what their shape is as a path. (The equivalent path of a 'path' element is simply the path itself.)
The idea consists in concatenating sub-path's d
attributes into one:
Here is the demo:
var width = 400,
height = 350;
var svg = d3.select('#svgContainer')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', 'white');
var shape = {
leftEdge: [],
topCurve: [],
supportJunction: [],
bottomCurve: [],
centralSupport: [],
width: 0
};
/* Set path data */
var startX = 200,
startY = 80,
centralSupportW = 10,
centralSupportH = 25;
shape.centralSupport.push({
x: startX - centralSupportW,
y: startY - centralSupportH
});
shape.centralSupport.push({
x: startX + centralSupportW,
y: startY - centralSupportH
});
shape.centralSupport.push({
x: startX + centralSupportW,
y: startY + centralSupportH
});
shape.centralSupport.push({
x: startX - centralSupportW,
y: startY + centralSupportH
});
var shapeW = 80,
shapeH = 60,
curve = 40,
intensity = 6;
shape.leftEdge.push({
x: startX - shapeW + curve,
y: startY + shapeH,
id: 1
});
shape.leftEdge.push({
x: startX - shapeW + curve / intensity,
y: startY + shapeH,
id: 2
});
shape.leftEdge.push({
x: startX - shapeW,
y: startY,
id: 3
});
shape.leftEdge.push({
x: startX - shapeW + curve / intensity,
y: startY - shapeH,
id: 4
});
shape.leftEdge.push({
x: startX - shapeW + curve,
y: startY - shapeH,
id: 5
});
var topCurveIntensity = 10;
var centralPosition = 5;
shape.topCurve.push({
x: shape.leftEdge[shape.leftEdge.length - 1].x,
y: shape.leftEdge[shape.leftEdge.length - 1].y,
id: 6
});
shape.topCurve.push({
x: shape.leftEdge[shape.leftEdge.length - 1].x - topCurveIntensity,
y: shape.centralSupport[0].y + centralPosition,
id: 7
});
shape.topCurve.push({
x: shape.centralSupport[0].x,
y: shape.centralSupport[0].y + centralPosition,
id: 8
});
shape.supportJunction.push({
x: shape.centralSupport[0].x,
y: shape.centralSupport[0].y + centralPosition,
id: 9
});
shape.supportJunction.push({
x: shape.centralSupport[shape.centralSupport.length - 1].x,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 10
});
shape.bottomCurve.push({
x: shape.centralSupport[shape.centralSupport.length - 1].x,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 11
});
shape.bottomCurve.push({
x: shape.leftEdge[0].x - topCurveIntensity,
y: shape.centralSupport[shape.centralSupport.length - 1].y - centralPosition,
id: 12
});
shape.bottomCurve.push({
x: shape.leftEdge[0].x,
y: shape.leftEdge[0].y,
id: 13
});
/* draw paths */
var regularLine = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var curvedLine = d3.line()
.curve(d3.curveBundle)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var closedLine = d3.line()
.curve(d3.curveLinearClosed)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
svg.append('path')
.datum(shape.leftEdge)
.attr('d', curvedLine)
.attr("id", "left_edge")
.style('fill', 'none')
.style('stroke', 'grey')
.style('stroke-width', '2px');
svg.append('path')
.datum(shape.topCurve)
.attr('d', curvedLine)
.attr("id", "top_curve")
.style('fill', 'none')
.style('stroke', 'grey')
.style('stroke-width', '2px');
svg.append('path')
.datum(shape.bottomCurve)
.attr('d', curvedLine)
.attr("id", "bottom_curve")
.style('fill', 'none')
.style('stroke', 'grey')
.style('stroke-width', '2px');
svg.append('path')
.datum(shape.supportJunction)
.attr('d', regularLine)
.attr("id", "support_junction")
.style('fill', 'none')
.style('stroke', 'grey')
.style('stroke-width', '1px');
var leftEdge = svg.select("#left_edge")
var topCurve = svg.select("#top_curve");
var supportJunction = svg.select("#support_junction");
var curvedLine = svg.select("#bottom_curve");
// Let's merge all paths together by replacing left edge's d attribute (path)
// with the concatenation of the different sub-paths:
leftEdge
.attr(
"d",
[
leftEdge.attr("d"),
topCurve.attr("d").replace("M160,20", ""),
supportJunction.attr("d"),
curvedLine.attr("d").replace("M190,100", "")
].join(" ")
)
.attr("id", "full_shape") // let's rename this shape (as it's not the left edge anymore)
.style("fill", "lightgray"); // and let's finally fill the shape (our goal!)
// Let's remove the initial sub-sections of the shape as they are not needed anymore:
topCurve.exit().remove();
curvedLine.exit().remove();
supportJunction.exit().remove();
/* draw points used */
svg.selectAll('circles')
.data(shape.leftEdge)
.enter()
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3)
.style('fill', 'black');
svg.selectAll('circles')
.data(shape.topCurve)
.enter()
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3)
.style('fill', 'blue');
svg.selectAll('circles')
.data(shape.bottomCurve)
.enter()
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3)
.style('fill', 'red');
/* draw labels */
svg.selectAll('circles')
.data(shape.leftEdge)
.enter()
.append('text')
.attr('x', function(d) { return d.x - 16; })
.attr('y', function(d) { return d.y + 10; })
.attr('dy', 1)
.text(function(d) { return d.id; });
svg.selectAll('circles')
.data(shape.topCurve)
.enter()
.append('text')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y - 8; })
.attr('dy', 1)
.text(function(d) { return d.id; })
.style('fill', 'blue');
svg.selectAll('circles')
.data(shape.supportJunction)
.enter()
.append('text')
.attr('x', function(d) { return d.x + 16; })
.attr('y', function(d) { return d.y - 8; })
.attr('dy', 1)
.text(function(d) { return d.id; })
.style('fill', 'purple');
svg.selectAll('circles')
.data(shape.bottomCurve)
.enter()
.append('text')
.attr('x', function(d) { return d.x - 16; })
.attr('y', function(d) { return d.y - 8; })
.attr('dy', 1)
.text(function(d) { return d.id; })
.style('fill', 'red');
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="svgContainer"></div>
And here is the section of interest:
var leftEdge = svg.select("#left_edge");
var topCurve = svg.select("#top_curve");
var supportJunction = svg.select("#support_junction");
var curvedLine = svg.select("#bottom_curve");
// Let's merge all paths together by replacing left edge's d attribute (path)
// with the concatenation of the different sub-paths:
leftEdge
.attr(
"d",
leftEdge.attr("d") + " " +
topCurve.attr("d").replace("M160,20", "") + " " +
supportJunction.attr("d") + " " +
curvedLine.attr("d").replace("M190,100", "")
)
.attr("id", "full_shape") // let's rename this shape (as it's not the left edge anymore)
.style("fill", "lightgray"); // and let's finally fill the shape (our goal!)
// Let's remove the initial sub-sections of the shape as they are not needed anymore:
topCurve.exit().remove();
curvedLine.exit().remove();
supportJunction.exit().remove();
Let's detail a bit more what we're actually doing:
After giving an id to each sub-path when creating them (.attr("id", "#left_edge")
), we can then easily select them and retrieve their d
attributes produced by d3:
svg.select("#left_edge").attr("d")
svg.select("#top_curve").attr("d")
svg.select("#support_junction").attr("d")
svg.select("#bottom_curve").attr("d")
which gives us these 4 sub-paths:
M160,140L155.2777777777778,139.25C150.55555555555557,138.5,141.11111111111111,137,135.44444444444446,127C129.7777777777778,117,127.8888888888889,98.5,127.88888888888891,80C127.8888888888889,61.5,129.7777777777778,43,135.44444444444446,33C141.11111111111111,23,150.55555555555557,21.5,155.2777777777778,20.75L160,20
M160,20L158.95833333333334,26.166666666666668C157.91666666666666,32.333333333333336,155.83333333333334,44.666666666666664,160.83333333333334,51.333333333333336C165.83333333333334,58,177.91666666666666,59,183.95833333333334,59.5L190,60
M190,60L190,100
M190,100L183.95833333333334,100.5C177.91666666666666,101,165.83333333333334,102,160.83333333333334,108.66666666666667C155.83333333333334,115.33333333333333,157.91666666666666,127.66666666666667,158.95833333333334,133.83333333333334L160,140
With these svg sub-paths, we can now create a new path which is the concatenation of these 4 sub-paths by joining them with " "
:
var wholeShapePath =
[
leftEdge.attr("d"),
topCurve.attr("d").replace("M160,20", ""),
supportJunction.attr("d"),
curvedLine.attr("d").replace("M190,100", "")
].join(" ");
Notice how I remove the "moveTo" M
commands (such as M160,20
) from the beginning of sub-paths, as it has the effect of starting a new sub-path (which is why your concatenation didn't work).
With this new path representing the whole shape, we can now apply it by modifying one of the sub-paths to use the whole shape's path instead (and by the way filling the inner area!):
svg.select("#left_edge")
.attr("d", wholeShapePath)
.style("fill", "lightgray");
Without forgetting to discard the other sub-paths which are not usefull anymore:
topCurve.exit().remove();
curvedLine.exit().remove();
supportJunction.exit().remove();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With