I need to move the label (Text) of all tick marks in an Axis such that the text is right in the middle of its own tick mark and the next tick mark.
I am using Roland's GanttChart control (with some modifications) and Christian Schudt's DateAxis for my X-axis. My objective is to plot a gantt chart based on Date values (ignoring time; all time values are truncated).
My gantt chart has a "start date" and an "end date" for every single task (i.e. visually on the chart it is represented by a single bar).
Consider this:
I have a task starting on 1st Feb, and it ends on the same day (1st Feb). I have two ways to render this on the chart:
Render it starting from 1st Feb, and ends at 1st Feb. This bar is effectively hidden, because its width is 0.
Render it starting from 1st Feb, with the right-edge of the bar touching 2nd Feb. This can potentially confuse the users because it would look like it starts from 1st Feb and ends on 2nd Feb.
To solve the problem caused by method 2, I need to shift the text labels by half a tick mark width to the right. Doing this would make it very clear to the user.
I have tried doing this:
final DateAxis xAxis = (DateAxis) this.ganttchart.getXAxis();
xAxis.getChildrenUnmodifiable().addListener(new ListChangeListener<Node>()
{
#Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends Node> c)
{
final List<Node> labels = xAxis.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
label.setTranslateX(xAxis.getWidth() / (labels.size() - 1) / 2);
}
}
});
However, doing so does not seem to shift the Text labels to the right at all.
The method I had tried actually works, but I was having problem with race condition. If I used Platform.runLater(), it would shift correctly. However, the position flickers whenever I resize the chart (it would jump between original position and shifted position).
This is the complete working version, which I need to override layoutChildren() in DateAxis.
#Override
protected void layoutChildren()
{
if (!isAutoRanging())
{
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
super.layoutChildren();
/*
* Newly added codes.
*/
final List<Node> labels = this.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
if (this.getSide() == Side.LEFT || this.getSide() == Side.RIGHT)
label.setTranslateY(this.getHeight() / (labels.size() - 1) / 2);
else if (this.getSide() == Side.TOP || this.getSide() == Side.BOTTOM)
label.setTranslateX(this.getWidth() / (labels.size() - 1) / 2);
}
/*
* End of new codes.
*/
}
Update
There was still some layout bugs. It has to do with the fact that Axis.positionTextNode() positions the texts using getBoundsInParent(), which takes into consider the translation.
This is the new working version, hopefully someday it would be helpful to someone.
#Override
protected void layoutChildren()
{
if (!isAutoRanging())
{
currentLowerBound.set(getLowerBound().getTime());
currentUpperBound.set(getUpperBound().getTime());
}
super.layoutChildren();
/*
* Newly added codes.
*/
final List<Node> labels = this.getChildrenUnmodifiable().filtered(node -> node instanceof Text);
for (Node label : labels)
{
if (this.getSide().isHorizontal())
{
if (label.getTranslateX() == 0)
label.setTranslateX(this.getWidth() / (labels.size() - 1) / 2);
else
{
label.setLayoutX(label.getLayoutX() + label.getTranslateX());
}
}
else if (this.getSide().isVertical())
{
if (label.getTranslateY() == 0)
label.setTranslateY(this.getHeight() / (labels.size() - 1) / 2);
else
{
label.setLayoutY(label.getLayoutY() + label.getTranslateY());
}
}
}
/*
* End of new codes.
*/
}
Related
I am wondering if it is possible to perform a fade out/fade in animation on a specific line of a textview. I have a two line textview which I would like the "title" to stay visible while the "data" fades out and in when it changes. I am attempting to limit the number of views on my fragment so separating the two lines into separate textviews is not preferable. I am fairly new to animations and was unsure if this is possible.
Update
After Cheticamps answer I developed my own java version of his solution and wanted to post it here if anyone else was looking for this.
ValueAnimator alphaAnim = ValueAnimator.ofInt(255,0).setDuration(1000);
alphaAnim.addUpdateListener(valueAnimator -> {
int alpha = (int) valueAnimator.getAnimatedValue();
int newColor = binding.ForegroundSpanText.getCurrentTextColor() & 0x00ffff | (alpha <<24);
System.out.println("Color: "+ Integer.toHexString(newColor));
SpannableString tempStringHolder = binding.getAnimString();
if(fadingSpan !=null){
tempStringHolder.removeSpan(fadingSpan);
}
fadingSpan = new ForegroundColorSpan(newColor);
tempStringHolder.setSpan(fadingSpan, 38, animStringHolder.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
binding.setAnimString(tempStringHolder);
});
alphaAnim.addListener(new AnimatorListenerAdapter(){
#Override
public void onAnimationEnd(Animator animation){
System.out.println("Finished");
}
});
You can set a series of ForegroundColorSpans on the section of the text that you want to fade. Each successive ForegroundColorSpan will decrease the alpha of the text color until the alpha value is zero. (Alpha == 255 is fully visible; alpha == 0 is invisible.)
Animation of the alpha value of the text color associated with the ForegroundColorSpan can be accomplished with a ValueAnimator. The following shows this technique.
The TextViw is simply
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:bufferType="spannable"
android:gravity="center"
android:text="Hello\nWorld!"
android:textColor="#android:color/black"
android:textSize="48sp"/>
The code is wrapped in a button's click listener for demo purposes:
var fadingSpan: ForegroundColorSpan? = null
val spannable = binding.textView.text as Spannable
binding.button.setOnClickListener {
// android:bufferType="spannable" must be set on the TextView for the following to work.
// Alpha value varies from the 255 to zero. (We are assuming the starting alpha is 255.)
ValueAnimator.ofInt(255, 0).apply {
duration = 3000 // 3 seconds to fade
// Update listener is called for every tick of the animation.
addUpdateListener { updatedAnimation ->
// Get the new alpha value and incorporate it into the color int (AARRGGBB)
val newAlpha = updatedAnimation.animatedValue as Int
val newColor =
binding.textView.currentTextColor and 0x00ffff or (newAlpha shl 24)
if (fadingSpan != null) {
spannable.removeSpan(fadingSpan)
}
fadingSpan = ForegroundColorSpan(newColor)
spannable.setSpan(
fadingSpan,
6,
12,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
start()
}
}
I got 78 labels in a pie chart like this picture.
And I want to show only top 3 labels in descending order.
How can I do this?
Create a custom PieSectionLabelGenerator and return null when you do not like to display the label.
Example
public class PieMaximumLabelsGenerator extends StandardPieSectionLabelGenerator {
private static final long serialVersionUID = 1385777973353453096L;
private int nrLabels;
private boolean showFirst;
/**
* A custom label generator to show only limited numbers of labels
* #param nrLabels, number of labels to show
* #param showFirst, if true, show first labels otherwise show the last
*/
public PieMaximumLabelsGenerator(int nrLabels, boolean showFirst){
this.nrLabels = nrLabels;
this.showFirst = showFirst;
}
#Override
public String generateSectionLabel(PieDataset dataset, Comparable key) {
int index = dataset.getIndex(key);
if (showFirst){
if (index>=nrLabels){
return null; //no more lables if index is above
}
}else{
if (index<dataset.getItemCount()-nrLabels){
return null; //no labels if index is not enough
}
}
return super.generateSectionLabel(dataset, key);
}
}
Then set this to your plot
((PiePlot) chart.getPlot()).setLabelGenerator(new PieMaximumLabelsGenerator(3, false));
Output, similar example but displaying first 5 values instead of last 3, hence ((PiePlot) chart.getPlot()).setLabelGenerator(new PieMaximumLabelsGenerator(5, true));
My preference however is to display label if the arc angle of slice is large enough. This can be done by collecting totale values of items in the chart and then calculating the angle using Number value = dataset.getValue(key); in generateSectionLabel to get the current angle (dimension) of slice.
I'm working on a project and I have a tableView, which consists of many columns (365 actually). I was wondering if it's possible to set a specific column to be in the middle, for example if I have 365 days, I want user to be able to see the column for the current date without scrolling left and right.
If a specific column is allowed to move to the left edge, the code below is better. (You might already know.)
tableView.scrollToColumn(anyColumn);
To scroll to the position that the column to be the center, the following approach might work.
public class TableViewUtil {
public static void centerColumn(TableView<?> tableView, TableColumn<?, ?> column) {
findScrollBar(tableView, Orientation.HORIZONTAL).ifPresent(scroll -> {
final double offset = getLeftOffset(tableView, column);
final double target = offset - tableView.getWidth() / 2.0 + column.getWidth() / 2.0;
scroll.setValue(Math.min(Math.max(target, scroll.getMin()), scroll.getMax()));
});
}
private static double getLeftOffset(TableView<?> tableView, TableColumn<?, ?> column) {
double offset = 0.0;
for (TableColumn<?,?> c: tableView.getColumns()) {
if (c == column) return offset;
if (c.isVisible()) offset += c.getWidth();
}
return offset;
}
private static Optional<ScrollBar> findScrollBar(TableView<?> tableView, Orientation orientation) {
return tableView.lookupAll(".scroll-bar").stream()
.filter(node -> node instanceof ScrollBar && ((ScrollBar)node).getOrientation() == orientation)
.map(node -> ((ScrollBar)node))
.findFirst();
}
}
For example in initialize(),
Platform.runLater(() -> TableViewUtil.centerColumn(tableView, anyColumn));
I use an extended class of BarChart to add vertical lines and text on top of bars. This works fine but I want to display a small text at the top right of the horizontal line.
I tried this without success:
public void addHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (horizontalMarkers.contains(marker)) return;
Line line = new Line();
line.setStroke(Color.RED);
marker.setNode(line);
getPlotChildren().add(line);
// Adding a label
Node text = new Text("average");
nodeMap.put(marker.getNode(), text);
getPlotChildren().add(text);
horizontalMarkers.add(marker);
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
for (Node bar : nodeMap.keySet()) {
Node text = nodeMap.get(bar);
text.relocate(bar.getBoundsInParent().getMinX() + bar.getBoundsInParent().getWidth()/2 - text.prefWidth(-1) / 2, bar.getBoundsInParent().getMinY() - 30);
}
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
}
}
What I'm doing wrong ?
You need to move the Text after you move the marker, i. e. your code integrated at the required position:
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
Node text = nodeMap.get(line);
text.relocate(line.getBoundsInParent().getMinX() + line.getBoundsInParent().getWidth()/2 - text.prefWidth(-1) / 2, line.getBoundsInParent().getMinY() - 30);
}
By the way, I suggest creating a dedicated marker class for that which holds the line and the text instead of using a "loose" map.
Whenever I click a JSlider it gets positioned one majorTick in the direction of the click instead of jumping to the spot I actually click. (If slider is at point 47 and I click 5 it'll jump to 37 instead of 5). Is there any way to change this while using JSliders, or do I have to use another datastructure?
As bizarre as this might seem, it's actually the Look and Feel which controls this behaviour. Take a look at BasicSliderUI, the method that you need to override is scrollDueToClickInTrack(int).
In order to set the value of the JSlider to the nearest value to where the user clicked on the track, you'd need to do some fancy pants translation between the mouse coordinates from getMousePosition() to a valid track value, taking into account the position of the Component, it's orientation, size and distance between ticks etc. Luckily, BasicSliderUI gives us two handy functions to do this: valueForXPosition(int xPos) and valueForYPosition(int yPos):
JSlider slider = new JSlider(JSlider.HORIZONTAL);
slider.setUI(new MetalSliderUI() {
protected void scrollDueToClickInTrack(int direction) {
// this is the default behaviour, let's comment that out
//scrollByBlock(direction);
int value = slider.getValue();
if (slider.getOrientation() == JSlider.HORIZONTAL) {
value = this.valueForXPosition(slider.getMousePosition().x);
} else if (slider.getOrientation() == JSlider.VERTICAL) {
value = this.valueForYPosition(slider.getMousePosition().y);
}
slider.setValue(value);
}
});
This question is kind of old, but I just ran across this problem myself. This is my solution:
JSlider slider = new JSlider(/* your options here if desired */) {
{
MouseListener[] listeners = getMouseListeners();
for (MouseListener l : listeners)
removeMouseListener(l); // remove UI-installed TrackListener
final BasicSliderUI ui = (BasicSliderUI) getUI();
BasicSliderUI.TrackListener tl = ui.new TrackListener() {
// this is where we jump to absolute value of click
#Override public void mouseClicked(MouseEvent e) {
Point p = e.getPoint();
int value = ui.valueForXPosition(p.x);
setValue(value);
}
// disable check that will invoke scrollDueToClickInTrack
#Override public boolean shouldScroll(int dir) {
return false;
}
};
addMouseListener(tl);
}
};
This behavior is derived from OS. Are you sure you want to redefine it and confuse users? I don't think so. ;)