如何在域十字线中显示音量并以编程方式设置十字线?

How to display volume in the domain crosshair and set the crosshair programmatically?

这个问题与我之前的问题[]有关。感谢@trashgod 我已经解决了那个问题的很多问题,我会问这个问题还剩下什么。我的示例代码如下所示:

import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import org.jfree.chart.*;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.*;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.time.*;
import org.jfree.data.xy.XYDataset;
import org.jfree.chart.labels.*;
import org.jfree.chart.panel.*;
import org.jfree.chart.plot.*;

public class PriceVolume_Chart extends JPanel implements ChartMouseListener    // A demo application for price-volume chart.   
{
  ChartPanel panel;
  TimeSeries Price_series=new TimeSeries("Price");
  TimeSeries Volume_Series=new TimeSeries("Volume");
  Crosshair xCrosshair,yCrosshair;
  static Vector<String> Volume_Color_Vector=new Vector();

  public PriceVolume_Chart(String Symbol)
  {
    JFreeChart chart=createChart(Symbol);
    panel=new ChartPanel(chart,true,true,true,false,true);
    panel.setPreferredSize(new Dimension(1000,500));
    panel.addChartMouseListener(this);
    CrosshairOverlay crosshairOverlay=new CrosshairOverlay();
    float[] dash={2f,0f,2f};
    BasicStroke bs=new BasicStroke(1,BasicStroke.CAP_BUTT,BasicStroke.JOIN_ROUND,1.0f,dash,2f);

    xCrosshair=new Crosshair(Double.NaN,Color.GRAY,bs);
    xCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
    xCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
    xCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));
    xCrosshair.setLabelVisible(true);
    yCrosshair=new Crosshair(Double.NaN,Color.GRAY,bs);
    yCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
    yCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
    yCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));
    yCrosshair.setLabelVisible(true);
    crosshairOverlay.addDomainCrosshair(xCrosshair);
    crosshairOverlay.addRangeCrosshair(yCrosshair);
    panel.addOverlay(crosshairOverlay);
    add(panel);
    xCrosshair.setValue(1.5700752E12);
    xCrosshair.setVisible(true);
    yCrosshair.setValue(119.959007);
    yCrosshair.setVisible(true);
  }

  private JFreeChart createChart(String Symbol)
  {
    createPriceDataset(Symbol);
    XYDataset priceData=new TimeSeriesCollection(Price_series);
    JFreeChart chart=ChartFactory.createTimeSeriesChart(Symbol,"Date",getYLabel("Price ( $ )"),priceData,true,true,true);
    XYPlot plot=chart.getXYPlot();
    plot.setBackgroundPaint(new Color(192,196,196));
    NumberAxis rangeAxis1=(NumberAxis)plot.getRangeAxis();
    rangeAxis1.setLowerMargin(0.40);                                           // Leave room for volume bars
//    plot.getRenderer().setDefaultToolTipGenerator(new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,DateFormat.getDateInstance(), NumberFormat.getCurrencyInstance()));
    plot.getRenderer().setDefaultToolTipGenerator(new StandardXYToolTipGenerator(StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,new SimpleDateFormat("yyyy-MM-d"),NumberFormat.getCurrencyInstance()));
//    DecimalFormat format=new DecimalFormat("00.00");
//    rangeAxis1.setNumberFormatOverride(format);
//    rangeAxis1.setNumberFormatOverride(NumberFormat.getCurrencyInstance());  

    NumberAxis rangeAxis2=new NumberAxis("Volume");
    rangeAxis2.setUpperMargin(1.00);                                           // Leave room for price line   
    rangeAxis2.setNumberFormatOverride(NumberFormat.getNumberInstance());
    plot.setRangeAxis(1,rangeAxis2);
    plot.setDataset(1,new TimeSeriesCollection(Volume_Series));
    plot.setRangeAxis(1,rangeAxis2);
    plot.mapDatasetToRangeAxis(1,1);
    plot.setRenderer(1,new MyRender());

    return chart;
  }

  private void createPriceDataset(String Symbol)
  {
    String Lines[]=new String[21], Items[], Date;
    int Year, Month, Day;
    long Volume,Last_Volume=0;
    double Price;

    Lines[0]="Date,Open,High,Low,Close,Adj Close,Volume";
    Lines[1]="2019-09-23,129.589996,130.710007,128.240005,129.300003,126.555969,553700";
    Lines[2]="2019-09-24,129.309998,129.529999,125.500000,126.750000,124.060089,732900";
    Lines[3]="2019-09-25,126.570000,128.500000,126.190002,127.879997,125.166100,422000";
    Lines[4]="2019-09-26,127.849998,128.589996,127.169998,127.779999,125.068230,376100";
    Lines[5]="2019-09-27,128.669998,129.289993,126.389999,126.419998,123.737083,332900";
    Lines[6]="2019-09-30,126.589996,128.789993,125.849998,128.130005,125.410797,456700";
    Lines[7]="2019-10-01,129.039993,130.899994,125.480003,126.040001,123.365158,322700";
    Lines[8]="2019-10-02,125.059998,125.180000,121.620003,123.120003,120.507126,577100";
    Lines[9]="2019-10-03,122.650002,123.320000,119.089996,122.559998,119.959007,581300";
    Lines[10]="2019-10-04,122.970001,123.949997,121.320000,123.879997,121.250992,315700";
    Lines[11]="2019-10-07,123.139999,124.610001,122.669998,122.879997,120.272217,510300";
    Lines[12]="2019-10-08,121.720001,121.879997,118.089996,118.660004,116.141777,616600";
    Lines[13]="2019-10-09,119.410004,119.610001,116.680000,118.419998,115.906868,603300";
    Lines[14]="2019-10-10,119.089996,121.209999,117.080002,118.209999,115.701324,483300";
    Lines[15]="2019-10-11,120.330002,123.040001,119.720001,122.550003,119.949226,700500";
    Lines[16]="2019-10-14,122.550003,123.720001,120.940002,122.540001,119.939430,492900";
    Lines[17]="2019-10-15,122.849998,124.220001,121.230003,123.699997,121.074814,598200";
    Lines[18]="2019-10-16,123.889999,124.849998,122.800003,123.209999,120.595207,663600";
    Lines[19]="2019-10-17,123.449997,124.889999,122.790001,123.360001,120.742035,563200";
    Lines[20]="2019-10-18,123.050003,124.620003,122.459999,123.540001,120.918213,650300";

    for (int i=1;i<Lines.length;i++)
    {
      Items=Lines[i].split(",");
      Date=Items[0].replace("-0","-");
      Price=Double.parseDouble(Items[5]);
      Volume=Long.parseLong(Items[6]);
      Items=Date.split("-");
      Year=Integer.parseInt(Items[0]);
      Month=Integer.parseInt(Items[1]);
      Day=Integer.parseInt(Items[2]);
      Price_series.add(new Day(Day,Month,Year),Price);
      Volume_Series.add(new Day(Day,Month,Year),Volume);
      Volume_Color_Vector.add(Volume>=Last_Volume?"+":"-");
      Last_Volume=Volume;
    }
  }

  @Override
  public void chartMouseClicked(ChartMouseEvent event)
  {
    // ignore
  }

  public void chartMouseMoved(ChartMouseEvent cmevent)
  {
    ChartEntity chartentity=cmevent.getEntity();
    if (chartentity instanceof XYItemEntity)
    {
      XYItemEntity e=(XYItemEntity)chartentity;
      XYDataset d=e.getDataset();
      int s=e.getSeriesIndex();
      int i=e.getItem();
      double x=d.getXValue(s,i);
      double y=d.getYValue(s,i);
//      Out("x = "+x+"  y = "+y);
      xCrosshair.setValue(x);
      yCrosshair.setValue(y);
    }
  }

  String getYLabel(String Text)
  {
    String Result="";

    for (int i=0;i<Text.length();i++) Result+=Text.charAt(i)+(i<Text.length()-1?"\u2009":"");
//    Out(Result);
    return Result;
  }

  private static void out(String message) { System.out.print(message); }

  private static void Out(String message) { System.out.println(message); }

  // Create the GUI and show it. For thread safety, this method should be invoked from the event-dispatching thread.
  static void Create_And_Show_GUI()
  {
    final PriceVolume_Chart demo=new PriceVolume_Chart("ADS");

    JFrame frame=new JFrame("PriceVolume_Chart Frame");
    frame.add(demo);
    frame.addWindowListener(new WindowAdapter()
    {
      public void windowActivated(WindowEvent e) { }
      public void windowClosed(WindowEvent e) { }
      public void windowClosing(WindowEvent e) { System.exit(0); }
      public void windowDeactivated(WindowEvent e) { }
      public void windowDeiconified(WindowEvent e) { demo.repaint(); }
      public void windowGainedFocus(WindowEvent e) { demo.repaint(); }
      public void windowIconified(WindowEvent e) { }
      public void windowLostFocus(WindowEvent e) { }
      public void windowOpening(WindowEvent e) { demo.repaint(); }
      public void windowOpened(WindowEvent e) { }
      public void windowResized(WindowEvent e) { demo.repaint(); }
      public void windowStateChanged(WindowEvent e) { demo.repaint(); }
    });
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }

  public static void main(String[] args)
  {
    // Schedule a job for the event-dispatching thread : creating and showing this application's GUI.
    SwingUtilities.invokeLater(new Runnable() { public void run() { Create_And_Show_GUI(); } });
  }
}

class MyRender extends XYBarRenderer
{
  @Override
  public Paint getItemPaint(int row,int col)
  {
    System.out.println(row+" "+col+" "+super.getItemPaint(row,col));
    return PriceVolume_Chart.Volume_Color_Vector.elementAt(col).equals("+")?super.getItemPaint(row,col):new Color(0.56f,0.2f,0.5f,1f);
  }
}

我的问题是:

[1] How to correctly show Volume data in cross-hair [ it's now showing the incorrect value, yet I don't know how to fix it ] ?
[2] How to point the mouse on the N-th [ e.g. 12th ] day when the app starts ? 

第二个问题的更多细节,当我移动鼠标时,我可以在屏幕上输出鼠标的x,y值,我可以执行以下操作在启动时将鼠标设置在所需位置:

...
panel.addOverlay(crosshairOverlay);
add(panel);
xCrosshair.setValue(1.5700752E12);
yCrosshair.setValue(119.959007);

但是因为不知道第N天的[x,y]位置,所以不知道那天鼠标怎么设置。

我想这样做的原因是:在我的应用程序中,我想在另一个面板中查看某一天的一些数据,用户可以单击某一天,我希望 JFreeChart 立即指向那天在图表上。

所以我的第二个问题是:“如何获得图表上第 N 天的 [x,y] 位置?

How to correctly show volume data in cross-hair; it's now showing the incorrect value.

按照建议 , "The x value is the date…absent formatting, the crosshair correctly displays the number of milliseconds from the epoch." To alter the crosshair's display, add a custom CrosshairLabelGenerator。下面的实现在您的卷系列中搜索匹配的时间戳并将相应的值格式化为数字。

xCrosshair.setLabelGenerator(new CrosshairLabelGenerator() {
    @Override
    public String generateLabel(Crosshair crosshair) {
        long ms = (long) crosshair.getValue();
        TimeSeriesDataItem item = null;
        for (int i = 0; i < Volume_Series.getItemCount(); i++) {
            item = Volume_Series.getDataItem(i);
            if (ms == item.getPeriod().getFirstMillisecond()) {
                break;
            }
        }
        long volume = item.getValue().longValue();
        return NumberFormat.getInstance().format(volume);
    }
});

考虑调整域轴的下边距以实现对称。

DateAxis domainAxis = (DateAxis) plot.getDomainAxis();
domainAxis.setLowerMargin(0.05);

还可以考虑设置 MyRenderer 的对齐因子,使条形图在价格点下方居中。

this.setBarAlignmentFactor(0.5);

当用户控制鼠标时,我会避免以编程方式移动十字准线。相反,请查看合适的 AnnotationMarker。话虽如此,将您对 chartMouseMoved() 的实现与显示的 here. Your's updates for each new XYItemEntity encountered, so the crosshairs "snap" to points or bars. In contrast, the latter one updates continuously, scaling via java2DToValue(), as described .

进行比较