matoos32 / jfreechart-builder

A builder pattern module for working with the jfreechart library.
Other
11 stars 2 forks source link

Add convenience ability to visually align cross-hairs on all sub-plots #78

Closed matoos32 closed 1 year ago

matoos32 commented 1 year ago
matoos32 commented 1 year ago

I noticed the cross-hairs begin to cease lining up when the demo app width is made very large, starting around 2050 pixels. The misalignment increases in magnitude as the app width increases. At extremely large width of 3440 pixels the cross-hairs move in different directions on the sub-plots. Reproduced this on Linux and Windows with Java 8 and 11. Seems like a logic problem somewhere, but the code in this change-set is using the same x-coordinate for all sub-plots. Needs investigating.

matoos32 commented 1 year ago

I did some debugging. Capturing some notes:

I found that XYPlot's draw() method will adjust the crosshair x-value in the presence of an achor. See: https://github.com/jfree/jfreechart/blob/v1.5.3/src/main/java/org/jfree/chart/plot/XYPlot.java#L3002

The anchor is set in the ChartPanel when handling a Java mouse click. See: https://github.com/jfree/jfreechart/blob/v1.5.3/src/main/java/org/jfree/chart/ChartPanel.java#L1923

This records the click location for use by JFreeChart components downstream, like the XYPlot draw() method.

jfreechart-builder always uses a CombinedDomainXYPlot with subplots even if there's just one plot. This is to maintain code for any number of plots with only one set of underlying logic.

This pull request's solution was to emulate a click on all subplots by calling handleClick() on them. This would leverage the existing JFreeChart logic to update and display crosshairs, to not write new crosshair logic in jfreechart-builder. However CombinedDomainXYPlot provides the anchor only to the draw() call of the subplot that is clicked on. For the others null is provided.

The outcome is the clicked-on subplot gets a crosshair x-value adjustment as linked at the top, and the other subplots don't.

The event's 2D coordinate and coordinate-converted value work visibly fine to me / match the anchor-adjustment value when the window size is much narrower than about 2050 pixels, but has a very noticable mismatch with the anchor-adjustment beyond that ~2050. I don't know if it has to do with density of data points along the x-axis, or simply the width of the window.

When the problem manifests, if you examine the x-value returned by the XYPlot draw() anchor-adjustment xx = xAxis.java2DToValue(anchor.getX(), dataArea, xAxisEdge);, where xAxis is the domain axis, you see its value differs from the x-value obtained using from the ChartMouseEvent's getTrigger().getX() passed into the x-axis's java2DToValue() in handleClick: https://github.com/jfree/jfreechart/blob/v1.5.3/src/main/java/org/jfree/chart/plot/XYPlot.java#L3970

Looking at java2DToValue() implementations, both DateAxis and NumberAxis used in jfreechart-builder perform a floating point conversion (and type cast in DateAxis's case) using a calculated percent of the Java 2D range. See:

Could these calculations be noticeably sensitive to the magnitude and floating point precision of its parameters?

Some runtime values inspected with the debugger at the window width where a difference starts being noticed (above 1924 pixels). This is for the "Stock Charts Daily | With Time Gaps | All Defaults" demo app case. The variables below are from the code locations linked in this comment. Compare the calculated hvalue of handleClick() to the xx of draw():

---------------------------------------
handleClick()
x=528, y=20
hvalue: 1.665302317283E12

draw()
Anchor: Point2D.Double[528.0, 203.0]
dataArea: java.awt.Rectangle[x=8,y=20,width=858,height=359]
xx:     1.665302317283E12

---------------------------------------
handleClick()
x=480, y=20
hvalue: 1.661062198591E12

draw()
Anchor: Point2D.Double[480.0, 336.0]
dataArea: java.awt.Rectangle[x=8,y=20,width=1924,height=359]
xx:     1.661062198591E12

---------------------------------------
handleClick()
x=888, y=20
hvalue: 1.663543577185E12
dataArea: java.awt.geom.Rectangle2D$Double[x=8.0,y=20.0,w=1928.0,h=619.421875] (combined plot)

draw()
Anchor: Point2D.Double[879.0, 274.0]
dataArea: java.awt.Rectangle[x=8,y=20,width=1928,height=359]
xx:     1.663488708928E12

It seems the anchor x-adjustment puts the vertical crosshair line (x-value) at the correct spot. It's the other plots that get a wrong x-crosshair. A solution could perhaps be to somehow pass a custom anchor in draw() for the other plots.

I also looked at an alternative suggested in 2014 by David Gilbert (JfreeChart author): https://stackoverflow.com/questions/24188142/jfreechart-dynamic-point-selection-in-chartpanel-using-chartmouselistener-and-m/24200665#24200665. The idea is to use a CrosshairOverlay with x and y Crosshair instances. In prototyping I think this may unfortunately only work for non combined plot solutions as the y-value of the crosshair is in the data space, and what value do you use with multiple sub-plots having different data-sets?

matoos32 commented 1 year ago

In the runtime values in the previous comment there is a 2D x-coordinate difference where the problem occurs: handleClick(): x=888 draw(): anchor x=879

In the cases with no problems these x-coordinates are the same (safe for the int/double type difference)

matoos32 commented 1 year ago

I found a way to obtain and use the calculated ChartPanel anchor. I improved the solution with this and added a second commit.

matoos32 commented 1 year ago

All discovered issues were resolved.

Tested on Linux with Java 8 and 11. Tested on Windows with Java 11.

No new issues observed.

Will merge this now.