LineChartPanel:

- support "synchron" charts
- reworked to make code easier to understand/maintain
- added some JavaBean properties to make it configurable in JFormDesigner
- fixes some bugs

removed `FlatAnimatorTest.LineChartPanel` and replaced it with `LineChartPanel`
This commit is contained in:
Karl Tauber
2025-03-12 18:35:56 +01:00
parent 34a7214dd8
commit 23fc3674c9
8 changed files with 308 additions and 616 deletions

View File

@@ -25,6 +25,7 @@ import java.awt.Insets;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.AbstractBorder; import javax.swing.border.AbstractBorder;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.AnimatedBorder; import com.formdev.flatlaf.util.AnimatedBorder;
import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.ColorFunctions;
@@ -92,7 +93,7 @@ public class FlatAnimatedBorderTest
private void initComponents() { private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
label3 = new JLabel(); label3 = new JLabel();
lineChartPanel = new FlatAnimatorTest.LineChartPanel(); lineChartPanel = new LineChartPanel();
fade1TextField = new JTextField(); fade1TextField = new JTextField();
fade1ChartColor = new FlatAnimatorTest.JChartColor(); fade1ChartColor = new FlatAnimatorTest.JChartColor();
fade2TextField = new JTextField(); fade2TextField = new JTextField();
@@ -118,7 +119,7 @@ public class FlatAnimatedBorderTest
// columns // columns
"[fill]" + "[fill]" +
"[fill]para" + "[fill]para" +
"[fill]", "[grow,fill]",
// rows // rows
"[]" + "[]" +
"[]" + "[]" +
@@ -136,7 +137,7 @@ public class FlatAnimatedBorderTest
//---- label3 ---- //---- label3 ----
label3.setText("Fade:"); label3.setText("Fade:");
add(label3, "cell 0 0"); add(label3, "cell 0 0");
add(lineChartPanel, "cell 2 0 1 12"); add(lineChartPanel, "cell 2 0 1 12,growy");
add(fade1TextField, "cell 0 1"); add(fade1TextField, "cell 0 1");
add(fade1ChartColor, "cell 1 1"); add(fade1ChartColor, "cell 1 1");
add(fade2TextField, "cell 0 2"); add(fade2TextField, "cell 0 2");
@@ -151,12 +152,12 @@ public class FlatAnimatedBorderTest
add(material2ChartColor, "cell 1 5"); add(material2ChartColor, "cell 1 5");
//---- material3TextField ---- //---- material3TextField ----
material3TextField.putClientProperty("FlatLaf.styleClass", "large"); material3TextField.putClientProperty(FlatClientProperties.STYLE_CLASS, "large");
add(material3TextField, "cell 0 6"); add(material3TextField, "cell 0 6");
add(material3ChartColor, "cell 1 6"); add(material3ChartColor, "cell 1 6");
//---- material4TextField ---- //---- material4TextField ----
material4TextField.putClientProperty("FlatLaf.styleClass", "large"); material4TextField.putClientProperty(FlatClientProperties.STYLE_CLASS, "large");
add(material4TextField, "cell 0 7"); add(material4TextField, "cell 0 7");
add(material4ChartColor, "cell 1 7"); add(material4ChartColor, "cell 1 7");
@@ -178,7 +179,7 @@ public class FlatAnimatedBorderTest
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JLabel label3; private JLabel label3;
private FlatAnimatorTest.LineChartPanel lineChartPanel; private LineChartPanel lineChartPanel;
private JTextField fade1TextField; private JTextField fade1TextField;
private FlatAnimatorTest.JChartColor fade1ChartColor; private FlatAnimatorTest.JChartColor fade1ChartColor;
private JTextField fade2TextField; private JTextField fade2TextField;
@@ -231,7 +232,7 @@ public class FlatAnimatedBorderTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "fade" );
} }
} }
@@ -295,7 +296,7 @@ public class FlatAnimatedBorderTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "material" );
} }
} }
@@ -410,7 +411,7 @@ public class FlatAnimatedBorderTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" );
} }
} }

View File

@@ -1,11 +1,11 @@
JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
root: new FormRoot { root: new FormRoot {
add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "insets dialog,hidemode 3" "$layoutConstraints": "insets dialog,hidemode 3"
"$columnConstraints": "[fill][fill]para[fill]" "$columnConstraints": "[fill][fill]para[grow,fill]"
"$rowConstraints": "[][][]para[][][][][]para[][][grow][]" "$rowConstraints": "[][][]para[][][][][]para[][][grow][]"
} ) { } ) {
name: "this" name: "this"
@@ -15,10 +15,10 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0" "value": "cell 0 0"
} ) } )
add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) {
name: "lineChartPanel" name: "lineChartPanel"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 0 1 12" "value": "cell 2 0 1 12,growy"
} ) } )
add( new FormComponent( "javax.swing.JTextField" ) { add( new FormComponent( "javax.swing.JTextField" ) {
name: "fade1TextField" name: "fade1TextField"
@@ -122,7 +122,7 @@ new FormModel {
} ) } )
}, new FormLayoutConstraints( null ) { }, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 ) "location": new java.awt.Point( 0, 0 )
"size": new java.awt.Dimension( 725, 325 ) "size": new java.awt.Dimension( 725, 465 )
} ) } )
} }
} }

View File

@@ -39,7 +39,7 @@ public class FlatAnimatedIconTest
private static final Color CHART_RADIO_BUTTON_3 = Color.green; private static final Color CHART_RADIO_BUTTON_3 = Color.green;
private static final Color CHART_CHECK_BOX_1 = Color.magenta; private static final Color CHART_CHECK_BOX_1 = Color.magenta;
private static final Color CHART_CHECK_BOX_2 = Color.orange; private static final Color CHART_CHECK_BOX_2 = Color.orange;
private static final Color[] CHART_SWITCH_EX = new Color[] { Color.red, Color.green, Color.blue }; private static final Color[] CHART_SWITCH_EX = { Color.red, Color.green, Color.blue };
private static final String CHART_COLOR_KEY = "chartColor"; private static final String CHART_COLOR_KEY = "chartColor";
@@ -79,7 +79,7 @@ public class FlatAnimatedIconTest
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
radioButton1 = new JRadioButton(); radioButton1 = new JRadioButton();
radioButton1ChartColor = new FlatAnimatorTest.JChartColor(); radioButton1ChartColor = new FlatAnimatorTest.JChartColor();
lineChartPanel = new FlatAnimatorTest.LineChartPanel(); lineChartPanel = new LineChartPanel();
radioButton2 = new JRadioButton(); radioButton2 = new JRadioButton();
radioButton2ChartColor = new FlatAnimatorTest.JChartColor(); radioButton2ChartColor = new FlatAnimatorTest.JChartColor();
radioButton3 = new JRadioButton(); radioButton3 = new JRadioButton();
@@ -114,7 +114,7 @@ public class FlatAnimatedIconTest
radioButton1.setSelected(true); radioButton1.setSelected(true);
add(radioButton1, "cell 0 0"); add(radioButton1, "cell 0 0");
add(radioButton1ChartColor, "cell 1 0"); add(radioButton1ChartColor, "cell 1 0");
add(lineChartPanel, "cell 2 0 1 7"); add(lineChartPanel, "cell 2 0 1 8,growy");
//---- radioButton2 ---- //---- radioButton2 ----
radioButton2.setText("radio 2"); radioButton2.setText("radio 2");
@@ -142,11 +142,11 @@ public class FlatAnimatedIconTest
//---- durationLabel ---- //---- durationLabel ----
durationLabel.setText("Duration:"); durationLabel.setText("Duration:");
add(durationLabel, "cell 0 7 3 1"); add(durationLabel, "cell 0 7 2 1");
//---- durationField ---- //---- durationField ----
durationField.setModel(new SpinnerNumberModel(200, 0, null, 50)); durationField.setModel(new SpinnerNumberModel(200, 0, null, 50));
add(durationField, "cell 0 7 3 1"); add(durationField, "cell 0 7 2 1");
//---- buttonGroup1 ---- //---- buttonGroup1 ----
ButtonGroup buttonGroup1 = new ButtonGroup(); ButtonGroup buttonGroup1 = new ButtonGroup();
@@ -159,7 +159,7 @@ public class FlatAnimatedIconTest
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JRadioButton radioButton1; private JRadioButton radioButton1;
private FlatAnimatorTest.JChartColor radioButton1ChartColor; private FlatAnimatorTest.JChartColor radioButton1ChartColor;
private FlatAnimatorTest.LineChartPanel lineChartPanel; private LineChartPanel lineChartPanel;
private JRadioButton radioButton2; private JRadioButton radioButton2;
private FlatAnimatorTest.JChartColor radioButton2ChartColor; private FlatAnimatorTest.JChartColor radioButton2ChartColor;
private JRadioButton radioButton3; private JRadioButton radioButton3;
@@ -216,7 +216,7 @@ public class FlatAnimatedIconTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "radio" );
} }
} }
@@ -261,7 +261,7 @@ public class FlatAnimatedIconTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "switch" );
} }
} }
@@ -328,7 +328,7 @@ public class FlatAnimatedIconTest
for( int i = 0; i < animatedValues.length; i++ ) { for( int i = 0; i < animatedValues.length; i++ ) {
float animatedValue = animatedValues[i]; float animatedValue = animatedValues[i];
if( animatedValue != 0 && animatedValue != 1 ) if( animatedValue != 0 && animatedValue != 1 )
lineChartPanel.lineChart.addValue( animatedValue, CHART_SWITCH_EX[i] ); lineChartPanel.addValue( CHART_SWITCH_EX[i], animatedValue, Integer.MIN_VALUE, "switch ex" );
} }
} }
@@ -387,7 +387,7 @@ public class FlatAnimatedIconTest
if( animatedValue != 0 && animatedValue != 1 ) { if( animatedValue != 0 && animatedValue != 1 ) {
Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY );
lineChartPanel.lineChart.addValue( animatedValue, chartColor ); lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" );
} }
} }

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -22,10 +22,10 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 0" "value": "cell 1 0"
} ) } )
add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) {
name: "lineChartPanel" name: "lineChartPanel"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 0 1 7" "value": "cell 2 0 1 8,growy"
} ) } )
add( new FormComponent( "javax.swing.JRadioButton" ) { add( new FormComponent( "javax.swing.JRadioButton" ) {
name: "radioButton2" name: "radioButton2"
@@ -83,7 +83,7 @@ new FormModel {
name: "durationLabel" name: "durationLabel"
"text": "Duration:" "text": "Duration:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 7 3 1" "value": "cell 0 7 2 1"
} ) } )
add( new FormComponent( "javax.swing.JSpinner" ) { add( new FormComponent( "javax.swing.JSpinner" ) {
name: "durationField" name: "durationField"
@@ -93,7 +93,7 @@ new FormModel {
value: 200 value: 200
} }
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 7 3 1" "value": "cell 0 7 2 1"
} ) } )
}, new FormLayoutConstraints( null ) { }, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 ) "location": new java.awt.Point( 0, 0 )

View File

@@ -17,17 +17,9 @@
package com.formdev.flatlaf.testing; package com.formdev.flatlaf.testing;
import java.awt.*; import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*; import javax.swing.*;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.ui.FlatUIUtils;
import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.Animator;
import com.formdev.flatlaf.util.CubicBezierEasing; import com.formdev.flatlaf.util.CubicBezierEasing;
import com.formdev.flatlaf.util.HSLColor;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.UIScale; import com.formdev.flatlaf.util.UIScale;
import com.formdev.flatlaf.util.Animator.Interpolator; import com.formdev.flatlaf.util.Animator.Interpolator;
import net.miginfocom.swing.*; import net.miginfocom.swing.*;
@@ -74,7 +66,7 @@ public class FlatAnimatorTest
} else { } else {
animator = new Animator( 1000, fraction -> { animator = new Animator( 1000, fraction -> {
scrollBar.setValue( Math.round( fraction * scrollBar.getMaximum() ) ); scrollBar.setValue( Math.round( fraction * scrollBar.getMaximum() ) );
lineChartPanel.lineChart.addValue( fraction, chartColor ); lineChartPanel.addValue( chartColor, fraction, Integer.MIN_VALUE, "animator" );
} ); } );
animator.setInterpolator( interpolator ); animator.setInterpolator( interpolator );
animator.start(); animator.start();
@@ -94,7 +86,7 @@ public class FlatAnimatorTest
standardEasingChartColor = new FlatAnimatorTest.JChartColor(); standardEasingChartColor = new FlatAnimatorTest.JChartColor();
standardEasingScrollBar = new JScrollBar(); standardEasingScrollBar = new JScrollBar();
startButton = new JButton(); startButton = new JButton();
lineChartPanel = new FlatAnimatorTest.LineChartPanel(); lineChartPanel = new LineChartPanel();
//======== this ======== //======== this ========
setLayout(new MigLayout( setLayout(new MigLayout(
@@ -159,420 +151,9 @@ public class FlatAnimatorTest
private FlatAnimatorTest.JChartColor standardEasingChartColor; private FlatAnimatorTest.JChartColor standardEasingChartColor;
private JScrollBar standardEasingScrollBar; private JScrollBar standardEasingScrollBar;
private JButton startButton; private JButton startButton;
private FlatAnimatorTest.LineChartPanel lineChartPanel; private LineChartPanel lineChartPanel;
// JFormDesigner - End of variables declaration //GEN-END:variables // JFormDesigner - End of variables declaration //GEN-END:variables
//---- class LineChartPanel -----------------------------------------------
static class LineChartPanel
extends JPanel
{
LineChartPanel() {
initComponents();
secondsWidthSlider.setValue( lineChart.getSecondsWidth() );
updateChartDelayedChanged();
}
void setSecondsWidth( int secondsWidth ) {
lineChart.setSecondsWidth( secondsWidth );
secondsWidthSlider.setValue( secondsWidth );
}
private void secondsWidthChanged() {
lineChart.setSecondsWidth( secondsWidthSlider.getValue() );
}
private void updateChartDelayedChanged() {
lineChart.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() );
}
private void clearChart() {
lineChart.clear();
}
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
JScrollPane lineChartScrollPane = new JScrollPane();
lineChart = new FlatAnimatorTest.LineChart();
JLabel lineChartInfoLabel = new JLabel();
secondsWidthSlider = new JSlider();
updateChartDelayedCheckBox = new JCheckBox();
JButton clearChartButton = new JButton();
//======== this ========
setLayout(new MigLayout(
"ltr,insets 0,hidemode 3",
// columns
"[fill]" +
"[grow,fill]",
// rows
"[400,grow,fill]" +
"[]"));
//======== lineChartScrollPane ========
{
lineChartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false);
lineChartScrollPane.setViewportView(lineChart);
}
add(lineChartScrollPane, "cell 0 0 2 1");
//---- lineChartInfoLabel ----
lineChartInfoLabel.setText("X: time (500ms per line) / Y: value (10% per line)");
add(lineChartInfoLabel, "cell 0 1 2 1");
//---- secondsWidthSlider ----
secondsWidthSlider.setMinimum(100);
secondsWidthSlider.setMaximum(2000);
secondsWidthSlider.addChangeListener(e -> secondsWidthChanged());
add(secondsWidthSlider, "cell 0 1 2 1");
//---- updateChartDelayedCheckBox ----
updateChartDelayedCheckBox.setText("Update chart delayed");
updateChartDelayedCheckBox.setMnemonic('U');
updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged());
add(updateChartDelayedCheckBox, "cell 0 1 2 1,alignx right,growx 0");
//---- clearChartButton ----
clearChartButton.setText("Clear Chart");
clearChartButton.setMnemonic('C');
clearChartButton.addActionListener(e -> clearChart());
add(clearChartButton, "cell 0 1 2 1,alignx right,growx 0");
// JFormDesigner - End of component initialization //GEN-END:initComponents
}
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
FlatAnimatorTest.LineChart lineChart;
private JSlider secondsWidthSlider;
private JCheckBox updateChartDelayedCheckBox;
// JFormDesigner - End of variables declaration //GEN-END:variables
}
//---- class LineChart ----------------------------------------------------
static class LineChart
extends JComponent
implements Scrollable
{
private static final int NEW_SEQUENCE_TIME_LAG = 500;
private static final int NEW_SEQUENCE_GAP = 100;
private boolean asynchron;
private int secondsWidth = 500;
private static class Data {
final double value;
final boolean dot;
final Color chartColor;
final long time; // in milliseconds
Data( double value, boolean dot, Color chartColor, long time ) {
this.value = value;
this.dot = dot;
this.chartColor = chartColor;
this.time = time;
}
@Override
public String toString() {
// for debugging
return String.valueOf( value );
}
}
private final List<Data> syncChartData = new ArrayList<>();
private final Map<Color, List<Data>> asyncColor2dataMap = new HashMap<>();
private final Timer repaintTime;
private Color lastUsedChartColor;
private boolean updateDelayed;
LineChart() {
repaintTime = new Timer( 20, e -> repaintAndRevalidate() );
repaintTime.setRepeats( false );
}
void enableAsynchron() {
if( !syncChartData.isEmpty() )
throw new IllegalStateException();
asynchron = true;
}
void addValue( double value, Color chartColor ) {
addValue( value, false, chartColor );
}
void addValue( double value, boolean dot, Color chartColor ) {
List<Data> chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() );
Data data = new Data( value, dot, chartColor, System.nanoTime() / 1000000 );
if( asynchron )
chartData.add( data );
else
syncChartData.add( data );
lastUsedChartColor = chartColor;
if( updateDelayed ) {
repaintTime.stop();
repaintTime.start();
} else
repaintAndRevalidate();
}
void clear() {
syncChartData.clear();
asyncColor2dataMap.clear();
lastUsedChartColor = null;
repaint();
revalidate();
}
void setUpdateDelayed( boolean updateDelayed ) {
this.updateDelayed = updateDelayed;
}
int getSecondsWidth() {
return secondsWidth;
}
void setSecondsWidth( int secondsWidth ) {
this.secondsWidth = secondsWidth;
repaint();
revalidate();
}
private void repaintAndRevalidate() {
repaint();
revalidate();
// scroll horizontally
if( lastUsedChartColor != null ) {
// compute chart width of last used color and start of last sequence
int[] lastSeqX = new int[1];
int cw = chartWidth( asynchron ? asyncColor2dataMap.get( lastUsedChartColor ) : syncChartData, lastSeqX );
// scroll to end of last sequence (of last used color)
int lastSeqWidth = cw - lastSeqX[0];
int width = Math.min( lastSeqWidth, getParent().getWidth() );
int x = cw - width;
scrollRectToVisible( new Rectangle( x, 0, width, getHeight() ) );
}
}
@Override
protected void paintComponent( Graphics g ) {
Graphics g2 = g.create();
try {
HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintImpl );
} finally {
g2.dispose();
}
}
private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
FlatUIUtils.setRenderingHints( g );
int secondsWidth = (int) (this.secondsWidth * scaleFactor);
Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray );
Color lineColor2 = FlatLaf.isLafDark()
? new HSLColor( lineColor ).adjustTone( 30 )
: new HSLColor( lineColor ).adjustShade( 30 );
g.translate( x, y );
// fill background
g.setColor( UIManager.getColor( "Table.background" ) );
g.fillRect( x, y, width, height );
// paint horizontal lines
for( int i = 1; i < 10; i++ ) {
int hy = (height * i) / 10;
g.setColor( (i != 5) ? lineColor : lineColor2 );
g.drawLine( 0, hy, width, hy );
}
// paint vertical lines
int twoHundredMillisWidth = secondsWidth / 5;
for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) {
g.setColor( (i % secondsWidth != 0) ? lineColor : lineColor2 );
g.drawLine( i, 0, i, height );
}
// paint lines
for( Map.Entry<Color, List<Data>> e : asyncColor2dataMap.entrySet() ) {
List<Data> chartData = asynchron ? e.getValue() : syncChartData;
Color chartColor = e.getKey();
paintChartData( g, chartData, chartColor, height, scaleFactor );
}
}
private void paintChartData( Graphics2D g, List<Data> chartData, Color chartColor, int height, double scaleFactor ) {
if( FlatLaf.isLafDark() )
chartColor = new HSLColor( chartColor ).adjustTone( 50 );
Color temporaryValueColor = new Color( (chartColor.getRGB() & 0xffffff) | 0x40000000, true );
int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor);
long seqTime = 0;
int seqX = 0;
long ptime = 0;
int px = 0;
int py = height - 1;
int cx = px;
int cy = py;
int pcount = 0;
g.setColor( chartColor );
boolean first = true;
int size = chartData.size();
for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i );
boolean useData = (data.chartColor == chartColor);
int dy = height - 1 - (int) ((height - 1) * data.value);
if( data.dot ) {
int dotx = px;
if( i > 0 && data.time > ptime + NEW_SEQUENCE_TIME_LAG )
dotx += seqGapWidth;
int o = UIScale.scale( 1 );
int s = UIScale.scale( 3 );
g.fillRect( dotx - o, dy - o, s, s );
continue;
}
if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) {
if( !first && pcount == 0 )
g.drawLine( px, py, px + (int) (4 * scaleFactor), py );
// start new sequence
seqTime = data.time;
seqX = !first ? px + seqGapWidth : 0;
px = seqX;
pcount = 0;
first = false;
} else {
boolean isTemporaryValue = isTemporaryValue( chartData, i ) || isTemporaryValue( chartData, i - 1 );
if( isTemporaryValue )
g.setColor( temporaryValueColor );
// line in sequence
int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondsWidth));
if( useData )
g.drawLine( cx, cy, dx, dy );
px = dx;
pcount++;
if( isTemporaryValue )
g.setColor( chartColor );
}
py = dy;
ptime = data.time;
if( useData ) {
cx = px;
cy = py;
}
}
}
/**
* One or two values between two equal values are considered "temporary",
* which means that they are the target value for the following scroll animation.
*/
private boolean isTemporaryValue( List<Data> chartData, int i ) {
if( i == 0 || i == chartData.size() - 1 )
return false;
Data dataBefore = chartData.get( i - 1 );
Data dataAfter = chartData.get( i + 1 );
if( dataBefore.dot || dataAfter.dot )
return false;
double valueBefore = dataBefore.value;
double valueAfter = dataAfter.value;
return valueBefore == valueAfter ||
(i < chartData.size() - 2 && valueBefore == chartData.get( i + 2 ).value) ||
(i > 1 && chartData.get( i - 2 ).value == valueAfter);
}
private int chartWidth() {
int width = 0;
if( asynchron ) {
for( List<Data> chartData : asyncColor2dataMap.values() )
width = Math.max( width, chartWidth( chartData, null ) );
} else
width = Math.max( width, chartWidth( syncChartData, null ) );
return width;
}
private int chartWidth( List<Data> chartData, int[] lastSeqX ) {
long seqTime = 0;
int seqX = 0;
long ptime = 0;
int px = 0;
int size = chartData.size();
for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i );
if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) {
// start new sequence
seqTime = data.time;
seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0;
px = seqX;
} else {
// line in sequence
int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondsWidth));
px = dx;
}
ptime = data.time;
}
if( lastSeqX != null )
lastSeqX[0] = seqX;
return px;
}
@Override
public Dimension getPreferredSize() {
return new Dimension( chartWidth(), 200 );
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension( chartWidth(), 200 );
}
@Override
public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) {
return secondsWidth;
}
@Override
public int getScrollableBlockIncrement( Rectangle visibleRect, int orientation, int direction ) {
JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this );
return (viewport != null) ? viewport.getWidth() : 200;
}
@Override
public boolean getScrollableTracksViewportWidth() {
JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this );
return (viewport != null) ? viewport.getWidth() > chartWidth() : true;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return true;
}
}
//---- class JChartColor -------------------------------------------------- //---- class JChartColor --------------------------------------------------
static class JChartColor static class JChartColor

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "7.0.5.0.382" Java: "16" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -103,7 +103,7 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 3" "value": "cell 0 3"
} ) } )
add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChartPanel" ) { add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) {
name: "lineChartPanel" name: "lineChartPanel"
auxiliary() { auxiliary() {
"JavaCodeGenerator.variableLocal": false "JavaCodeGenerator.variableLocal": false
@@ -115,67 +115,5 @@ new FormModel {
"location": new java.awt.Point( 0, 0 ) "location": new java.awt.Point( 0, 0 )
"size": new java.awt.Dimension( 625, 625 ) "size": new java.awt.Dimension( 625, 625 )
} ) } )
add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$columnConstraints": "[fill][grow,fill]"
"$rowConstraints": "[400,grow,fill][]"
"$layoutConstraints": "ltr,insets 0,hidemode 3"
} ) {
name: "lineChartPanelNested"
auxiliary() {
"JavaCodeGenerator.className": "LineChartPanel"
}
add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) {
name: "lineChartScrollPane"
"$client.JScrollPane.smoothScrolling": false
add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$LineChart" ) {
name: "lineChart"
auxiliary() {
"JavaCodeGenerator.variableLocal": false
"JavaCodeGenerator.variableModifiers": 0
}
} )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0 2 1"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "lineChartInfoLabel"
"text": "X: time (500ms per line) / Y: value (10% per line)"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1 2 1"
} )
add( new FormComponent( "javax.swing.JSlider" ) {
name: "secondsWidthSlider"
"minimum": 100
"maximum": 2000
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "secondsWidthChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1 2 1"
} )
add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "updateChartDelayedCheckBox"
"text": "Update chart delayed"
"mnemonic": 85
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1 2 1,alignx right,growx 0"
} )
add( new FormComponent( "javax.swing.JButton" ) {
name: "clearChartButton"
"text": "Clear Chart"
"mnemonic": 67
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1 2 1,alignx right,growx 0"
} )
}, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 650 )
"size": new java.awt.Dimension( 603, 325 )
} )
} }
} }

View File

@@ -77,6 +77,33 @@ class LineChartPanel
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
} }
public boolean isYZeroAtTop() {
return lineChart.yZeroAtTop;
}
public void setYZeroAtTop( boolean yZeroAtTop ) {
lineChart.yZeroAtTop = yZeroAtTop;
lineChart.repaint();
}
public boolean isAsynchron() {
return lineChart.asynchron;
}
public void setAsynchron( boolean asynchron ) {
lineChart.asynchron = asynchron;
lineChart.repaint();
}
public boolean isTemporaryValueDetection() {
return lineChart.temporaryValueDetection;
}
public void setTemporaryValueDetection( boolean temporaryValueDetection ) {
lineChart.temporaryValueDetection = temporaryValueDetection;
lineChart.repaint();
}
public String getLegendYValueText() { public String getLegendYValueText() {
return yValueLabel.getText(); return yValueLabel.getText();
} }
@@ -101,12 +128,21 @@ class LineChartPanel
legend2Label.setText( s ); legend2Label.setText( s );
} }
public int getOneSecondWidth() {
return oneSecondWidthSlider.getValue();
}
public void setOneSecondWidth( int oneSecondWidth ) {
oneSecondWidthSlider.setValue( oneSecondWidth );
}
public boolean isUpdateChartDelayed() { public boolean isUpdateChartDelayed() {
return updateChartDelayedCheckBox.isSelected(); return updateChartDelayedCheckBox.isSelected();
} }
public void setUpdateChartDelayed( boolean updateChartDelayed ) { public void setUpdateChartDelayed( boolean updateChartDelayed ) {
updateChartDelayedCheckBox.setSelected( updateChartDelayed ); updateChartDelayedCheckBox.setSelected( updateChartDelayed );
updateChartDelayedChanged();
} }
void addValue( Color chartColor, double value, int ivalue, String name ) { void addValue( Color chartColor, double value, int ivalue, String name ) {
@@ -137,8 +173,8 @@ class LineChartPanel
oneSecondWidth <= 8000 ? 25 : oneSecondWidth <= 8000 ? 25 :
10; 10;
lineChart.setOneSecondWidth( oneSecondWidth ); lineChart.oneSecondWidth = oneSecondWidth;
lineChart.setMsPerLineX( msPerLineX ); lineChart.msPerLineX = msPerLineX;
lineChart.revalidate(); lineChart.revalidate();
lineChart.repaint(); lineChart.repaint();
@@ -149,7 +185,7 @@ class LineChartPanel
private String xLabelText; private String xLabelText;
private void updateChartDelayedChanged() { private void updateChartDelayedChanged() {
lineChart.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); lineChart.updateDelayed = updateChartDelayedCheckBox.isSelected();
} }
private void clearChart() { private void clearChart() {
@@ -227,15 +263,17 @@ class LineChartPanel
add(oneSecondWidthLabel, "cell 0 1,alignx right,growx 0"); add(oneSecondWidthLabel, "cell 0 1,alignx right,growx 0");
//---- oneSecondWidthSlider ---- //---- oneSecondWidthSlider ----
oneSecondWidthSlider.setMinimum(1000); oneSecondWidthSlider.setMinimum(100);
oneSecondWidthSlider.setMaximum(10000); oneSecondWidthSlider.setMaximum(10000);
oneSecondWidthSlider.setSnapToTicks(true);
oneSecondWidthSlider.setMajorTickSpacing(100);
oneSecondWidthSlider.setValue(500);
oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged());
add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0,wmax 100"); add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0");
//---- updateChartDelayedCheckBox ---- //---- updateChartDelayedCheckBox ----
updateChartDelayedCheckBox.setText("Update chart delayed"); updateChartDelayedCheckBox.setText("Update chart delayed");
updateChartDelayedCheckBox.setMnemonic('P'); updateChartDelayedCheckBox.setMnemonic('P');
updateChartDelayedCheckBox.setSelected(true);
updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged());
add(updateChartDelayedCheckBox, "cell 0 1,alignx right,growx 0"); add(updateChartDelayedCheckBox, "cell 0 1,alignx right,growx 0");
@@ -267,27 +305,36 @@ class LineChartPanel
implements Scrollable implements Scrollable
{ {
private static final int UPDATE_DELAY_MS = 20; private static final int UPDATE_DELAY_MS = 20;
private static final int NEW_SEQUENCE_TIME_LAG_MS = 500;
private static final int NEW_SEQUENCE_TIME_LAG = 500; private static final int NEW_SEQUENCE_GAP_MS = 100;
private static final int NEW_SEQUENCE_GAP = 100;
private static final int HIT_OFFSET = 4; private static final int HIT_OFFSET = 4;
private int oneSecondWidth = 1000; private static final boolean TEST = false;
private int msPerLineX = 200;
// asynchron means that chart for each color starts at x=0
private boolean asynchron;
private boolean temporaryValueDetection;
private boolean yZeroAtTop;
private int oneSecondWidth = 500;
private int msPerLineX = 100;
private final HashMap<String, String> methodHighlightMap = new HashMap<>(); private final HashMap<String, String> methodHighlightMap = new HashMap<>();
private static class Data { private static class Data {
final double value; final double value;
final int ivalue; final int ivalue;
final Color chartColor;
final Color dotColor; final Color dotColor;
final boolean dotOnly; final boolean dotOnly;
final long time; // in milliseconds final long time; // in milliseconds
final String name; final String name;
final Exception stack; final Exception stack;
Data( double value, int ivalue, Color dotColor, boolean dotOnly, long time, String name, Exception stack ) { Data( double value, int ivalue, Color chartColor, Color dotColor,
boolean dotOnly, long time, String name, Exception stack )
{
this.value = value; this.value = value;
this.ivalue = ivalue; this.ivalue = ivalue;
this.chartColor = chartColor;
this.dotColor = dotColor; this.dotColor = dotColor;
this.dotOnly = dotOnly; this.dotOnly = dotOnly;
this.time = time; this.time = time;
@@ -303,7 +350,8 @@ class LineChartPanel
} }
} }
private final Map<Color, List<Data>> color2dataMap = new HashMap<>(); private final List<Data> syncChartData = new ArrayList<>();
private final Map<Color, List<Data>> asyncColor2dataMap = new HashMap<>();
private final Timer repaintTime; private final Timer repaintTime;
private Color lastUsedChartColor; private Color lastUsedChartColor;
private boolean updateDelayed; private boolean updateDelayed;
@@ -318,11 +366,21 @@ class LineChartPanel
repaintTime.setRepeats( false ); repaintTime.setRepeats( false );
ToolTipManager.sharedInstance().registerComponent( this ); ToolTipManager.sharedInstance().registerComponent( this );
if( TEST )
initTestData();
} }
void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) { void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) {
List<Data> chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); if( TEST )
chartData.add( new Data( value, ivalue, dotColor, dotOnly, System.nanoTime() / 1000000, name, new Exception() ) ); return;
List<Data> chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() );
Data data = new Data( value, ivalue, chartColor, dotColor, dotOnly, System.nanoTime() / 1_000_000, name, new Exception() );
if( asynchron )
chartData.add( data );
else
syncChartData.add( data );
lastUsedChartColor = chartColor; lastUsedChartColor = chartColor;
@@ -334,25 +392,19 @@ class LineChartPanel
} }
void clear() { void clear() {
color2dataMap.clear(); if( TEST ) {
repaint();
return;
}
syncChartData.clear();
asyncColor2dataMap.clear();
lastUsedChartColor = null; lastUsedChartColor = null;
repaint(); repaint();
revalidate(); revalidate();
} }
void setUpdateDelayed( boolean updateDelayed ) {
this.updateDelayed = updateDelayed;
}
void setOneSecondWidth( int oneSecondWidth ) {
this.oneSecondWidth = oneSecondWidth;
}
void setMsPerLineX( int msPerLineX ) {
this.msPerLineX = msPerLineX;
}
private void repaintAndRevalidate() { private void repaintAndRevalidate() {
repaint(); repaint();
revalidate(); revalidate();
@@ -361,7 +413,7 @@ class LineChartPanel
if( lastUsedChartColor != null ) { if( lastUsedChartColor != null ) {
// compute chart width of last used color and start of last sequence // compute chart width of last used color and start of last sequence
int[] lastSeqX = new int[1]; int[] lastSeqX = new int[1];
int cw = chartWidth( color2dataMap.get( lastUsedChartColor ), lastSeqX ); int cw = chartWidth( asynchron ? asyncColor2dataMap.get( lastUsedChartColor ) : syncChartData, lastSeqX );
// scroll to end of last sequence (of last used color) // scroll to end of last sequence (of last used color)
int lastSeqWidth = cw - lastSeqX[0]; int lastSeqWidth = cw - lastSeqX[0];
@@ -375,17 +427,17 @@ class LineChartPanel
protected void paintComponent( Graphics g ) { protected void paintComponent( Graphics g ) {
Graphics g2 = g.create(); Graphics g2 = g.create();
try { try {
HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintImpl ); HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintAt1x );
} finally { } finally {
g2.dispose(); g2.dispose();
} }
} }
private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { private void paintAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) {
FlatUIUtils.setRenderingHints( g ); FlatUIUtils.setRenderingHints( g );
int oneSecondWidth = (int) (UIScale.scale( this.oneSecondWidth ) * scaleFactor); int oneSecondWidth = (int) (UIScale.scale( this.oneSecondWidth ) * scaleFactor);
int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000;
int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * scaleFactor ); int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * scaleFactor );
Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray );
@@ -418,8 +470,8 @@ class LineChartPanel
lastSystemScaleFactor = scaleFactor; lastSystemScaleFactor = scaleFactor;
// paint lines // paint lines
for( Map.Entry<Color, List<Data>> e : color2dataMap.entrySet() ) { for( Map.Entry<Color, List<Data>> e : asyncColor2dataMap.entrySet() ) {
List<Data> chartData = e.getValue(); List<Data> chartData = asynchron ? e.getValue() : syncChartData;
Color chartColor = e.getKey(); Color chartColor = e.getKey();
if( FlatLaf.isLafDark() ) if( FlatLaf.isLafDark() )
chartColor = new HSLColor( chartColor ).adjustTone( 50 ); chartColor = new HSLColor( chartColor ).adjustTone( 50 );
@@ -427,77 +479,91 @@ class LineChartPanel
Color dataPointColor = fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); Color dataPointColor = fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f );
// sequence start time and x coordinate // sequence start time and x coordinate
long seqTime = 0; long seqStartTime = 0;
int seqX = 0; int seqStartX = 0;
// "previous" data point time, x/y coordinates and count // "previous" data point time and x coordinate (used for "new sequence" detection)
long ptime = 0; long ptime = Long.MIN_VALUE;
int px = 0; int px = 0;
int py = 0;
int pcount = 0;
boolean first = true; // "line" data point x/y coordinates
int lx = -1;
int ly = -1;
boolean isTemporaryValue = false; boolean isTemporaryValue = false;
int lastTemporaryValueIndex = -1; int lastTemporaryValueIndex = -1;
int size = chartData.size(); int size = chartData.size();
for( int i = 0; i < size; i++ ) { for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i ); Data data = chartData.get( i );
boolean useData = (data.chartColor == chartColor);
boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG); // start new sequence if there is a larger time gap to previous data point
boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS);
ptime = data.time; ptime = data.time;
if( newSeq ) { if( newSeq ) {
// paint short horizontal line for previous sequence that has only one data point
if( !first && pcount == 0 ) {
g.setColor( chartColor );
g.drawLine( px, py, px + (int) Math.round( UIScale.scale( 8 ) * scaleFactor ), py );
}
// start new sequence // start new sequence
seqTime = data.time; seqStartTime = data.time;
seqX = !first ? px + seqGapWidth : 0; seqStartX = (i > 0) ? px + seqGapWidth : 0;
px = seqX; px = seqStartX;
pcount = 0; lx = -1;
first = false; ly = -1;
isTemporaryValue = false; isTemporaryValue = false;
} }
// x/y coordinates of current data point // x/y coordinates of current data point
int dx = (int) (seqStartX + (((data.time - seqStartTime) / 1000.) * oneSecondWidth));
int dy = (int) ((height - 1) * data.value); int dy = (int) ((height - 1) * data.value);
int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); if( !yZeroAtTop )
dy = height - 1 - dy;
// paint rectangle to indicate data point // remember x coordinate for "new sequence" detection
g.setColor( dataPointColor ); px = dx;
g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 );
if( !useData )
continue;
// remember data point for tooltip // remember data point for tooltip
lastPoints.add( new Point( dx, dy ) ); lastPoints.add( new Point( dx, dy ) );
lastDatas.add( data ); lastDatas.add( data );
// paint rectangle to indicate data point
g.setColor( dataPointColor );
g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 );
// paint dot
if( data.dotColor != null ) { if( data.dotColor != null ) {
int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor );
int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor );
g.setColor( data.dotColor ); g.setColor( data.dotColor );
g.fillRect( dx - s1, dy - s1, s3, s3 ); g.fillRect( dx - s1, dy - s1, s3, s3 );
if( data.dotOnly ) if( data.dotOnly )
continue; continue;
} }
if( !newSeq ) { // start of line?
if( lx < 0 ) {
// remember x/y coordinates for first line
lx = dx;
ly = dy;
continue;
}
if( isTemporaryValue && i > lastTemporaryValueIndex ) if( isTemporaryValue && i > lastTemporaryValueIndex )
isTemporaryValue = false; isTemporaryValue = false;
// draw line in sequence
g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); g.setColor( isTemporaryValue ? temporaryValueColor : chartColor );
g.drawLine( lx, ly, dx, dy );
// line in sequence // remember x/y coordinates for next line
g.drawLine( px, py, dx, dy ); lx = dx;
ly = dy;
px = dx;
pcount++;
// check next data points for "temporary" value(s) // check next data points for "temporary" value(s)
if( !isTemporaryValue ) { if( temporaryValueDetection && !isTemporaryValue ) {
// one or two values between two equal values are considered "temporary", // one or two values between two equal values are considered "temporary",
// which means that they are the target value for the following scroll animation // which means that they are the target value for the following scroll animation
int stage = 0; int stage = 0;
@@ -518,16 +584,16 @@ class LineChartPanel
} }
} }
} }
py = dy;
}
} }
} }
private int chartWidth() { private int chartWidth() {
int width = 0; int width = 0;
for( List<Data> chartData : color2dataMap.values() ) if( asynchron ) {
for( List<Data> chartData : asyncColor2dataMap.values() )
width = Math.max( width, chartWidth( chartData, null ) ); width = Math.max( width, chartWidth( chartData, null ) );
} else
width = Math.max( width, chartWidth( syncChartData, null ) );
return width; return width;
} }
@@ -536,19 +602,21 @@ class LineChartPanel
int seqX = 0; int seqX = 0;
long ptime = 0; long ptime = 0;
int px = 0; int px = 0;
int oneSecondWidth = UIScale.scale( this.oneSecondWidth );
int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000;
int size = chartData.size(); int size = chartData.size();
for( int i = 0; i < size; i++ ) { for( int i = 0; i < size; i++ ) {
Data data = chartData.get( i ); Data data = chartData.get( i );
if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { if( data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS ) {
// start new sequence // start new sequence
seqTime = data.time; seqTime = data.time;
seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0; seqX = (i > 0) ? px + seqGapWidth : 0;
px = seqX; px = seqX;
} else { } else {
// line in sequence // line in sequence
int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * UIScale.scale( oneSecondWidth ))); int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth ));
px = dx; px = dx;
} }
@@ -618,7 +686,11 @@ class LineChartPanel
buf.append( "<h2>" ); buf.append( "<h2>" );
if( data.dotOnly ) if( data.dotOnly )
buf.append( "DOT: " ); buf.append( "DOT: " );
buf.append( data.name ).append( ' ' ).append( data.ivalue ).append( "</h2>" ); buf.append( data.name );
if( data.ivalue != Integer.MIN_VALUE )
buf.append( ' ' ).append( data.ivalue );
buf.append( " (" ).append( String.format( "%.3f", data.value ) ).append( ')' );
buf.append( "</h2>" );
StackTraceElement[] stackTrace = data.stack.getStackTrace(); StackTraceElement[] stackTrace = data.stack.getStackTrace();
for( int j = 0; j < stackTrace.length; j++ ) { for( int j = 0; j < stackTrace.length; j++ ) {
@@ -678,6 +750,7 @@ class LineChartPanel
classAndMethod.equals( "java.awt.Component.processMouseWheelEvent" ) || classAndMethod.equals( "java.awt.Component.processMouseWheelEvent" ) ||
classAndMethod.equals( "java.awt.Component.processMouseMotionEvent" ) || classAndMethod.equals( "java.awt.Component.processMouseMotionEvent" ) ||
classAndMethod.equals( "javax.swing.JComponent.processKeyBinding" ) || classAndMethod.equals( "javax.swing.JComponent.processKeyBinding" ) ||
classAndMethod.equals( "javax.swing.JComponent.paintComponent" ) ||
classAndMethod.equals( "com.formdev.flatlaf.util.Animator.timingEvent" ) ) classAndMethod.equals( "com.formdev.flatlaf.util.Animator.timingEvent" ) )
break; break;
} }
@@ -704,6 +777,103 @@ class LineChartPanel
return buf.toString(); return buf.toString();
} }
private void initTestData() {
// asynchron = true;
addTestSimpleLine( Color.red, 0.0, "red" );
addTestSimpleLine( Color.green, 0.1, "green" );
addTestSimpleLine( Color.blue, 0.2, "blue" );
addTestSimpleLine( Color.magenta, 0.3, "magenta" );
addTestMiddleDotOnly( Color.red, 0.0, "red" );
addTestMiddleDotOnly( Color.green, 0.1, "green" );
addTestMiddleDotOnly( Color.blue, 0.2, "blue" );
addTestMiddleDotOnly( Color.magenta, 0.3, "magenta" );
addTestLeadingDotOnly( Color.red, 0.0, "red" );
addTestLeadingDotOnly( Color.green, 0.1, "green" );
addTestLeadingDotOnly( Color.blue, 0.2, "blue" );
addTestLeadingDotOnly( Color.magenta, 0.3, "magenta" );
addTestTrailingDotOnly( Color.red, 0.0, "red" );
addTestTrailingDotOnly( Color.green, 0.1, "green" );
addTestTrailingDotOnly( Color.blue, 0.2, "blue" );
addTestTrailingDotOnly( Color.magenta, 0.3, "magenta" );
addTestSingleData( Color.red, 0.0, "red" );
addTestSingleData( Color.green, 0.1, "green" );
addTestSingleData( Color.blue, 0.2, "blue" );
addTestSingleData( Color.magenta, 0.3, "magenta" );
temporaryValueDetection = true;
addTestWithTemporaryValues( Color.red, 0.0, "red" );
addTestWithTemporaryValues( Color.green, 0.1, "green" );
addTestWithTemporaryValues( Color.blue, 0.2, "blue" );
addTestWithTemporaryValues( Color.magenta, 0.3, "magenta" );
}
private void addTestSimpleLine( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestMiddleDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.3, chartColor, true, name );
addTestValue( 30, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestLeadingDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.05, chartColor, true, name );
addTestValue( 20, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
testTime += 1000;
}
private void addTestTrailingDotOnly( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 30, chartColor, baseValue + 0.4, null, false, name );
addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name );
testTime += 1000;
}
private void addTestSingleData( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.15, chartColor, false, name );
testTime += 1000;
}
private void addTestWithTemporaryValues( Color chartColor, double baseValue, String name ) {
addTestValue( 0, chartColor, baseValue + 0.0, null, false, name );
addTestValue( 50, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 5, chartColor, baseValue + 0.4, null, false, name );
addTestValue( 5, chartColor, baseValue + 0.1, null, false, name );
addTestValue( 40, chartColor, baseValue + 0.3, null, false, name );
testTime += 1000;
}
private void addTestValue( int timeDelta, Color chartColor, double value, Color dotColor, boolean dotOnly, String name ) {
testTime += timeDelta;
List<Data> chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() );
Data data = new Data( value, testIValue++, chartColor, dotColor, dotOnly, testTime, name, new Exception() );
if( asynchron )
chartData.add( data );
else
syncChartData.add( data );
lastUsedChartColor = chartColor;
}
private int testIValue;
private long testTime;
//TODO remove and use ColorFunctions.fade() when merging to main //TODO remove and use ColorFunctions.fade() when merging to main
private static Color fade( Color color, float amount ) { private static Color fade( Color color, float amount ) {
int newAlpha = Math.round( 255 * amount ); int newAlpha = Math.round( 255 * amount );

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8" JFDML JFormDesigner: "8.3" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -90,17 +90,19 @@ new FormModel {
} ) } )
add( new FormComponent( "javax.swing.JSlider" ) { add( new FormComponent( "javax.swing.JSlider" ) {
name: "oneSecondWidthSlider" name: "oneSecondWidthSlider"
"minimum": 1000 "minimum": 100
"maximum": 10000 "maximum": 10000
"snapToTicks": true
"majorTickSpacing": 100
"value": 500
addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) ) addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,alignx right,growx 0,wmax 100" "value": "cell 0 1,alignx right,growx 0"
} ) } )
add( new FormComponent( "javax.swing.JCheckBox" ) { add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "updateChartDelayedCheckBox" name: "updateChartDelayedCheckBox"
"text": "Update chart delayed" "text": "Update chart delayed"
"mnemonic": 80 "mnemonic": 80
"selected": true
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1,alignx right,growx 0" "value": "cell 0 1,alignx right,growx 0"