You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

ScalaFX散点图数据点样式修改及线性回归可视化问题

解决ScalaFX ScatterChart散点样式与回归线覆盖问题

我来帮你搞定这个问题!你的核心痛点有两个:一是回归线被渲染成矩形散点,直接覆盖了原始数据点;二是不知道怎么在ScalaFX里调整散点的大小、形状和透明度,还有CSS怎么用。咱们一步步来拆解解决:

一、先搞定回归线的显示问题(别用散点画回归线!)

你现在的代码里把回归线的所有点都加到了ScatterChart里,所以它会被默认渲染成矩形散点,这肯定会盖住原始数据。正确的做法是把回归线做成真正的直线,这里给你两种简单方案:

方案1:在ScatterChart上叠加直线组件

计算出回归线的起点和终点,直接创建一个Line节点,加到图表的绘图区域里:

修改你的Scene部分代码:

scene = new Scene(400, 400) {
  val xAxis = NumberAxis()
  val yAxis = NumberAxis()
  // 只把原始数据点加到ScatterChart里
  val pData = XYChart.Series[Number, Number](
    "Data", ObservableBuffer(wrapper.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
  val plot = new ScatterChart(xAxis, yAxis, ObservableBuffer(pData))

  // 把回归线的坐标转换为图表的屏幕坐标
  val startX = xAxis.getDisplayPosition(graphPoints.head._1)
  val startY = yAxis.getDisplayPosition(graphPoints.head._2)
  val endX = xAxis.getDisplayPosition(graphPoints.last._1)
  val endY = yAxis.getDisplayPosition(graphPoints.last._2)

  // 创建红色的回归线
  val regressionLine = new Line(startX, startY, endX, endY) {
    stroke = scalafx.scene.paint.Color.Red
    strokeWidth = 2.0 // 线条粗细
  }

  // 把直线添加到图表的绘图区域
  plot.plotChildren.add(regressionLine)

  root = plot
}

方案2:用CombinedChart同时显示散点和折线

如果需要保留回归线的图例,可以用CombinedChart,让原始数据用散点图,回归线用折线图:

import scalafx.scene.chart.CombinedChart
import scalafx.scene.chart.LineChart

// ... 其他代码不变 ...

scene = new Scene(400, 400) {
  val xAxis = NumberAxis()
  val yAxis = NumberAxis()
  
  // 原始数据系列(散点类型)
  val pData = XYChart.Series[Number, Number](
    "Data", ObservableBuffer(wrapper.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))
  // 回归线系列(折线类型)
  val graph = XYChart.Series[Number, Number](
    "RegressionLine", ObservableBuffer(graphPoints.map(z => XYChart.Data[Number, Number](z._1, z._2)): _*))

  val combinedChart = new CombinedChart[Number, Number](xAxis, yAxis) {
    data.addAll(
      // 指定第一个系列用散点图
      XYChart.Series[Number, Number]().subChartType = "scatter", data = ObservableBuffer(pData),
      // 指定第二个系列用折线图
      XYChart.Series[Number, Number]().subChartType = "line", data = ObservableBuffer(graph)
    )
  }

  root = combinedChart
}

这样回归线就会变成平滑的直线,再也不会覆盖数据点了。

二、调整散点的样式(大小、形状、透明度)

接下来解决散点样式的问题,ScalaFX完全支持JavaFX的CSS,也可以直接在代码里设置,两种方式任你选:

方式1:代码里直接自定义单个点的样式

这种方式灵活性高,可以给不同点设置不同样式:

val pData = XYChart.Series[Number, Number](
  "Data", ObservableBuffer(wrapper.map { z =>
    val dataPoint = XYChart.Data[Number, Number](z._1, z._2)
    // 把默认的矩形改成圆形,设置大小、颜色和透明度
    dataPoint.node = new scalafx.scene.shape.Circle {
      radius = 5.0 // 点的大小
      fill = scalafx.scene.paint.Color.Blue.opaque(0.7) // 70%不透明的蓝色
      stroke = scalafx.scene.paint.Color.DarkBlue // 边框颜色
      strokeWidth = 1.0 // 边框粗细
    }
    dataPoint
  }: _*))

如果需要其他形状,比如三角形,换成Polygon就行:

dataPoint.node = new scalafx.scene.shape.Polygon(0.0, -5.0, 5.0, 5.0, -5.0, 5.0) {
  fill = scalafx.scene.paint.Color.Green.opaque(0.8)
}

方式2:用CSS统一设置所有散点的样式

如果想统一管理样式,就用CSS文件:

  1. 手动创建一个chart-styles.css文件,放在你的项目资源目录(比如src/main/resources)里:
/* 修改所有散点的默认样式 */
.chart-scatter-symbol {
    -fx-background-color: blue; /* 填充色 */
    -fx-background-radius: 50%; /* 圆形(默认是矩形) */
    -fx-padding: 5px; /* 控制点的大小 */
    -fx-opacity: 0.7; /* 透明度 */
    -fx-background-insets: 0; /* 去掉内边距 */
}

/* 给不同系列的点设置不同颜色 */
.default-color0.chart-scatter-symbol {
    -fx-background-color: blue;
}
.default-color1.chart-scatter-symbol {
    -fx-background-color: red;
}

/* 自定义形状:比如三角形 */
.triangle-symbol {
    -fx-shape: "M0,-5 L5,5 L-5,5 Z";
    -fx-background-color: green;
    -fx-opacity: 0.8;
}
  1. 在ScalaFX代码里加载这个CSS文件:
scene = new Scene(400, 400) {
  // ... 创建图表的代码 ...
  
  // 加载CSS,路径根据你的资源位置调整
  stylesheets.add(getClass.getResource("/chart-styles.css").toExternalForm())
  
  // 如果要给某个系列用自定义样式类
  pData.getStyleClass.add("triangle-symbol")
  
  root = plot
}

三、整合后的完整代码示例

把上面的方案整合起来,这里用方案1(叠加直线)+ 代码设置散点样式的完整代码:

import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.chart.ScatterChart
import scalafx.collections.ObservableBuffer
import scalafx.scene.chart.NumberAxis
import scalafx.scene.chart.XYChart
import scalafx.scene.shape.Line
import scalafx.scene.shape.Circle
import org.ejml.simple.SimpleMatrix
import scala.collection.mutable.Buffer

object Plotting extends JFXApp {
  /*
   * 示例x和y值,用于计算回归线
   */
  val xValues = Array(Array(1.0, 1.0, 1.0, 1.0, 1.0, 1.0), Array(14.0, 19.0, 22.0, 26.0, 31.0, 43.0))
  val yValues = Array(Array(51.0, 57.0, 66.0, 71.0, 72.0, 84.0))
  val temp = yValues.flatten
  val wrapper = xValues(1).zip(temp)

  /*
   * 矩阵计算回归系数,生成回归线的点
   */
  val X = new SimpleMatrix(xValues).transpose
  val Y = new SimpleMatrix(yValues).transpose
  val secondX = new SimpleMatrix(xValues(0).size, 2)
  for (i <- 0 until xValues(0).size) {
    secondX.set(i, 0, xValues(0)(i))
    secondX.set(i, 1, xValues(1)(i))
  }
  val invertedSecondX = secondX.pseudoInverse()
  val B = invertedSecondX.mult(Y)
  val graphPoints = Buffer[(Double, Double)]()
  for (i <- 0 to xValues(1).max.toInt) {
    graphPoints.append((i.toDouble, B.get(0, 0) + i * B.get(1, 0)))
  }

  stage = new JFXApp.PrimaryStage {
    title = "Linear Regression Visualization"
    scene = new Scene(600, 400) { // 稍微放大一点,显示更清楚
      val xAxis = NumberAxis("X Value", 0, 45, 5)
      val yAxis = NumberAxis("Y Value", 50, 90, 5)
      
      // 自定义原始数据点的样式
      val pData = XYChart.Series[Number, Number](
        "Raw Data", ObservableBuffer(wrapper.map { z =>
          val dataPoint = XYChart.Data[Number, Number](z._1, z._2)
          dataPoint.node = new Circle {
            radius = 5.0
            fill = scalafx.scene.paint.Color.Blue.opaque(0.7)
            stroke = scalafx.scene.paint.Color.DarkBlue
            strokeWidth = 1.0
          }
          dataPoint
        }: _*))
      
      // 创建散点图,只包含原始数据
      val plot = new ScatterChart(xAxis, yAxis, ObservableBuffer(pData)) {
        title = "Linear Regression Demo"
      }
      
      // 添加回归线
      val startX = xAxis.getDisplayPosition(graphPoints.head._1)
      val startY = yAxis.getDisplayPosition(graphPoints.head._2)
      val endX = xAxis.getDisplayPosition(graphPoints.last._1)
      val endY = yAxis.getDisplayPosition(graphPoints.last._2)
      
      val regressionLine = new Line(startX, startY, endX, endY) {
        stroke = scalafx.scene.paint.Color.Red
        strokeWidth = 2.0
        // 给线条加个虚线样式(可选)
        strokeDashArray = ObservableBuffer(5.0, 3.0)
      }
      
      plot.plotChildren.add(regressionLine)
      
      root = plot
    }
  }
}

小提示

  • 如果用getDisplayPosition获取坐标时出现偏差,说明图表还没完成布局,可以把添加直线的代码放到布局监听里:
plot.layoutBoundsProperty().onChange { _ =>
  val startX = xAxis.getDisplayPosition(graphPoints.head._1)
  val startY = yAxis.getDisplayPosition(graphPoints.head._2)
  val endX = xAxis.getDisplayPosition(graphPoints.last._1)
  val endY = yAxis.getDisplayPosition(graphPoints.last._2)
  regressionLine.setStartX(startX)
  regressionLine.setStartY(startY)
  regressionLine.setEndX(endX)
  regressionLine.setEndY(endY)
}

这样就能保证布局完成后再计算正确的坐标了。

内容的提问来源于stack exchange,提问作者Wasradin

火山引擎 最新活动