JDK-8134315 : [LineChart] Blurry elements since 8u60
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: 8u60,9
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2015-08-24
  • Updated: 2018-09-05
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Relates :  
Description
Since 8u60 nearly all elements of the LineChart are blurry (see attached image). This makes the LineChart of 8u60 almost unusable. For us this is a blocker for the update from 8u51 to 8u60.

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;

public class RunTest extends Application {

	@Override
	public void start(Stage primaryStage) throws Exception {
		ObservableList<Data<Number, Number>> dataList = FXCollections.observableArrayList();
		dataList.add(new Data<Number, Number>(0, 0));
		dataList.add(new Data<Number, Number>(1, 0));
		dataList.add(new Data<Number, Number>(2, 1));
		dataList.add(new Data<Number, Number>(2, 2));
		dataList.add(new Data<Number, Number>(3, 2));
		dataList.add(new Data<Number, Number>(4, 0));
		Series<Number, Number> series = new Series<>(dataList);
		Platform.runLater(() -> series.getNode().setStyle("-fx-stroke-width: 1px;-fx-stroke: black;"));
		ObservableList<Series<Number, Number>> seriesList = FXCollections.observableArrayList(series);
		LineChart<Number, Number> lineChart = new LineChart<>(new NumberAxis(), new NumberAxis(), seriesList);
		primaryStage.setScene(new Scene(lineChart));
		primaryStage.show();
	}

	public static void main(String[] args) {
		launch(args);
	}

}

Comments
As regression labeled and introduced in 8u or 9 -- targeted to 10
17-02-2017

Approved by component triage team to defer
21-11-2016

SQE: JDK 9: OK to defer
21-11-2016

9-defer-request: No resources to fix
18-11-2016

One additional issue to think about, and this issue will affect even the non-HiDPI cases. The best sub-pixel location to place odd-pixel-sized lines is on the center of pixels, then each half of the line, which is an integer+0.5 pixels wide, will fall on a pixel boundary and be crisp. The best sub-pixel location to place even-pixel-sized lines is at the edge of pixels, then each half of the line, which is an integer number of pixels wide, will also fall on a pixel boundary and be crisp. If the stroke width of the ticks maps to an odd number of pixels, but the stroke width of the line graph maps to an even number of pixels, then there is no single mapping that will map both the tick marks and the rectilinear parts of the line graph consistently so as to avoid blurriness in both. We would need some way to help the developer ensure that both stroke widths are either odd or even number of pixels wide. And if there are multiple lines in a line graph, that all of the stroke widths are the same parity of even or odd pixel widths. If it is just "ticks vs. chart lines" and they have customized the chart lines, but left the ticks the default style width, we could always make sure to snap the tick width to the same parity of pixel thickness as the chart lines, but at some point the developer will need to be aware of this or we'll have a really convoluted heuristic to try to make everything crisp (for rectilinear segments and ticks and grid lines). Note that this isn't a simple matter where a "choose an odd size for all of them" kind of solution works. That would work for 1:1 rendering, but might not for 125% or 150% since rounding will be involved to determine the true "pixel size" of the specified widths. "1 and 3" on a 100% screen both map to odd pixel sizes (1 and 3), but not on a 125% screen where they map to 1.25 rounded to 1 pixel, and 3.75 rounded to 4 pixels. If we don't round them then the 1.25 and 3.75 pixel wide lines will always be fuzzy. If we round them the one is odd and the other is even and a consistent mapping can only force one of them to be crisp.
13-09-2016

I think this issue may be better solved by having the algorithms that determine the mapping from chart sampling variables to pixels take DPI into account and apply the adjustments globally to all elements - both the grid/ticks and the data itself. There would be no snapping of individual tick elements in this case. The formula that converts between a chart sample and its coordinate in the graph needs to be adjusted so that mapping the sample values for tick marks and grid lines maps onto a location that makes those lines crisp, then that same mapping should be applied to the data itself so that any data that maps directly to a tick value will also be crisp. Data that is somewhere between the tick marks may or may not be crisp or fuzzy. For example: target size of graph is W x H pixels range of data (difference from minTickVal to maxTickVal) is Sx,Sy units in whatever units the sample data is. if we want tick marks every Tx,Ty units (horizontal or vertical) Number of tick regions (Nx,Ny) is then (Sx/Tx, Sy/Ty) (we'll render N+1 tick marks, but this is the number of tick regions) tickThickness = snapSize(tick stroke width) If you can adjust the size of the graph/chart from WxH then the spacing of the ticks Tw,Th would be snapSize((W,H - tickThickness) / Nx,y). Set size of graph to (Tw*Nx + tickThickness, Th*Ny + tickThickness) If you must fit within W x H then you should use Tw,Th = snapFloor((W,H - tickThickness) / Nx,y) for the tick spacing and pad accordingly. In either case, the true size of the snapped graph (Gw,Gh) within that region is (Tw*Nx + tickThickness, Th*Ny + tickThickness) First tick would be at tickThickness/2.0, last tick would be at Gw,h - tickThickness/2.0 Tick #n would be at (tickThickness/2.0 + n*Tw,Th) Then map the ticks and the data by the same reversible mapping: dataLocationX,Y = tickThickness/2.0 + (dataX,Y - minTickValX,Y) * (Tw,h/Tx,y) sampleValueX,Y = (dataLocationX,Y - tickThickness / 2.0) * (Tx,y/Tw,h) + minTickValX,Y
07-09-2016

They are thinner after the fix, before the fix they are always 2 pixels wide in non-HiDPI due to rounding, while chart elements are not only rounded, but additionally offset by 0.5
07-09-2016

I don't see why the horizontal and vertical lines should be any thinner than the diagonal ones. Is that element of the chart using a different stroke width?
07-09-2016

When I said jagged I was referring to "noisy" lines as in the JDK-8097482. Yes, I agree that we shouldn't revert that fix but then horizontal and vertical data lines' width will not be consistent anymore and that's much more likely to be noticed. So my first fix addressed most of the concerns regarding snapping chart's elements but the rectilinear data lines remained blurred and "thin" as in the attached "bad" screenshots. I have an update in the works which addressed most of other concerns (about 0.5 offsets and so on) but it uncovers a lot more HiDPI-related problems (like incorrect caching of snapped insets)
07-09-2016

Where was the code that manually added the 0.5 to the tick marks? We should teach that code how to properly offset them using a snap...() calculation...
07-09-2016

With respect to charts and graphs I would say that we know that the following tend to be rectilinear: tick marks grid lines bar outlines for bar charts and they should probably be snapped when the control is set to snap-to-pixel, but we do not know whether random line graphs are rectilinear or not. We might want to add a property so that the developer can request that data points in line graphs and other graphs can be snapped, but we should not do it automatically based on the general Controls snap property which we might apply to tick marks and other known rectilinear elements. It would probably also not be advisable to run through the data set and attempt to determine rectilinearity on our own as we might end up with cases where a small amount of data happens to be rectilinear and we decide to snap it and then they add a non-rectilinear chart sample and suddenly the entire graph unsnaps - it should be controlled at a higher level within the application...
07-09-2016

I don't think diagonal lines were ever not fuzzy, were they? They will always be rendered with antialiasing regardless of what we do about snapping locations. Where did someone see "jagged diagonal lines"? Trying to round coordinates that are part of a path that isn't known to be strictly rectilinear (as in all elements are straight lines aligned either horizontally or vertically) is not advised and is the reason for the fix in JDK-8097482. We should not revert that fix. Getting tick marks to be non-fuzzy should be a matter of snapping the stroke widths and the tick marker locations, using: snappedStrokeWidth = snapSize(requestedStrokeWidth) That should probably always be done regardless of whether the lines are known to be rectilinear or not, but then the following should only be applied to cases where we are stroking a path that is known to be rectilinear, like tick marks: halfSnappedStrokeWidth = snappedStrokeWidth/2.0 snappedTickX = snapLocationX(tickx - halfSnappedStrokeWidth) + halfSnappedStrokeWidth snappedTickY = snapLocationY(ticky - halfSnappedStrokeWidth) + halfSnappedStrokeWidth
07-09-2016

Just for the sake of completeness: originally most of the chart elements which rendered as a path with 1px width stroke (tick marks, grid lines, zero lines) had coordinates manually offset by 0.5 so in non-HiDPI case they were rendered exactly at the pixel center thus producing 1 device pixel width. While the actual data lines were rendered without offsets so the same 1px width line were rendered on the edge of the pixel producing 2 device pixels width due to AA.
07-09-2016

I don't see any way to fix both issues. Either we have jagged diagonal line as in the original bug JDK-8097482 because individual segments coordinates are rounded but the horizontal or vertical lines are aligned to pixels (not in the HiDPI non-integer scales though). Or we have straight diagonal line but the lines could be not aligned to the pixel bounds. Perhaps the best solution would be to revert the fix for JDK-8097482, because the use case there is quite unusual and file additional bugs to correct HiDPI charts rendering (that is, not only round the coordinates and offset by 0.5, but use corresponding snap* methods) Maybe we can fix JDK-8097482 by simplifying paths - merging segments that have almost identical tangents?
07-09-2016

Jim, thanks for the review! I'll quickly answer a question about previous behavior and why I did this fix and will try to answer other questions later. Before the fix for "noisy lines" getDisplayPosition was always rounded. This is why all elements were exactly on pixel bounds (not quite sure about HiDPI case but still) After that fix they were not rounded and in this fix I tried to use snapPosition in those places where previously the positions were rounded. Yes, there are far more places where various snap* calls could be added, I will look into lines issues and would like to postpone everything else for later.
29-08-2016

A couple of comments on the philosophy used in various places in the proposed fix... - In some locations we had "coord - size/2" and you changed them to "snap(coord) - size/2" which doesn't help in all cases, particularly if the pixel size is odd then you've positioned this on a half pixel location which would then be blurry. (Note that pixel size would be "snapSize(size)", not "size" - even if size were an even number like 20, if you are on a 125% screen then that size would be zoomed to 25 pixels which would be an odd number of pixels, etc.). I'd instead snap the whole equation, as in "snap(coord - size/2)". I see in Axis that it has a few locations where it adjusts paths to fall on a center pixel by setting the layoutXY to +0.5,-0.5 - those should really be adjusted to use a line width that is an even number of pixels wide (using snap) and then set the layout offsets to half of that number. Those might be why your lines are still sometimes blurred. - You seem to be snapping the axis tick mark locations. I didn't see any code there that was previously trying to get them to integer locations so I wasn't sure why you were snapping those coordinates. Also, there are 2 ways to go with tick marks - either you punt and position them where they may fall and rely on AA to make them look consistently spaced, but possibly blurry - or you plan at a higher level to make them all land naturally on pixel boundaries by planning how much space you will use and how you will space them. The latter would require you to adjust the scaling of the entire graph to match so if you are governed by how the graph was drawn then you have to go with what they gave you and live with the fuzziness. If you have control over how much space the graph takes up, then you can request a size (preferred dimensions) such that N tick marks and the space between them come out to an even number of pixels. Thus, much of tick placement is probably better handled by fixing preferred dimension calculations. - 1 question, were the graphs in the original example all nice and crisp if you forced a new size on the graph? Bump the sizes manually by 1 pixel up and down for a few pixels and see if they maintain their crispness. If they used to get blurry when not allowed to use their preferred sizes before, then the answer is to fix the preferred size calculations so that the same considerations of "make sure I'm an even number of pixels wide/tall" happen now (I would guess that there would be a round/floor/ceil set of calculations in the preferred size code). If they used to be always crisp regardless of size, then they must be doing some rounding to "pixels" somewhere that assumed that all integers were pixels and you then need to find where that rounding was done and fix that, not snap everything to a pixel at the last stage of calculations at the bottom end of the layout code.
29-08-2016

I'm attaching a test case used for testing and results of the testing.
29-08-2016

Jim, Jonathan, Could you please review this fix? http://cr.openjdk.java.net/~vadim/8134315/webrev.00/ This regression was introduced in the fix for JDK-8097482 where I removed the rounding in the ValueAxis.getDisplayPosition method. The fix is to introduce more correct snapPosition's basically everywhere in the charts: AreaChart, StackedAreaChart, LineChart, BubbleChart, ScatterChart - snapping of symbols. BarChart, StackedBarChart - snapping of positions and sizes of bars. Axis - snapping of major tick mark's positions. ValueAxis - the same for minor marks. XYChart - snapping of zero- and gridlines. The only issue remained here is, well, lines! Previously rounded coordinates produced more or less consistent result for 1px lines as in the test case for this bug. Now since we don't round them due to that fix, now they are not consistent and sometimes are one pixel off, sometimes blurred. I'm not really sure what we can do here, these 2 use cases are mutually exclusive...
29-08-2016

Hi, are there any news for this issue? This is really a showstopper for our application. We stuck on 8u51 until this regression is fixed.
27-10-2015

Raising priority to P3 and adding the "regression" keyword. I can confirm this regression and will evaluate it. Depending on what the fix is and whether there is a workaround we might backport the fix to an 8u release.
24-08-2015