66import com .yishape .lab .math .plot .javafx .JavaFxThemeManager ;
77import javafx .scene .canvas .GraphicsContext ;
88import javafx .scene .paint .Color ;
9- import javafx .scene .text .Font ;
109import javafx .scene .text .TextAlignment ;
1110
1211import java .util .List ;
@@ -38,18 +37,9 @@ public void render(GraphicsContext gc, List<SeriesData> data, ChartConfig config
3837
3938 // 判断是否为分组柱状图
4039 boolean isGrouped = data .size () > 1 ;
41-
42- // 计算所有数据的最大值
43- double maxValue = Double .MIN_VALUE ;
44- int maxBars = 0 ;
45- for (SeriesData s : data ) {
46- if (s .y == null ) continue ;
47- maxBars = Math .max (maxBars , s .y .length ());
48- for (int i = 0 ; i < s .y .length (); i ++) {
49- maxValue = Math .max (maxValue , s .y .get (i ));
50- }
51- }
52- double [] yRange = new double []{0 , maxValue * 1.1 };
40+
41+ // 与 Seaborn/matplotlib 一致:Y 轴包含 0,负值自基线向下延伸
42+ double [] yRange = computeVerticalBarYRange (data );
5343
5444 // 清空画布
5545 gc .setFill (themeManager .getBackgroundColor ());
@@ -75,6 +65,57 @@ public void render(GraphicsContext gc, List<SeriesData> data, ChartConfig config
7565 }
7666 }
7767
68+ /**
69+ * 垂直柱图 Y 轴范围:必含 0;全非负时从 0 起算,全非正时到 0 止,有正有负时在两侧留边距。
70+ */
71+ private static double [] computeVerticalBarYRange (List <SeriesData > data ) {
72+ double minV = Double .POSITIVE_INFINITY ;
73+ double maxV = Double .NEGATIVE_INFINITY ;
74+ for (SeriesData s : data ) {
75+ if (s .y == null ) continue ;
76+ for (int i = 0 ; i < s .y .length (); i ++) {
77+ double v = s .y .get (i );
78+ if (!Double .isFinite (v )) continue ;
79+ minV = Math .min (minV , v );
80+ maxV = Math .max (maxV , v );
81+ }
82+ }
83+ if (minV == Double .POSITIVE_INFINITY ) {
84+ return new double [] { 0 , 1 };
85+ }
86+ double pad ;
87+ if (minV >= 0 ) {
88+ double hi = maxV ;
89+ double span = Math .max (hi , 1e-15 );
90+ pad = span * 0.08 ;
91+ return new double [] { 0 , hi + pad };
92+ }
93+ if (maxV <= 0 ) {
94+ double lo = minV ;
95+ double span = Math .max (-lo , 1e-15 );
96+ pad = span * 0.08 ;
97+ return new double [] { lo - pad , 0 };
98+ }
99+ double span = maxV - minV ;
100+ pad = span * 0.08 ;
101+ return new double [] { minV - pad , maxV + pad };
102+ }
103+
104+ private static double yPixelForValue (double v , double [] yRange , ChartConfig config , double chartHeight ) {
105+ double span = yRange [1 ] - yRange [0 ];
106+ if (span <= 1e-30 ) span = 1 ;
107+ double t = (v - yRange [0 ]) / span ;
108+ return config .height - config .paddingBottom - t * chartHeight ;
109+ }
110+
111+ /** 横向柱图:数值映射到 X 像素(与垂直柱共用 {@code computeVerticalBarYRange} 的区间逻辑) */
112+ private static double xPixelForBarhValue (double v , double [] valueRange , ChartConfig config , double plotW ) {
113+ double span = valueRange [1 ] - valueRange [0 ];
114+ if (span <= 1e-30 ) span = 1 ;
115+ double t = (v - valueRange [0 ]) / span ;
116+ return config .paddingLeft + t * plotW ;
117+ }
118+
78119 /**
79120 * 绘制柱状图专用的坐标轴(只显示Y轴和分类X轴)
80121 */
@@ -128,18 +169,25 @@ private void renderSimpleBars(GraphicsContext gc, SeriesData series, ChartConfig
128169 double barWidth = chartWidth / numBars * 0.8 ;
129170 double barSpacing = chartWidth / numBars * 0.2 ;
130171
172+ double y0 = yPixelForValue (0 , yRange , config , chartHeight );
173+
131174 // 绘制柱状图
132175 for (int i = 0 ; i < numBars ; i ++) {
133176 double value = series .y .get (i );
134- double barHeight = (value / yRange [1 ]) * chartHeight ;
177+ double yVal = yPixelForValue (value , yRange , config , chartHeight );
178+ double top = Math .min (yVal , y0 );
179+ double barHeight = Math .abs (yVal - y0 );
180+ if (value != 0 && barHeight < 1 ) {
181+ barHeight = 1 ;
182+ }
135183 double x = config .paddingLeft + i * (barWidth + barSpacing ) + barSpacing / 2 ;
136- double y = config . height - config . paddingBottom - barHeight ;
137-
184+ double y = top ;
185+
138186 // 绘制柱子
139187 Color barColor = Color .web (palette [i % palette .length ]);
140188 gc .setFill (barColor );
141189 gc .fillRect (x , y , barWidth , barHeight );
142-
190+
143191 // 绘制边框 - 使用主题文本颜色
144192 gc .setStroke (themeManager .getTextColor ());
145193 gc .setLineWidth (1 );
@@ -148,19 +196,21 @@ private void renderSimpleBars(GraphicsContext gc, SeriesData series, ChartConfig
148196 double hitPx = x + barWidth / 2 ;
149197 double hitPy = y + barHeight / 2 ;
150198 JavaFxChartUtils .registerHit (config , hitPx , hitPy , i , value , series .name , 0 , i );
151-
199+
152200 // 绘制X轴标签
153201 if (series .labels != null && i < series .labels .size ()) {
154202 gc .setFill (themeManager .getTextColor ());
155203 gc .setFont (themeManager .getLabelFont ());
156204 gc .setTextAlign (TextAlignment .CENTER );
157- gc .fillText (series .labels .get (i ), x + barWidth / 2 ,
205+ gc .fillText (series .labels .get (i ), x + barWidth / 2 ,
158206 config .height - config .paddingBottom + 20 );
159207 }
160-
161- // 绘制数值标签 - 使用主题文本颜色
208+
209+ // 绘制数值标签:正值在柱顶上方,负值在柱底下方(与常见 bar 图一致)
162210 gc .setFill (themeManager .getTextColor ());
163- gc .fillText (String .format ("%.1f" , value ), x + barWidth / 2 , y - 5 );
211+ gc .setTextAlign (TextAlignment .CENTER );
212+ double labelY = value >= 0 ? y - 5 : y + barHeight + 14 ;
213+ gc .fillText (String .format ("%.1f" , value ), x + barWidth / 2 , labelY );
164214 }
165215 }
166216
@@ -182,6 +232,8 @@ private void renderGroupedBars(GraphicsContext gc, List<SeriesData> data, ChartC
182232 double barWidth = groupWidth * 0.8 / numGroups ;
183233 double groupSpacing = groupWidth * 0.2 ;
184234
235+ double y0 = yPixelForValue (0 , yRange , config , chartHeight );
236+
185237 // 绘制每个series的柱子
186238 for (int g = 0 ; g < numGroups ; g ++) {
187239 SeriesData series = data .get (g );
@@ -199,12 +251,17 @@ private void renderGroupedBars(GraphicsContext gc, List<SeriesData> data, ChartC
199251 // 值为0时不绘制(表示该组在该X位置无数据)
200252 if (value == 0 ) continue ;
201253
202- double barHeight = (value / yRange [1 ]) * chartHeight ;
254+ double yVal = yPixelForValue (value , yRange , config , chartHeight );
255+ double top = Math .min (yVal , y0 );
256+ double barHeight = Math .abs (yVal - y0 );
257+ if (barHeight < 1 ) {
258+ barHeight = 1 ;
259+ }
203260
204261 // X位置: 组起始 + 组内偏移
205262 double groupStartX = config .paddingLeft + i * groupWidth + groupSpacing / 2 ;
206263 double x = groupStartX + g * barWidth ;
207- double y = config . height - config . paddingBottom - barHeight ;
264+ double y = top ;
208265
209266 gc .fillRect (x , y , barWidth - 2 , barHeight );
210267 gc .setStroke (themeManager .getTextColor ());
@@ -373,9 +430,8 @@ private void renderHorizontalBars(GraphicsContext gc, List<SeriesData> data, Cha
373430 SeriesData series = data .get (0 );
374431 if (series .y == null || series .labels == null ) return ;
375432 int n = Math .min (series .y .length (), series .labels .size ());
376- double maxV = 0 ;
377- for (int i = 0 ; i < n ; i ++) maxV = Math .max (maxV , series .y .get (i ));
378- if (maxV <= 0 ) maxV = 1 ;
433+ List <SeriesData > one = List .of (series );
434+ double [] xRange = computeVerticalBarYRange (one );
379435
380436 gc .setFill (themeManager .getBackgroundColor ());
381437 gc .fillRect (0 , 0 , config .width , config .height );
@@ -393,22 +449,29 @@ private void renderHorizontalBars(GraphicsContext gc, List<SeriesData> data, Cha
393449 double rowH = chartH / n * 0.72 ;
394450 double gap = chartH / n * 0.28 ;
395451 boolean uniformColor = Boolean .TRUE .equals (series .extraData .get ("uniformBarColor" ));
452+ double plotW = chartW * 0.92 ;
453+ double x0line = xPixelForBarhValue (0 , xRange , config , plotW );
396454 for (int i = 0 ; i < n ; i ++) {
397455 double v = series .y .get (i );
398456 double y = config .paddingTop + i * (rowH + gap );
399- double x0 = config .paddingLeft ;
400- double x1 = config .paddingLeft + (v / maxV ) * chartW * 0.92 ;
457+ double xVal = xPixelForBarhValue (v , xRange , config , plotW );
458+ double left = Math .min (x0line , xVal );
459+ double w = Math .abs (xVal - x0line );
460+ if (v != 0 && w < 1 ) {
461+ w = 1 ;
462+ }
401463 gc .setFill (Color .web (uniformColor ? palette [0 ] : palette [i % palette .length ]));
402- gc .fillRect (x0 , y , x1 - x0 , rowH );
464+ gc .fillRect (left , y , w , rowH );
403465 gc .setStroke (themeManager .getTextColor ());
404- gc .strokeRect (x0 , y , x1 - x0 , rowH );
466+ gc .strokeRect (left , y , w , rowH );
405467 gc .setFill (themeManager .getTextColor ());
406468 gc .setFont (themeManager .getLabelFont ());
407469 gc .setTextAlign (TextAlignment .RIGHT );
408470 gc .fillText (series .labels .get (i ), config .paddingLeft - 8 , y + rowH / 2 + 4 );
409471 gc .setTextAlign (TextAlignment .CENTER );
410- gc .fillText (String .format ("%.1f" , v ), x1 + 18 , y + rowH / 2 + 4 );
411- JavaFxChartUtils .registerHit (config , (x0 + x1 ) / 2 , y + rowH / 2 , i , v , series .name , 0 , i );
472+ double labelX = v >= 0 ? Math .max (x0line , xVal ) + 18 : Math .min (x0line , xVal ) - 18 ;
473+ gc .fillText (String .format ("%.1f" , v ), labelX , y + rowH / 2 + 4 );
474+ JavaFxChartUtils .registerHit (config , left + w / 2 , y + rowH / 2 , i , v , series .name , 0 , i );
412475 }
413476 }
414477
0 commit comments