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文件:
- 手动创建一个
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; }
- 在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




