Search code examples
javagwtgxt

How to add a general vertical axis to a chart using ValueProvider


This is how I add a line to my chart at the moment. This is the abstract class for an arbitrry funciton I want to display:

public abstract class ArbitraryFunction implements
    ValueProvider<ArbitraryFunctionData, Double> {

  private String field;

  public abstract Double f(Double x);

  /**
   * Constructor
   */
  public ArbitraryFunction(String field) {
    this.field = field;
  }

  @Override
  public Double getValue(ArbitraryFunctionData object) {
    return object.get(field);
  }

  @Override
  public void setValue(ArbitraryFunctionData object, Double value) {
    object.put(field, value);
  }

  @Override
  public String getPath() {
    return field;
  }
}

This is how the chart is created:

ArbitraryFunction f1 = new ArbitraryFunction("f1") {
      @Override
      public Double f(Double x) {
        return Math.sin(x);
      }
    };

functionMap.put(f1.getPath(), f1);

// collects the data of the functions and adds them to the store
for (Double x = 0.0; x <= 2 * Math.PI; x = x + 0.1) {
  ArbitraryFunctionData d = new ArbitraryFunctionData();
  d.setName("" + x);
  for (Map.Entry<String, ArbitraryFunction> entry : functionMap.entrySet()) {
    ArbitraryFunction tmp = entry.getValue();
    d.put(tmp.getPath(), tmp.f(x));
  }
  store.add(d);
}

chart.setStore(store);

verticalAxis.setPosition(Position.LEFT);
verticalAxis.addField(f1);
verticalAxis.setSteps(2);
verticalAxis.setMinorTickSteps(5);
chart.addAxis(verticalAxis);

This works so far as intended. The graph shows my lines as it should do it and the vertical axis is correct too. But I have problems drawing the horizontal axis since I don't know what I need to give horizontalAxis.addField( ??? ). I've tried a few things, but nothing worked.

Does anyone know how I need to set up the horizontal axis?


Solution

  • What do you want the horizontal axis value to be? Is it another NumericAxis - does each data point have a x value that it should be drawn on? Each d in your loop has a String name and some value - perhaps you want a CategoryAxis<ArbitraryFunctionData, String> that just draws those name values?


    Looks like I misunderstood earlier - your Function objects are just used in setup, not in changing how you plot data

    I'm still not sure what you are after, but it sounds like you mostly want to plot some lines. Each data point (ArbitraryFunctionData?) seems to have Y values for each function being used, and a title, but no X values, so there is no way to plot each point as (X,Y) with two numeric axes, just as (name, Y) using a CategoryAxis and a NumericAxis. This would end up more or less like this sample: http://www.sencha.com/examples/#ExamplePlace:linechart - strings along the bottom, and numbers along the side.

    Here's take one, build mostly off of the idea/structure you already have:

    public class FunctionPlotter implements EntryPoint {
      public static class ArbitraryFunctionData {
        private double xValue;
        private Map<String, Double> yValues = new HashMap<String, Double>();
        public double get(String key) {
          return yValues.get(key);
        }
        public void put(String key, double yValue) {
          yValues.put(key, yValue);
        }
        public double getXValue() {
          return xValue;
        }
        public void setxValue(double xValue) {
          this.xValue = xValue;
        }
      }
      public interface AFDProperties extends PropertyAccess<ArbitraryFunctionData> {
        //xvalue is unique, key off of that
        @Path("xValue")
        ModelKeyProvider<ArbitraryFunctionData> key();
    
        //automatic ValueProvider generation for the get/setXValue methods
        ValueProvider<ArbitraryFunctionData, Double> xValue();
      }
    
      /** 
       * This is really doing two different jobs at once - wasn't quite was I was trying to suggest in 
       * that other question. See the second version of this for clarification...
       */
      public static abstract class ArbitraryFunction implements ValueProvider<ArbitraryFunctionData, Double> {
        private final String field;
    
        public ArbitraryFunction(String field) {
          this.field = field;
        }
    
        public abstract Double f(Double x);
    
        @Override
        public Double getValue(ArbitraryFunctionData object) {
          return object.get(field);
        }
    
        @Override
        public void setValue(ArbitraryFunctionData object, Double value) {
          object.put(field, value);
        }
    
        @Override
        public String getPath() {
          return field;
        }
      }
    
      @Override
      public void onModuleLoad() {
        Viewport vp = new Viewport();
    
        Set<ArbitraryFunction> functions = new HashSet<ArbitraryFunction>();
        ArbitraryFunction f1 = new ArbitraryFunction("f1") {
          @Override
          public Double f(Double x) {
            return Math.sin(x);
          }
        };
        functions.add(f1);
    
        AFDProperties props = GWT.create(AFDProperties.class);
        ListStore<ArbitraryFunctionData> store = new ListStore<ArbitraryFunctionData>(props.key());
        // collects the data of the functions and adds them to the store
        for (Double x = 0.0; x <= 2 * Math.PI; x = x + 0.1) {
          // Create one data object, and set the X value, since that is the same for all Y values
          ArbitraryFunctionData d = new ArbitraryFunctionData();
          d.setxValue(x);
    
          // For each function, set the corresponding Y value
          for (ArbitraryFunction func : functions) {
            d.put(func.getPath(), func.f(x));
          }
          store.add(d);
        }
    
        Chart<ArbitraryFunctionData> chart = new Chart<ArbitraryFunctionData>();
        chart.setStore(store);
    
        //Y-axis
        NumericAxis<ArbitraryFunctionData> verticalAxis = new NumericAxis<ArbitraryFunctionData>();
        verticalAxis.setPosition(Position.LEFT);
        verticalAxis.addField(f1);//needs to know this field to properly set the range of values
        //f2, f3, etc
        verticalAxis.setSteps(2);
        verticalAxis.setMinorTickSteps(5);
        chart.addAxis(verticalAxis);
    
        // X-Axis, this time reading from the xValue, not the series of ValueProviders
        NumericAxis<ArbitraryFunctionData> horizAxis = new NumericAxis<ArbitraryFunctionData>();
        horizAxis.setPosition(Position.BOTTOM);
        horizAxis.addField(props.xValue());//same value for all
        horizAxis.setSteps(2);
        horizAxis.setMinorTickSteps(5);
        chart.addAxis(horizAxis);
    
        for (ArbitraryFunction func : functions) {
          LineSeries<ArbitraryFunctionData> line = new LineSeries<ArbitraryFunctionData>();
          // configure x axis
          line.setXAxisPosition(Position.BOTTOM);//where is it
          line.setXField(props.xValue());//what value do i use
          // configure y axis
          line.setYAxisPosition(Position.LEFT);//where is it
          line.setYField(func);//what value do i use
    
          //probably want to customized per func
          line.setStroke(RGB.GRAY);
          line.setStrokeWidth(2);
    
          chart.addSeries(line);
        }
    
        vp.setWidget(chart);
        RootPanel.get().add(vp);
      }
    }
    

    And here's take two, this time with much simpler data and actually making the Function its own ValueProvider, and keeping the data dirt simple - just a double! Note that the ValueProvider is the function, and we never call getValue ourselves, we let the axis/series do it for us! Added a second function here to demonstrate that it does actually work.

    public class FunctionPlotter implements EntryPoint {
    
      /**
       * Where did everything go? We're just making a ValueProvider now that can handle 
       * each number as a value, and working out the details from there
       *
       * For fun, added per-function coloring too
       */
      public abstract static class Function implements ValueProvider<Double, Double> {
        private final String name;
        private final Color color;
        public Function(String name, Color color) {
          this.name = name;
          this.color = color;
        }
        @Override
        public abstract Double getValue(Double object);
    
        @Override
        public String getPath() {
          return name;
        }
        @Override
        public void setValue(Double object, Double value) {
          //no-op
        }
        public Color getColor() {
          return color;
        }
      }
    
      @Override
      public void onModuleLoad() {
        Viewport vp = new Viewport();
    
        Set<Function> functions = new HashSet<Function>();
        Function f1 = new Function("f1", RGB.RED) {
          @Override
          public Double getValue(Double x) {
            return Math.sin(x);
          }
        };
        functions.add(f1);
        Function f2 = new Function("f2", RGB.BLACK) {
          @Override
          public Double getValue(Double x) {
            return Math.cos(x);
          }
        };
        functions.add(f2);
    
        //Turns out Stores can hold any objects - should probably factor out this key provider for reuse...
        ListStore<Double> store = new ListStore<Double>(new ModelKeyProvider<Double>() {
          @Override
          public String getKey(Double item) {
            return item.toString();
          }
        });
        // collects the data of the functions and adds them to the store
        for (Double x = 0.0; x <= 2 * Math.PI; x = x + 0.1) {
          store.add(x);
        }
    
        Chart<Double> chart = new Chart<Double>();
        chart.setStore(store);
    
        //Y-axis
        NumericAxis<Double> verticalAxis = new NumericAxis<Double>();
        verticalAxis.setPosition(Position.LEFT);
        for (Function func : functions) {
          verticalAxis.addField(func);//needs to know this field to properly set the range of values
        }
        verticalAxis.setSteps(2);
        verticalAxis.setMinorTickSteps(5);
        chart.addAxis(verticalAxis);
    
        // X-Axis, this time reading from the xValue, not the series of ValueProviders
        NumericAxis<Double> horizAxis = new NumericAxis<Double>();
        horizAxis.setPosition(Position.BOTTOM);
        horizAxis.addField(new IdentityValueProvider<Double>());//magic value provider that returns the same string
        horizAxis.setSteps(2);
        horizAxis.setMinorTickSteps(5);
        chart.addAxis(horizAxis);
    
        for (Function func : functions) {
          LineSeries<Double> line = new LineSeries<Double>();
          // configure x axis
          line.setXAxisPosition(Position.BOTTOM);//where is it
          line.setXField(new IdentityValueProvider<Double>());//what value do i use
          // configure y axis
          line.setYAxisPosition(Position.LEFT);//where is it
          line.setYField(func);//what value do i use
    
          //probably want to customized per func
          line.setStroke(func.getColor());
          line.setStrokeWidth(2);
    
          chart.addSeries(line);
        }
    
        vp.setWidget(chart);
        RootPanel.get().add(vp);
      }
    }