使用 PDFBox,我创建了一个折线图来绘制一些数据,它看起来很像您通过谷歌搜索看到的任何通用折线图。它看起来也与我附加到这个问题的折线图相同。折线图绘制算法的工作方式是先查看当前点,然后查看下一个点,如果在那里找到有效点,则绘制一条线。
我的问题是客户不喜欢线路相互连接的尖锐程度。相反,他们希望线条之间的连接以更多弯曲的方式发生。附件是客户想要什么的粗略想法的图片。请注意,虽然线条看起来非常弯曲,但客户特别关心线条连接本身是否弯曲,而不是像标准折线图中那样尖锐。
到目前为止,我已经尝试使用贝塞尔曲线,但我似乎无法找到正确的值来使其针对点之间的所有不同幅度正确缩放。我首先尝试更改线帽和线连接样式,但这并没有在线连接之间产生所需的“弯曲度”。我也考虑过使用路径来实现这个结果,但我还没有弄清楚如何继续。
我是否遗漏了一些可以让绘制这些线条变得更容易的东西?如果没有,任何人都可以帮助我找出正确的贝塞尔值/路径来实现这些曲线吗?预先感谢您的任何建议/代码示例。
由于保密协议,我无法给出显示如何绘制和绘制图表的代码示例(这将完全放弃我们的算法)。我只能说,我创建了一个关于如何在图表中绘制数据的内部表示,并且该系统在提供的图像中进行了非常粗略的翻译。我可以说,在基于我们的内部坐标表示初始 moveTo 到起点位置之后,绘制数据的函数专门使用 PDPageContentStream 类的 lineTo 和 strokeTo 函数。
---一个快速的“解决方案”是使用圆线连接而不是斜接连接(默认)---看来我错过了这一点。
您的示例中的图表可能使用曲线插值,此问题和答案可能对您有帮助:Adobe Illustrator 中的折线简化如何工作?
下面的代码展示了如何将线列表转换为 Bezier 连接线(它是 C#,但可以通过最小的更改转换为 Java):
/// <summary>
/// Draws the Bezier connected lines on the page.
/// </summary>
/// <param name="page">Page where to draw the lines.</param>
/// <param name="points">List of points representing the connected lines.</param>
/// <param name="pen">Pen to draw the final path.</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="font"></param>
private static void DrawBezierConnectedLines(PDFPage page, PDFPoint[] points, PDFPen pen, double smoothFactor, PDFFont font)
{
PDFPath path = new PDFPath();
path.StartSubpath(points[0].X, points[0].Y);
for (int i = 0; i < points.Length - 2; i++)
{
PDFPoint[] pts = ComputeBezierConnectedLines(points[i], points[i + 1], points[i + 2], smoothFactor, i == 0, i == points.Length - 3);
switch (pts.Length)
{
case 2: // Intermediate/last section - straight lines
path.AddLineTo(pts[0].X, pts[0].Y);
path.AddLineTo(pts[1].X, pts[1].Y);
break;
case 3: // First section - straight lines
path.AddLineTo(pts[0].X, pts[0].Y);
path.AddLineTo(pts[1].X, pts[1].Y);
path.AddLineTo(pts[2].X, pts[2].Y);
break;
case 4: // Intermediate/last section
path.AddLineTo(pts[0].X, pts[0].Y);
path.AddBezierTo(pts[1].X, pts[1].Y, pts[1].X, pts[1].Y, pts[2].X, pts[2].Y);
path.AddLineTo(pts[3].X, pts[3].Y);
break;
case 5: // First section
path.AddLineTo(pts[0].X, pts[0].Y);
path.AddLineTo(pts[1].X, pts[1].Y);
path.AddBezierTo(pts[2].X, pts[2].Y, pts[2].X, pts[2].Y, pts[3].X, pts[3].Y);
path.AddLineTo(pts[4].X, pts[4].Y);
break;
}
}
page.Canvas.DrawPath(pen, path);
page.Canvas.DrawString($"Smooth factor = {smoothFactor}", font, new PDFBrush(), points[points.Length - 1].X, points[0].Y);
}
/// <summary>
/// Given a sequence of 3 consecutive points representing 2 connected lines the method computes the points required to display the new lines and the connecting curve.
/// </summary>
/// <param name="pt1">First point</param>
/// <param name="pt2">Second point</param>
/// <param name="pt3">Third point</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="isFirstSection">True if the points are the first 3 in the list of points</param>
/// <param name="isLastSection">True if the 3 points are last 3 in the list of points.</param>
/// <returns>A list of points representing the new lines and the connecting curve.</returns>
/// <remarks>The method returns 5 points if this is the first section, points that represent the first line, connecting curve and last line.
/// If this is not the first section the method returns 4 points representing the connecting curve and the last line.</remarks>
private static PDFPoint[] ComputeBezierConnectedLines(PDFPoint pt1, PDFPoint pt2, PDFPoint pt3, double smoothFactor, bool isFirstSection, bool isLastSection)
{
PDFPoint[] outputPoints = null;
if (smoothFactor > 0.5)
{
smoothFactor = 0.5; // Half line maximum
}
if (((pt1.X == pt2.X) && (pt2.X == pt3.X)) || // Vertical lines
((pt1.Y == pt2.Y) && (pt2.Y == pt3.Y)) || // Horizontal lines
(smoothFactor == 0))
{
if (!isFirstSection)
{
pt1 = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
}
if (!isLastSection)
{
pt3 = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
}
if (isFirstSection)
{
outputPoints = new PDFPoint[] { pt1, pt2, pt3 };
}
else
{
outputPoints = new PDFPoint[] { pt2, pt3 };
}
}
else
{
PDFPoint startPoint = new PDFPoint(pt1);
if (!isFirstSection)
{
startPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
}
PDFPoint firstIntermediaryPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, true);
PDFPoint secondIntermediaryPoint = new PDFPoint(pt2);
PDFPoint thirdIntermediaryPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, false);
PDFPoint endPoint = new PDFPoint(pt3);
if (!isLastSection)
{
endPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
}
if (isFirstSection)
{
outputPoints = new PDFPoint[] { startPoint, firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
}
else
{
outputPoints = new PDFPoint[] { firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
}
}
return outputPoints;
}
/// <summary>
/// Given the line from pt1 to pt2 the method computes an intermediary point on the line.
/// </summary>
/// <param name="pt1">Start point</param>
/// <param name="pt2">End point</param>
/// <param name="smoothFactor">Smooth factor specifying how from from the line end the intermediary point is located.</param>
/// <param name="isEndLocation">True if the intermediary point should be computed relative to end point,
/// false if the intermediary point should be computed relative to start point.</param>
/// <returns>A point on the line defined by pt1->pt2</returns>
private static PDFPoint ComputeIntermediatePoint(PDFPoint pt1, PDFPoint pt2, double smoothFactor, bool isEndLocation)
{
if (isEndLocation)
{
smoothFactor = 1 - smoothFactor;
}
PDFPoint intermediate = new PDFPoint();
if (pt1.X == pt2.X)
{
intermediate.X = pt1.X;
intermediate.Y = pt1.Y + (pt2.Y - pt1.Y) * smoothFactor;
}
else
{
intermediate.X = pt1.X + (pt2.X - pt1.X) * smoothFactor;
intermediate.Y = (intermediate.X * (pt2.Y - pt1.Y) + (pt2.X * pt1.Y - pt1.X * pt2.Y)) / (pt2.X - pt1.X);
}
return intermediate;
}
对于这组点:
PDFPoint[] points = new PDFPoint[] {
new PDFPoint(50, 150), new PDFPoint(100, 200), new PDFPoint(150, 50), new PDFPoint(200, 150), new PDFPoint(250, 50) };
DrawBezierConnectedLines(page, points, pen, 0, helvetica);
相应的PDF文件可以在这里下载: https://github.com/o2solutions/pdf4net/blob/master/GettingStarted/BezierConnectedLines/BezierConnectedLines.pdf
这是已接受答案的 PDFBox 转换
private static void drawBezierConnectedLines(PDPageContentStream contentStream,
PDFPoint[] points, double smoothFactor) throws Exception {
contentStream.moveTo(points[0].X, points[0].Y);
for (int i = 0; i < points.length - 2; i++)
{
PDFPoint[] pts = computeBezierConnectedLines(points[i], points[i + 1], points[i + 2],
smoothFactor, i == 0, i == points.length - 3);
switch (pts.length)
{
case 2: // Intermediate/last section - straight lines
contentStream.lineTo(pts[0].X, pts[0].Y);
contentStream.lineTo(pts[1].X, pts[1].Y);
break;
case 3: // First section - straight lines
contentStream.lineTo(pts[0].X, pts[0].Y);
contentStream.lineTo(pts[1].X, pts[1].Y);
contentStream.lineTo(pts[2].X, pts[2].Y);
break;
case 4: // Intermediate/last section
contentStream.lineTo(pts[0].X, pts[0].Y);
contentStream.curveTo(pts[1].X, pts[1].Y, pts[1].X, pts[1].Y, pts[2].X, pts[2].Y);
contentStream.lineTo(pts[3].X, pts[3].Y);
break;
case 5: // First section
contentStream.lineTo(pts[0].X, pts[0].Y);
contentStream.lineTo(pts[1].X, pts[1].Y);
contentStream.curveTo(pts[2].X, pts[2].Y, pts[2].X, pts[2].Y, pts[3].X, pts[3].Y);
contentStream.lineTo(pts[4].X, pts[4].Y);
break;
}
}
}
private static PDFPoint[] computeBezierConnectedLines(PDFPoint pt1, PDFPoint pt2, PDFPoint pt3,
double smoothFactor,
boolean isFirstSection, boolean isLastSection) {
PDFPoint[] outputPoints = null;
if (smoothFactor > 0.5) {
smoothFactor = 0.5; // Half line maximum
}
if (((pt1.X == pt2.X) && (pt2.X == pt3.X)) || // Vertical lines
((pt1.Y == pt2.Y) && (pt2.Y == pt3.Y)) || // Horizontal lines
(smoothFactor == 0)) {
if (!isFirstSection) {
pt1 = computeIntermediatePoint(pt1, pt2, smoothFactor, false);
} if (!isLastSection) {
pt3 = computeIntermediatePoint(pt2, pt3, smoothFactor, true);
} if (isFirstSection) {
outputPoints = new PDFPoint[] { pt1, pt2, pt3 };
} else {
outputPoints = new PDFPoint[] { pt2, pt3 };
}
} else {
PDFPoint startPoint = new PDFPoint(pt1);
if (!isFirstSection) {
startPoint = computeIntermediatePoint(pt1, pt2, smoothFactor, false);
}
PDFPoint firstIntermediaryPoint = computeIntermediatePoint(pt1, pt2, smoothFactor, true);
PDFPoint secondIntermediaryPoint = new PDFPoint(pt2);
PDFPoint thirdIntermediaryPoint = computeIntermediatePoint(pt2, pt3, smoothFactor, false);
PDFPoint endPoint = new PDFPoint(pt3);
if (!isLastSection) {
endPoint = computeIntermediatePoint(pt2, pt3, smoothFactor, true);
}
if (isFirstSection) {
outputPoints = new PDFPoint[] { startPoint, firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
} else {
outputPoints = new PDFPoint[] { firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
}
}
return outputPoints;
}
private static PDFPoint computeIntermediatePoint(PDFPoint pt1, PDFPoint pt2,
double smoothFactor, boolean isEndLocation)
{
if (isEndLocation) {
smoothFactor = 1 - smoothFactor;
}
PDFPoint intermediate = new PDFPoint();
if (pt1.X == pt2.X) {
intermediate.X = pt1.X;
intermediate.Y = (float) (pt1.Y + (pt2.Y - pt1.Y) * smoothFactor);
} else {
intermediate.X = (float) (pt1.X + (pt2.X - pt1.X) * smoothFactor);
intermediate.Y = (intermediate.X * (pt2.Y - pt1.Y) + (pt2.X * pt1.Y - pt1.X * pt2.Y)) / (pt2.X - pt1.X);
}
return intermediate;
}
static class PDFPoint {
float X;
float Y;
PDFPoint(PDFPoint point) {
this.X = point.X;
this.Y = point.Y;
}
PDFPoint(float X, float Y) {
this.X = X;
this.Y = Y;
}
PDFPoint() {
}
}