Я могу нарисовать сплайн с помощью d3 без проблем.
<html>
<head>
<script src = "https://d3js.org/d3.v7.min.js"></script>
<style>
.graph-container {
width: 100%;
height: 800px;
position: relative;
}
</style>
</head>
<body>
<div id = "graph-container" class = "graph-container"></div>
<script>
// Define the data points for the spline curve
const data = [
{ x: 0, y: 80, r: 12 },
{ x: 100, y: 100, r: 40 },
{ x: 200, y: 30, r: 6 },
{ x: 300, y: 50, r: 8 },
{ x: 400, y: 40, r: 10 },
{ x: 500, y: 80, r: 12 },
];
// Set up the SVG container
const svgWidth = 600;
const svgHeight = 200;
const svg = d3.select('#graph-container') // Select the graph-container div
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
// Create the spline generator
const line = d3.line()
.x((d) => d.x)
.y((d) => d.y)
// .curve(d3.curveMonotoneX);
.curve(d3.curveCardinal);
// Draw the spline curve
svg.append('path')
.datum(data)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'green')
.attr('stroke-width', (d) => d.r);
</script>
</body>
</html>
Моя проблема состоит в том, чтобы нарисовать каждый сегмент с разной толщиной (шириной штриха). Я хочу, чтобы каждый сегмент линии плавно переходил к следующей контрольной точке.
Кажется, что d3 .attr('stroke-width', (d) => d.r) не работает. Как я могу сделать это в React JS?
Спасибо,





Вот сплайн, построенный как аппроксимированная мультилиния:
const data = [
{ x: 0, y: 80, r: 12 },
{ x: 100, y: 100, r: 30 },
{ x: 200, y: 30, r: 6 },
{ x: 300, y: 50, r: 8 },
{ x: 400, y: 40, r: 10 },
{ x: 500, y: 80, r: 12 },
];
const rValueByX = (x) => {
if (x === data[0].x) {
return data[0].r;
}
const index = data.findIndex(item => item.x >= x);
const prevX = data[index - 1].x;
const nextX = data[index].x;
const delta = (x - prevX) / (nextX - prevX);
const prevR = data[index - 1].r;
const nextR = data[index].r;
return (nextR - prevR) * delta + prevR;
}
// Set up the SVG container
const svgWidth = 600;
const svgHeight = 150;
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
const line = d3.line()
.x((d) => d.x)
.y((d) => d.y)
.curve(d3.curveCardinal);
const path = svg.append('path')
.attr('d', line(data))
.attr('fill', 'none')
.attr('stroke', 'green');
const total = path.node().getTotalLength();
const step = total / 100;
for (let len = 0; len <= total; len += step) {
const fromLen = Math.max(0, len - step);
const toLen = Math.min(len + step, total);
const point = path.node().getPointAtLength(len);
const r = rValueByX(point.x);
const from = path.node().getPointAtLength(fromLen);
const to = path.node().getPointAtLength(toLen);
svg.append('line')
.attr('x1', from.x)
.attr('y1', from.y)
.attr('x2', to.x)
.attr('y2', to.y)
.attr('stroke-width', r)
.attr('stroke', 'green');
}<script src = "https://d3js.org/d3.v7.min.js"></script>
<div id = "graph-container"/>На основе rValueByX() помощника по интерполяции Майкла Ровински вы также можете нарисовать 2 сплайна и соединить их.
r непосредственно в yмы можем даже еще больше упростить сценарий, так как нам не нужны никакие интерполированные значения r для сплайна Эрмита.
let data = [
{ x: 0, y: 80, r: 12 },
{ x: 100, y: 100, r: 30 },
{ x: 200, y: 30, r: 6 },
{ x: 300, y: 50, r: 8 },
{ x: 400, y: 40, r: 10 },
{ x: 500, y: 80, r: 12 }
];
// Set up the SVG container
const svgWidth = 600;
const svgHeight = 150;
// last index
const dataL = data.length-1;
const svg = d3
.select("#graph-container")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
const lineTop = d3
.line()
.x( d => d.x)
.y( d => {
return d.y - d.r/ 2;
})
.curve(d3.curveCardinal);
const lineBottom = d3
.line()
.x((d, i) => data[dataL-i].x)
.y( (d,i) => data[dataL-i].y + data[dataL-i].r / 2)
.curve(d3.curveCardinal);
const lineMiddle= d3
.line()
.x( d => d.x)
.y( d => d.y )
.curve(d3.curveCardinal);
const path = svg
.append("path")
// concatenate paths
.attr("d", lineTop(data) + (lineBottom(data).replaceAll('M', 'L') +'z' ) )
.attr("fill", "green")
.attr("fill-opacity", "0.5");
const pathMiddle = svg
.append("path")
.attr("d", lineMiddle(data))
.attr("fill", "none")
.attr("stroke", "green");<script src = "https://d3js.org/d3.v7.min.js"></script>
<div id = "graph-container" />На самом деле мы рисуем верхний и нижний сплайны, используя смещение по оси Y в соответствии с текущим r радиусом штриха (как определено в значении объекта данных r).
const lineTop = d3
.line()
.x( d => d.x)
.y( d => {
return d.y - d.r/ 2;
})
.curve(d3.curveCardinal);
const lineBottom = d3
.line()
.x((d, i) => data[dataL-i].x)
.y( (d,i) => data[dataL-i].y + data[dataL-i].r / 2)
.curve(d3.curveCardinal);
Нижний сплайн должен изменить координаты данных. Затем мы можем объединить оба сплайна в один путь:
const path = svg
.append("path")
// concatenate paths
.attr("d", lineTop(data) + (lineBottom(data).replaceAll('M', 'L') +'z' ) )
.attr("fill", "green")
.attr("fill-opacity", "0.5");
Преимущество такого подхода — нам не нужно аппроксимировать форму множеством <line> элементов.
Стоит отметить: сотни вызовов getPointAtLength() могут значительно повлиять на производительность рендеринга.
Если вам не нужны все навороты d3.js, вы можете взглянуть на сообщение Милана Джи "Сплайн-интерполяция Catmull-Rom".
В следующем примере используется слегка измененная версия вспомогательной функции toCatmullRomBezier() Милана Г.
const data = [{
x: 0,
y: 80,
r: 12
},
{
x: 100,
y: 100,
r: 30
},
{
x: 200,
y: 30,
r: 6
},
{
x: 300,
y: 50,
r: 8
},
{
x: 400,
y: 40,
r: 10
},
{
x: 500,
y: 80,
r: 12
},
];
let options = {
decimals: 2
};
let dMiddle = toCatmullRomBezier(data, options);
pathMid.setAttribute('d', dMiddle)
let dStroke = drawStrokedCatmullSpline(data, options)
catmullPath.setAttribute('d', dStroke);
function drawStrokedCatmullSpline(data, options) {
let dataTop = [];
let dataBottom = [];
for (let i = 0; i < data.length; i++) {
let item = data[i];
let itemRev = data[data.length - 1 - i];
dataTop.push({
x: item.x,
y: item.y - item.r / 2
});
dataBottom.push({
x: itemRev.x,
y: itemRev.y + itemRev.r / 2
});
}
let lineTop = toCatmullRomBezier(dataTop, options)
let lineBottom = toCatmullRomBezier(dataBottom, options);
// concatenate paths
let dCombined = lineTop + lineBottom.replaceAll('M', 'L') + 'z';
return dCombined;
}
/**
* based on:
* https://observablehq.com/@milangress/catmull-rom-spline-interpolation
*/
function toCatmullRomBezier(coords = [], options = {}) {
// get options / set defaults
options = {
tension: options.tension ? options.tension : 0.5,
closing: options.closing ? options.closing : false,
decimals: options.decimals ? options.decimals : 3,
returnPathData: options.returnPathData ? options.returnPathData : false
}
let {
tension,
closing,
decimals,
returnPathData
} = options;
// normalize coords array or object
let points = [];
if (Array.isArray(coords[0])) {
for (let i = 0; i < coords.length; i += 2) {
points.push([coords[i], coords[i + 1]]);
}
} else {
for (let i = 0; i < coords.length; i++) {
points.push([coords[i].x, coords[i].y]);
}
}
// sets tension [0.0, 1.0] +/-
let tens = tension !== 0 ? tension * 12 : 0.5 * 12;
// duplicate First point to the end if the Path is not closed
const PointList = closing ? [...copyFirstPointToLast(points)] : [...points];
// Make sure Points have correct type
const floats = PointList.map((x) => x.map((x) => parseFloat(x)));
// Set starting point for SVG
const M = {
x: floats[0][0],
y: floats[0][1]
};
const firstMoveto = ["M" + floats[0][0] + " " + floats[0][1] + " "];
// Generate Point Matrix from points
const matrixPoints = floats
.map((point, i, arr) => {
if (i == 0) {
return getMatrix([arr[i], arr[i], arr[i + 1], arr[i + 2]]);
} else if (i == arr.length - 2) {
return getMatrix([arr[i - 1], arr[i], arr[i + 1], arr[i + 1]]);
} else {
return getMatrix([arr[i - 1], arr[i], arr[i + 1], arr[i + 2]]);
}
})
.filter((mx) => mx[3] !== undefined);
// some Matrix Multiplication for the Bezier points
const matrixMathToBezier = matrixPoints.map((p) => {
let points = [{
x: p[1].x,
y: p[1].y
},
{
x: (-p[0].x + tens * p[1].x + p[2].x) / tens,
y: (-p[0].y + tens * p[1].y + p[2].y) / tens
},
{
x: (p[1].x + tens * p[2].x - p[3].x) / tens,
y: (p[1].y + tens * p[2].y - p[3].y) / tens
},
{
x: p[2].x,
y: p[2].y
}
];
// round
for (let i = 0; i < points.length; i++) {
points[i].x = +points[i].x.toFixed(decimals);
points[i].y = +points[i].y.toFixed(decimals);
}
return points;
});
// collect pathdata
let pathData = [{
type: 'M',
values: [M.x, M.y]
}];
const toSVGNotation =
matrixMathToBezier.map((bp) => {
let d = ["C", bp[1].x, bp[1].y, bp[2].x, bp[2].y, bp[3].x, bp[3].y].join(' ');
pathData.push({
type: 'C',
values: [bp[1].x, bp[1].y, bp[2].x, bp[2].y, bp[3].x, bp[3].y]
})
return d;
});
// Add the Moveto comand and join to string
if (returnPathData) {
return pathData;
} else {
return firstMoveto.concat(toSVGNotation).join(" ");
}
// Functions:
// Seperate X and Y values from Point Array for clarity -> eg. [[x,y],[x,y],[x,y]]
function getMatrix(arr) {
return arr.map((p) => {
if (p !== undefined) {
return {
x: p[0],
y: p[1]
};
}
});
}
// Duplicate first element to the end
function copyFirstPointToLast(arr) {
return arr[0] !== arr[arr.length - 1] ? [...arr, arr[0]] : arr;
}
}svg {
border: 1px solid #ccc;
width: 100%;
display: inline-block;
}
#pathOut {
fill: none;
transform: translate(0, 50px);
stroke: red;
}
#catmullPath {
fill: none;
}<svg class = "svg" overflow = "visible" viewBox = "0 0 600 200">
<path id = "pathMid" stroke = "#ccc" stroke-width = "0.5" fill = "none" />
<path id = "catmullPath" d = "" fill = "" stroke = "red" stroke-width = "1" />
</svg>
Отлично! Да, кажется, что рисование мультилинии — лучшее приближение к тому, чего я хочу достичь. Большое спасибо.