I'm using JFreeChart to create a timeseries chart in my application.
I'm setting it's domain axis range manually using:
...
plot.getDomainAxis().setAutoRange(false);
Calendar c1=Calendar.getInstance();
c1.set(Calendar.HOUR_OF_DAY, 10);
c1.set(Calendar.MINUTE, 0);
Calendar c2=Calendar.getInstance();
c2.set(Calendar.HOUR_OF_DAY, 18);
c2.set(Calendar.MINUTE, 0);
plot.getDomainAxis().setRange(c1.getTimeInMillis(),c2.getTimeInMillis());
...
Zooming in to chart and then zooming out (Using mouse on chartplot itself) triggers AutoRange on both axis that makes Domain axis range change to series borders and not my own manual rages.
Example (Look at Domain axis's range):
Before zooming in-out (Correct):
After zooming in-out (Incorrect-is Auto ranged):
How can I make it to zoom out to my manually set range?
Thanks
You might try restoreAutoBounds(), shown here, followed by your custom domain setting.
Addendum: The behavior you see is defined in the mouse listener implementation of ChartPanel. You could override chartProgress() and restore your domain axis when the chart is finished drawing and not zoomed.
here a solution:
class MyNumberAxis extends org.jfree.chart.axis.NumberAxis
{
private boolean m_RestoreDefaultAutoRange;
MyNumberAxis()
{
super();
}
MyNumberAxis(String label)
{
super(label);
}
MyNumberAxis(String label, boolean restoreDefaulAutoRange)
{
super(label);
m_RestoreDefaultAutoRange = restoreDefaulAutoRange;
}
#Override
protected void autoAdjustRange()
{
if( m_RestoreDefaultAutoRange )
{
Plot plot = getPlot();
if( plot != null && plot instanceof ValueAxisPlot)
{
Range r = getDefaultAutoRange();
setRange(r, false, false);
}
}
else
super.autoAdjustRange();
}
}
Create an instance of MyNumberAxis setting the boolean to true and use it in your plot (plot.setRangeAxis() method). If you want to behave like the default NumberAxis, pass false as boolean.
Magallo's solution above worked great. I found it even more useful if I added another constructor:
MyNumberAxis(String label, boolean restoreDefaulAutoRange, Range defaultRange) {
super(label);
m_RestoreDefaultAutoRange = restoreDefaulAutoRange;
setDefaultAutoRange(defaultRange);
}
I created a custom NumberAxis with a fixed range. Zooming out will auto-zoom to this fixed range.
class FixedRangeNumberAxis extends NumberAxis
{
private Range range;
FixedRangeNumberAxis(String label, Range range)
{
super(label);
this.range = range;
}
#Override
protected void autoAdjustRange()
{
setRange(range, false, false);
}
}
Related
I am trying to build a simple JFreeChart XYLineChart object, and embed it into a ChartPanel object.
For some unknown reason, the plot area doesn't look properly: You can see how gridlines are inconsistent in thickness, and the edges of the plot have these thick black markings in random places. What could be the cause of this?
public class Main extends JFrame() {
public static void main (String [] args) {
ECGPanel myECGPanel = new ECGPanel();
this.add(myECGPanel);
}
}
public class ECGPanel extends Jpanel {
lineChart= ChartFactory.createXYLineChart("ECG", "Time(ms)", "Voltage(mV)", dataset,
PlotOrientation.VERTICAL, true, false, false);
chartPanel=new ChartPanel(lineChart);
chartPanel.setPreferredSize(new Dimension(1000,400));
this.add(chartPanel);
}
As it was pointed out in comments, the problem relates to Windows and its Display settings. I have found the discussion about how Swing behaves on higher DPI systems here
Interestingly, I don't have this problem when I create a JFreeChart in JavaFX (both using a SwingNode and when using a ChartViewer). You can also see read about that here
Triggered by a question about Slider and a slight bug, I tried to implement a SliderSkin that makes use of axis services, in particular its conversion methods between pixel and value. Works fine except that NumberAxis keeps its conversion offset and scale factor as internal fields that are updated only during a layout pass.
Which poses a problem, if I want to use the conversion during a layout pulse for updating another collaborator: in the case of a Slider that would be the thumb.
Below is a small examples to demonstrate the problem: simply a NumberAxis and a CheckBox at some value. On startup, the box is placed at the value at the middle. For maximal effect, maximize the window and note that the box position is not changed - now sitting near the beginning of the axis. Actually, it's the same when resizing the window but not so visible - see the printout of the difference.
Options to make it work
delay the thumb positioning until the next layout pulse (feels clumsy)
force the axis to update immediately
Looking for a way for the latter (couldn't find anything except reflectively invoke the axis' layoutChildren).
The example:
public class AxisInvalidate extends Application {
public static class AxisInRegion extends Region {
NumberAxis axis;
Control thumb;
IntegerProperty value = new SimpleIntegerProperty(50);
private double thumbWidth;
private double thumbHeight;
public AxisInRegion() {
axis = new NumberAxis(0, 100, 25);
thumb = new CheckBox();
getChildren().addAll(axis, thumb);
}
#Override
protected void layoutChildren() {
thumbWidth = snapSize(thumb.prefWidth(-1));
thumbHeight = snapSize(thumb.prefHeight(-1));
thumb.resize(thumbWidth, thumbHeight);
double axisHeight = axis.prefHeight(-1);
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// this marks the layout as dirty but doesn't immediately update internals
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
Platform.runLater(() -> {
LOG.info("diff " + (pixelOnAxis - axis.getDisplayPosition(value.getValue())));
});
// moving this line into the runlater "solves" the problem
thumb.relocate(pixelOnAxis, getHeight() /4);
}
}
private Parent getContent() {
AxisInRegion region = new AxisInRegion();
BorderPane content = new BorderPane(region);
return content;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent(), 500, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(AxisInvalidate.class
.getName());
}
Actually, I think it's a bug: the value/pixel conversion is a public service of the Axis - that should work always.
Not sure if it helps for your initial issue but simply do the calculation manually. Another plus - request layout becomes needless and can be removed.
double range = axis.getUpperBound()-axis.getLowerBound();
double pixelOnAxis = axis.getWidth()*(value.get()-axis.getLowerBound())/range;
Just got a working hack from the bug report: we need to call axis.layout() after changing size/location and before querying the conversion methods, something like:
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
// working hack from bug report:
axis.layout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
thumb.relocate(pixelOnAxis, getHeight() /4);
I'm trying to add a ToolTip to a custom MapMarker on JMapViewer. But repeaded searches on are not helping me solve this.
The custom MapMarker is:
public class MapMarkerUnit extends MapObjectImpl implements MapMarker
and the Paint Method overide is
public void paint(Graphics g, Point position, int radio) {
String filename = "marker.png";
//System.out.print(filename);
BufferedImage x = null;
try {
x = ImageIO.read(getClass().getResource(filename));
} catch (IOException ex) {
Logger.getLogger(MapMarkerUnit.class.getName()).log(Level.SEVERE, null, ex);
}
g.drawImage(x, position.x-16, position.y-37,null);
//if(getLayer()==null||getLayer().isVisibleTexts()) paintText(g, new Point(position.x+20,position.y));
}
Thanks for any help your able to offer.
Override the getToolTipText() method of JMapViewer. In your implementation, use getPosition() to convert the MouseEvent coordinates into geodetic coordinates. The example below simply displays the unformatted coordinates; you'll want to find the nearest MapMarker and return the appropriate text.
JMapViewer map = new JMapViewer() {
#Override
public String getToolTipText(MouseEvent e) {
Coordinate c = getPosition(e.getX(), e.getY());
return c.getLat() + " " + c.getLon();
}
};
map.setToolTipText(""); // initialize
Addendum: Is there a way of adding a tooltip directly to an image?
No; JMapViewer is the enclosing JComponent that handles tool tips.
I have about 50 markers on the map…that's a lot of iterations.
You definitely can't load images in your MapMarker implementation; use a SWingWorker to load images in the background, for example.
As a concrete iteration example, JFreeChart easily handles tool tips for scores of entities in this way. Here's the enclosing panel's getToolTipText() implementation, and here's the loop that invokes Shape#contains(). A simplified example that illustrates the approach is seen here.
In the code below, two circles are drawn, and added to one grouping node.
The following behavior variants observed:
1) Able to drag by any point of circle, including exterior and interior; if drag by intercircle point, drag does not occur
2) Able to drag only by exterior
3) Unable to drag
4) Able to drag by any point in extended bounds
Any behavior is inited by subinitialize() params.
I wonder, is it possible to finetune active "pickeble" points of a node? For example, can I leave subnodes not pickable, but make the entire group is draggable by circles exteriors only, as it was in case (2)?
The need is required because Piccolo does not allow to determine, in which group object the click was made. Particularly, I can't determine the group node, to which listener were attached, so if I have single listener and attach it to multiple nodes, I won't be able to distinguish, which one was called.
public class Try_Picking_01 {
private static final Logger log = LoggerFactory.getLogger(Try_Picking_01.class);
public static void main(String[] args) {
new PFrame() {
final PNode group = new PNode();
#Override
public void initialize() {
group.addInputEventListener(new PBasicInputEventHandler() {
#Override
public void mouseDragged(PInputEvent event) {
PDimension dim = event.getDeltaRelativeTo(group.getParent());
group.offset(dim.getWidth(), dim.getHeight());
}
});
getCanvas().getLayer().addChild(group);
getCanvas().setPanEventHandler(null);
// able to drag by any point of circle, including exterior and interior
// if drag by intercircle point, drag does not occur
// subinitialize(false, true, false);
// able to drag only by exterior
//subinitialize(true, true, false);
// unable to drag
// subinitialize(true, false, false);
// able to drag by any point in extended bounds
subinitialize(true, false, true);
}
private void subinitialize(boolean emptyFill, boolean pickable, boolean expandBounds) {
PPath path1 = PPath.createEllipse(100, 100, 100, 100);
path1.setStrokePaint(Color.RED);
path1.setPaint( emptyFill ? null : Color.YELLOW ); // line #1
log.info("path1.bounds = {}", path1.getBounds());
path1.setPickable(pickable); // line #2
PPath path2 = PPath.createEllipse(200, 200, 100, 100);
path2.setPaint( emptyFill ? null : Color.YELLOW ); // line #3
log.info("path2.bounds = {}", path2.getBounds());
path2.setPickable(pickable); // line #4
group.addChild(path1);
group.addChild(path2);
log.info("group.bounds = {}", group.getBounds());
if( expandBounds ) {
group.setBounds(group.getFullBounds()); // line #5
log.info("group.bounds = {}", group.getBounds());
}
}
};
}
}
Suzan,
Looking at how Piccolo manages input events the most sensible thing would be to create a specific event handler for each of your node groups. The piccolo only provides you with a generic input handler at the PNode level. This renders all PNode events effectively the same. If you want to define different behavior per node (or group) you'll need to derive a class (for instance from PNode or PPath) and add the logic to detect which node group was clicked and drag according to the settings passed in subInitialize.
The great thing about Java is that you can easily extend libraries like Piccolo to suit your own purpose.
JavaFX's TableView has a placeholder property that is basically a Node that gets displayed in the TableView whenever it is empty. If this property is set to null (its default value), it appears as a Label or some other text based Node that says "There is no content in the table."
But if there are any rows of data in the table, then the placeholder Node disappears and the entire vertical space in the TableView gets filled with rows, including empty rows if there isn't enough data to fill the whole table.
These empty rows are what I want, even when the table is empty. In other words, I don't want to use the placeholder at all. Does anyone know how I can do this?
I'd rather not do something kludgey like put a empty-looking row in the TableView whenever it's supposed to be actually empty.
Unfortunately, the old issue is still not fixed in fx9 nor later versions up and including fx18.
The original hack of the skin (see below the horizontal line) for fx9 seems to still be working in fx18. Though can be slightly improved since fx12 introduced access to the flow and tableHeaderRow.
While trying to adjust it to newer api, I came up with a - just as dirty and untested - approach. The idea is to override the actual layout method, that is the method that's called when laying out individual children of the table, watch out for the placeholder and replace it with the flow:
public class NoPlaceHolderSkin<T> extends TableViewSkin<T> {
public NoPlaceHolderSkin(TableView<T> control) {
super(control);
}
#Override
protected void layoutInArea(Node child, double areaX, double areaY, double areaWidth,
double areaHeight, double areaBaselineOffset, HPos halignment, VPos valignment) {
if (child.getStyleClass().contains("placeholder")) {
child.setVisible(false);
child = getVirtualFlow();
child.setVisible(true);
}
super.layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, halignment, valignment);
}
}
So revisited the hacks in the context of fx9. There had been changes, good and bad ones:
Skins moved into a public package which now allows to subclass them without accessing hidden classes (good)
the move introduced a bug which doesn't allow to install a custom VirtualFlow (fixed in fx10)
reflective access to hidden members will be strongly disallowed (read: not possible) sometime in future
While digging, I noticed ever so slight glitches with the hacks (note: I did not run them against fx8, so these might be due differences in fx8 vs fx9!)
the forced in-/visibility of placeholder/flow worked fine except when starting up with an empty table (placeholder was shown) and enlarging the table while empty (the "new" region looks empty)
faking the itemCount to not-empty lets the rows dissappear on pressing navigation keys (which probably is not a big problem because users tend to not navigate an empty table) - this is definitely introduced in fx9, working fine in fx8
So I decided to go with the visibility enforcement: the reason for the slight glitches is that layoutChildren doesn't layout the flow if it thinks the placeholder is visible. That's handled by including the flow in the layout if super didn't.
The custom skin:
/**
* TableViewSkin that doesn't show the placeholder.
* The basic trick is keep the placeholder/flow in-/visible at all
* times (similar to https://stackoverflow.com/a/27543830/203657).
* <p>
*
* Updated for fx9 plus ensure to update the layout of the flow as
* needed.
*
* #author Jeanette Winzenburg, Berlin
*/
public class NoPlaceHolderTableViewSkin<T> extends TableViewSkin<T>{
private VirtualFlow<?> flowAlias;
private TableHeaderRow headerAlias;
private Parent placeholderRegionAlias;
private ChangeListener<Boolean> visibleListener = (src, ov, nv) -> visibleChanged(nv);
private ListChangeListener<Node> childrenListener = c -> childrenChanged(c);
/**
* Instantiates the skin.
* #param table the table to skin.
*/
public NoPlaceHolderTableViewSkin(TableView<T> table) {
super(table);
flowAlias = (VirtualFlow<?>) table.lookup(".virtual-flow");
headerAlias = (TableHeaderRow) table.lookup(".column-header-background");
// startet with a not-empty list, placeholder not yet instantiatet
// so add alistener to the children until it will be added
if (!installPlaceholderRegion(getChildren())) {
installChildrenListener();
}
}
/**
* Searches the given list for a Parent with style class "placeholder" and
* wires its visibility handling if found.
* #param addedSubList
* #return true if placeholder found and installed, false otherwise.
*/
protected boolean installPlaceholderRegion(
List<? extends Node> addedSubList) {
if (placeholderRegionAlias != null)
throw new IllegalStateException("placeholder must not be installed more than once");
List<Node> parents = addedSubList.stream()
.filter(e -> e.getStyleClass().contains("placeholder"))
.collect(Collectors.toList());
if (!parents.isEmpty()) {
placeholderRegionAlias = (Parent) parents.get(0);
placeholderRegionAlias.visibleProperty().addListener(visibleListener);
visibleChanged(true);
return true;
}
return false;
}
protected void visibleChanged(Boolean nv) {
if (nv) {
flowAlias.setVisible(true);
placeholderRegionAlias.setVisible(false);
}
}
/**
* Layout of flow unconditionally.
*
*/
protected void layoutFlow(double x, double y, double width,
double height) {
// super didn't layout the flow if empty- do it now
final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
double headerHeight = headerAlias.getHeight();
y += headerHeight;
double flowHeight = Math.floor(height - headerHeight);
layoutInArea(flowAlias, x, y,
width, flowHeight,
baselineOffset, HPos.CENTER, VPos.CENTER);
}
/**
* Returns a boolean indicating whether the flow should be layout.
* This implementation returns true if table is empty.
* #return
*/
protected boolean shouldLayoutFlow() {
return getItemCount() == 0;
}
/**
* {#inheritDoc} <p>
*
* Overridden to layout the flow always.
*/
#Override
protected void layoutChildren(double x, double y, double width,
double height) {
super.layoutChildren(x, y, width, height);
if (shouldLayoutFlow()) {
layoutFlow(x, y, width, height);
}
}
/**
* Listener callback from children modifications.
* Meant to find the placeholder when it is added.
* This implementation passes all added sublists to
* hasPlaceHolderRegion for search and install the
* placeholder. Removes itself as listener if installed.
*
* #param c the change
*/
protected void childrenChanged(Change<? extends Node> c) {
while (c.next()) {
if (c.wasAdded()) {
if (installPlaceholderRegion(c.getAddedSubList())) {
uninstallChildrenListener();
return;
}
}
}
}
/**
* Installs a ListChangeListener on the children which calls
* childrenChanged on receiving change notification.
*
*/
protected void installChildrenListener() {
getChildren().addListener(childrenListener);
}
/**
* Uninstalls a ListChangeListener on the children:
*/
protected void uninstallChildrenListener() {
getChildren().removeListener(childrenListener);
}
}
Usage example:
public class EmptyPlaceholdersInSkin extends Application {
private Parent createContent() {
// initially populated
//TableView<Person> table = new TableView<>(Person.persons()) {
// initially empty
TableView<Person> table = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new NoPlaceHolderTableViewSkin<>(this);
}
};
TableColumn<Person, String> first = new TableColumn<>("First Name");
first.setCellValueFactory(new PropertyValueFactory<>("firstName"));
table.getColumns().addAll(first);
Button clear = new Button("clear");
clear.setOnAction(e -> table.getItems().clear());
clear.disableProperty().bind(Bindings.isEmpty(table.getItems()));
Button fill = new Button("populate");
fill.setOnAction(e -> table.getItems().setAll(Person.persons()));
fill.disableProperty().bind(Bindings.isNotEmpty(table.getItems()));
BorderPane pane = new BorderPane(table);
pane.setBottom(new HBox(10, clear, fill));
return pane;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(EmptyPlaceholdersInSkin.class.getName());
}
I think I found a solution. It is definitely not nice, since it is accessing the API in a not wanted way, and I'm probably also making undesired use of the visibleProperty, but here you go:
You can try to hack the TableViewSkin. Basically do this to retrieve a hacked Skin:
public class ModifiedTableView<E> extends TableView<E> {
#Override
protected Skin<?> createDefaultSkin() {
final TableViewSkin<E> skin = new TableViewSkin<E>(this) {
// override method here
}
// modifiy skin here
return skin;
}
}
For the TableViewSkin you then need to override following method:
#Override
protected VirtualFlow<TableRow<E>> createVirtualFlow() {
final VirtualFlow<TableRow<E>> flow = new VirtualFlow<TableRow<E>>();
// make the 'scroll-region' always visible:
flow.visibleProperty().addListener((invalidation) -> {
flow.setVisible(true);
});
return flow;
}
And for the skin using reflection stop showing the placeholder:
final Field privateFieldPlaceholderRegion = TableViewSkinBase.class.getDeclaredField("placeholderRegion");
privateFieldPlaceholderRegion.setAccessible(true);
final StackPane placeholderRegion = (StackPane) privateFieldPlaceholderRegion.get(skin);
// make the 'placeholder' never visible:
placeholderRegion.visibleProperty().addListener((invalidation) -> {
placeholderRegion.setVisible(false);
});
Maybe you can change the visibility of the flow in the same method to make the code shorter... But I think you get the concept
I found a solution for javafx8. It makes use of the non-public api, but it uses no reflection (luckly). Basically you need to set (or replace) the skin of the TableView and return a non-zero value in the method getItemCount(). Like so:
(TableView)t.setSkin(new TableViewSkin<Object>(t)
{
#Override
public int getItemCount()
{
int r = super.getItemCount();
return r == 0 ? 1 : r;
}
});
This method can also be used to add an extra row at the bottom of your last item (for if you want to include an add button for example). Basically return always one higher than the actual item-count.
Eventhough this is an old question, hopefully this was helpfull to someone.
If any one is still looking for an alternate solution apart from what others had provided, below is the one which I worked with. As far as to me, this is the most simpliest approach I can go with (no custom skins, no API tweaking & no heavy controls like ListView).
Set a StackPane with a customized CSS that resembles alternate row coloring.
StackPane placeHolder = new StackPane();
placeHolder.setStyle("-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #e8e8e8 49% , #f7f7f7 12% );");
tableView.setPlaceholder(placeHolder);
Below is the quick reference for implementation. The left table is with data and the right table is without data showing customized placeholder.
If your are specific with showing the column lines as well, you can follow the #Shreyas Dave approach of building a HBox of the StackPane(s) with border implementation.
HBox placeHolder = new HBox();
tableView.getColumns().forEach(tc->{
StackPane colHolder = new StackPane();
colHolder.getStyleClass().add("table-view-place-holder");
colHolder.prefWidthProperty().bind(tc.widthProperty());
placeHolder.getChildren().add(colHolder);
});
tableView.setPlaceholder(placeHolder);
And the CSS implementation is as below:
.table-view-place-holder{
-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #232A2D 49% , #3D4549 12% );
-fx-border-width: 0px 1px 0px 0px;
-fx-border-color: linear-gradient(from 50px 14px to 50px 40px , repeat, #3D4549 49% , #232a2d 12% );
}
I have a requirement of having contrast border color to row background. I can easily acheive that with the above approach for having border color to my placeholder columns.
Here is a tricky way to perform your task,
HBox box = new HBox();
box.setDisable(true);
for (TableColumn column : patientsTable.getColumns()) {
ListView<String> listView = new ListView<>();
listView.getItems().add("");
listView.setPrefWidth(column.getWidth());
box.getChildren().add(listView);
}
tableView.setPlaceholder(box);