如何突出显示 JFreeChart 中的一个部分?

How to highlight a section in JFreeChart?

我的示例代码如下所示:

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import org.jfree.chart.*;
import org.jfree.chart.annotations.*;
import org.jfree.chart.axis.*;
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.*;
import org.jfree.chart.renderer.xy.*;
import org.jfree.chart.ui.*;

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;
  Vector<String> Volume_Color_Vector;
  XYTextAnnotation note;
  XYPlot plot;
  XYLineAndShapeRenderer r;
  JFreeChart chart;
  LinkedHashMap<String,String> From_To_Date_Map;
  
  public PriceVolume_Chart(String Symbol,LinkedHashMap<String,String> From_To_Date_Map,int Index)
  {
//    super(new GridLayout());
    this.From_To_Date_Map=From_To_Date_Map;
    try
    {
      chart=createChart(Symbol);
      panel=new ChartPanel(chart,true,true,true,false,true);
      panel.setPreferredSize(new java.awt.Dimension(1000,500));
      panel.addChartMouseListener(this);

      if (From_To_Date_Map!=null && From_To_Date_Map.size()>0)
      {
        for (String key : From_To_Date_Map.keySet())
        {
          String fromDate=key;
          int xStart=getX(fromDate);
          String toDate=From_To_Date_Map.get(key);
          int xEnd=getX(toDate);

          XYShapeAnnotation highlight=new XYShapeAnnotation(new Rectangle2D.Double(xStart,36,xEnd-xStart,669),new BasicStroke(1.0f),Color.blue);
          plot.addAnnotation(highlight);
          Out("[ "+fromDate+" : "+toDate+" ]");
        }
      }

      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.black,bs);
      xCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
      xCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
      xCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));

      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();
          String s=NumberFormat.getInstance().format(volume);
          return MessageFormat.format(" Volume: {0} ",s);
        }
      });

/*
      xCrosshair.setLabelGenerator(new StandardCrosshairLabelGenerator()
      {
        @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)+" ";
        }
      });
*/
      xCrosshair.setLabelVisible(true);
      yCrosshair=new Crosshair(Double.NaN,Color.black,bs);
      yCrosshair.setLabelBackgroundPaint(new Color(0f,0f,0f,1f));
      yCrosshair.setLabelFont(xCrosshair.getLabelFont().deriveFont(14f));
      yCrosshair.setLabelPaint(new Color(1f,1f,1f,1f));
      
      yCrosshair.setLabelGenerator(new StandardCrosshairLabelGenerator(" Price: {0} ",NumberFormat.getCurrencyInstance()));

      yCrosshair.setLabelVisible(true);
      crosshairOverlay.addDomainCrosshair(xCrosshair);
      crosshairOverlay.addRangeCrosshair(yCrosshair);
      panel.addOverlay(crosshairOverlay);
      add(panel);
/*
      if (Index!=-1 && Index<Volume_Series.getItemCount())
      {
        TimeSeriesDataItem itemX=Volume_Series.getDataItem(Index);
        xCrosshair.setValue(itemX.getPeriod().getFirstMillisecond());
        
        TimeSeriesDataItem itemY=Price_Series.getDataItem(Index);
        yCrosshair.setValue(itemY.getValue().doubleValue());

        TimeSeriesDataItem item=Price_Series.getDataItem(Index);
        float time=item.getPeriod().getFirstMillisecond();
        float price=item.getValue().floatValue();
        SimpleDateFormat f=new SimpleDateFormat("yyyy-MM-d");
        String st=" Price : "+f.format(new Date((long)time))+" , "+NumberFormat.getCurrencyInstance().format(price)+" ";
        note=new XYTextAnnotation(st,time,price-1);
        note.setFont(UIManager.getFont("ToolTip.font"));
        note.setBackgroundPaint(UIManager.getColor("ToolTip.background"));
        Out("Index = "+Index);
        note.setTextAnchor(Index<Volume_Color_Vector.size()/2?TextAnchor.TOP_LEFT:TextAnchor.TOP_RIGHT);
        note.setOutlinePaint(Color.blue);
        note.setOutlineVisible(true);
        plot.getRenderer().addAnnotation(note);
      }
*/
    }
    catch (Exception e) { e.printStackTrace(); }
  }

  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);
    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,new SimpleDateFormat("yyyy-MM-d"),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);
    MyRender Renderer=new MyRender(this);
    Renderer.setShadowVisible(false);
    plot.setRenderer(1,Renderer);

    DateAxis domainAxis=(DateAxis) plot.getDomainAxis();                     // Consider adjusting the lower margin of the domain axis for symmetry.
    domainAxis.setLowerMargin(0.05);
          
    r=(XYLineAndShapeRenderer)plot.getRenderer();

//    r.addAnnotation(new XYShapeAnnotation(new Ellipse2D.Double(300,300,60,60)));

    return chart;
  }

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

    Lines[0]="Date,Open,High,Low,Close,Adj Close,Volume";
    Lines[1]="2020-07-17,44.110001,44.369999,41.919998,42.509998,42.323395,849700";
    Lines[2]="2020-07-20,41.630001,41.680000,39.669998,40.119999,39.943886,1319300";
    Lines[3]="2020-07-21,40.880001,42.860001,40.860001,42.270000,42.084450,2070300";
    Lines[4]="2020-07-22,41.919998,42.700001,41.090000,42.570000,42.383133,1317600";
    Lines[5]="2020-07-23,43.919998,46.389999,43.279999,44.759998,44.563519,1917700";
    Lines[6]="2020-07-24,46.500000,46.500000,43.950001,44.410000,44.215057,1384600";
    Lines[7]="2020-07-27,44.000000,44.240002,42.610001,43.860001,43.667469,799800";
    Lines[8]="2020-07-28,43.389999,44.590000,42.930000,43.020000,42.831158,699700";
    Lines[9]="2020-07-29,42.759998,45.590000,42.740002,45.430000,45.230579,826200";
    Lines[10]="2020-07-30,44.160000,44.639999,42.959999,44.500000,44.304661,798100";
    Lines[11]="2020-07-31,44.330002,44.419998,42.580002,44.360001,44.165276,1037800";
    Lines[12]="2020-08-03,44.560001,45.599998,43.419998,44.939999,44.742729,797000";
    Lines[13]="2020-08-04,44.900002,45.500000,43.450001,43.540001,43.348877,971100";
    Lines[14]="2020-08-05,44.860001,45.389999,43.650002,45.330002,45.131020,902000";
    Lines[15]="2020-08-06,45.049999,46.279999,44.330002,45.299999,45.101147,645200";
    Lines[16]="2020-08-07,44.849998,46.189999,44.189999,46.150002,45.947418,604900";
    Lines[17]="2020-08-10,46.669998,48.410000,46.549999,47.290001,47.082417,960200";
    Lines[18]="2020-08-11,49.110001,50.849998,48.799999,48.910000,48.695301,1187700";
    Lines[19]="2020-08-12,49.759998,50.009998,47.060001,47.840000,47.630001,752800";
    Lines[20]="2020-08-13,46.950001,48.369999,46.459999,47.110001,47.110001,535700";

    Volume_Color_Vector=new Vector();
    for (int i=1;i<Lines.length;i++)
    {
      Items=Lines[i].split(",");
      Date=Items[0].replace("-0","-");
      Price=Float.parseFloat(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;
    }
  }

  public int getX(int index)                     // Find X pixel location by date index : In this example : index = 0 points to "2020-07-17"
  {
    Rectangle2D dataArea=panel.getScreenDataArea();
    RectangleEdge xAxisLocation=plot.getDomainAxisEdge();
    DateAxis domainAxis=(DateAxis)plot.getDomainAxis();
    TimeSeriesDataItem item=Price_Series.getDataItem(index);
    long t=item.getPeriod().getFirstMillisecond();
    double x=domainAxis.valueToJava2D(t,dataArea,xAxisLocation);
//    Out("X = "+(int)x);
    return (int) x;
  }

  public int getX(String date)                   // Find X pixel location by date string : For example "2020-08-03"
  {
    double x=-999;
    
    Rectangle2D dataArea=panel.getScreenDataArea();
    RectangleEdge xAxisLocation=plot.getDomainAxisEdge();
    DateAxis domainAxis=(DateAxis)plot.getDomainAxis();
    try
    {
      SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
      RegularTimePeriod rtp=new Day(sdf.parse(date));
      TimeSeriesDataItem item=Price_Series.getDataItem(Price_Series.getIndex(rtp));
      long t=item.getPeriod().getFirstMillisecond();
      x=domainAxis.valueToJava2D(t,dataArea,xAxisLocation);
//    Out("X = "+(int)x);
    }
    catch (Exception e) { e.printStackTrace(); }
    return (int) x;
  }

  @Override
  public void chartMouseClicked(ChartMouseEvent e)
  {
    Out("x = "+e.getTrigger().getX());           // Find X pixel location by mouse click
  }

  public void chartMouseMoved(ChartMouseEvent cmevent)
  {
    ChartEntity chartentity=cmevent.getEntity();
    if (chartentity instanceof XYItemEntity)
    {
      if (!r.getAnnotations().isEmpty()) r.removeAnnotation(note);

      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);
//      Out(cmevent.getTrigger().getX()+" "+getX(i)+"  getX(2020-07-23) = "+getX("2020-07-23"));
    }
  }
/*
  public void paint(Graphics g)
  {
    int x=58,y=36,w=87,h=401;
    Graphics2D g2d=(Graphics2D)g;
    
    chart.draw((Graphics2D)g,getBounds());
    chart.setAntiAlias(true);
        
    g2d.setPaint(new Color(98,98,198));
    g2d.setComposite(makeComposite(0.36f));
    g2d.fillRect(x+313,y,w,h);   
    
    repaint();
  }

  private AlphaComposite makeComposite(float alpha)
  {
    int type=AlphaComposite.SRC_OVER;
    return (AlphaComposite.getInstance(type,alpha));
  }
*/
  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()
  {
    LinkedHashMap<String,String> From_To_Date_Map=new LinkedHashMap();
    
    From_To_Date_Map.put("2020-07-27","2020-07-29");
    final PriceVolume_Chart demo=new PriceVolume_Chart("ADS",From_To_Date_Map,10);

    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
{
  PriceVolume_Chart PVC;

  MyRender(PriceVolume_Chart PVC)
  {
    this.PVC=PVC;
  }

  @Override
  public Paint getItemPaint(int row,int col)
  {
    this.setBarAlignmentFactor(0.5);
//      System.out.println(row+" "+col+" "+super.getItemPaint(row,col));
    return PVC.Volume_Color_Vector.elementAt(col).equals("+")?super.getItemPaint(row,col):new Color(0.56f,0.2f,0.5f,1f);
  }
}

我正在尝试突出显示图表上的一个部分,它应该如下所示,但它没有显示,如何修复我的代码?

如所见here, concrete annotations require that coordinates be specified in data space. Given a Map.Entry<Integer, Integer> hightlight of series indices to highlight, seen here,

Map.Entry<Integer, Integer> highlight = new HashMap.SimpleEntry<>(6, 8);

下面的片段添加了一个 XYBoxAnnotation 跨越指定域和整个范围。请注意 setBarAlignmentFactor() 的 non-zero 值需要相应的调整 BAR_SHIFT:

double BAR_FACTOR = 0.5;
double BAR_SHIFT = Duration.ofDays(1).toMillis() * BAR_FACTOR;
…
double t0 = priceSeries.getTimePeriod(hightlight.getKey()).getFirstMillisecond() - BAR_SHIFT;
double t1 = priceSeries.getTimePeriod(hightlight.getValue()).getLastMillisecond() - BAR_SHIFT;
double minY = 0;
double maxY = priceSeries.getMaxY() * 1.1;
Color color = new Color(0, 0, 255, 50);
xyRenderer.addAnnotation(new XYBoxAnnotation(
    t0, minY, t1, maxY, null, null, color), Layer.BACKGROUND);

搜索一系列匹配日期字符串的说明