Table of Contents

How to add a graph easily

This documentation is for the Capsis modellers. It explains how to implement easily a new graph in Java for their own model.

Extension reminder

Once a new Graph created from one of the examples below, compile it with 'ant compile', then when built successfully, declare it in capsis with 'capsis -se' (i.e. 'search extensions'). To configure a graph, right click on it > configure.

When everything is ok, add all files to svn, then commit.

Reminder: all extensions must provide these properties:

Graphs

A graph is a combination of one or several data extractors (synchronised on a given Step in a simulation, create series of data) and a data renderer (shows the data series, e.g. a chart, a table…). The whole is often called a graph, but we focus here on various data extractors (the data renderers are generic).

Data extractors implement the capsis.extensiontype.DataExtractor interface. They generally extend a more convenient superclass, e.g. capsis.extension.dataextractor.superclass.AbstractDataExtractor.

All data extractors extending AbstractDataExtractor must provide these properties :

A general purpose graph

This general purpose data extractor implements the DFListOfXYSeries interface, it must provide a list containing one or several XYSeries. Each XYSeries has a name, a color and an list of x and y (Vertex2d). It must also provide the axes names:

An example:

package simq.extension.dataextractor;
 
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
 
import capsis.extension.dataextractor.format.DFListOfXYSeries;
import capsis.extension.dataextractor.superclass.AbstractDataExtractor;
import capsis.kernel.GModel;
import capsis.kernel.Step;
import jeeb.lib.util.Log;
import jeeb.lib.util.Translator;
import jeeb.lib.util.XYSeries;
import simq.model.SimqModel;
import simq.model.SimqStand;
import simq.model.tree.SimqTree;
import simq.model.tree.SimqTreeProfile;
import simq.model.util.SimqVerticalProfile;
 
/**
 * Profiles of the stem
 * 
 * @author Julien Sainte-Marie, F. de Coligny - April 2022
 */
public class SimqDETreeProfile extends AbstractDataExtractor implements DFListOfXYSeries {
 
	static {
		Translator.addBundle("simq.extension.dataextractor.DELabels");
	}
 
	private List<XYSeries> listOfXYSeries;
 
	/**
	 * Constructor.
	 */
	public SimqDETreeProfile() {
	}
 
	/**
	 * Init method, receives the Step to be synchronized on.
	 */
	@Override
	public void init(GModel model, Step step) throws Exception {
 
		super.init(model, step);
		listOfXYSeries = new ArrayList<>();
 
	}
 
	/**
	 * Extension dynamic compatibility mechanism. This matchwith method checks if
	 * the extension can deal (i.e. is compatible) with the referent.
	 */
	static public boolean matchWith(Object referent) {
		// Compatible with SimqModel only
		return referent instanceof SimqModel;
	}
 
	/**
	 * From DFListOfXYSeries.
	 */
	@Override
	public String getName() {
		return getNamePrefix() + Translator.swap("SimqDETreeProfile.name");
	}
 
	/**
	 * From AbstractDataExtractor.
	 */
	@Override
	public String getAuthor() {
		return "F. de Coligny, J. Sainte-Marie";
	}
 
	/**
	 * From AbstractDataExtractor.
	 */
	@Override
	public String getDescription() {
		return Translator.swap("SimqDETreeProfile.description");
	}
 
	/**
	 * From AbstractDataExtractor interface.
	 */
	@Override
	public String getVersion() {
		return "1.0";
	}
 
	@Override
	public String getDefaultDataRendererClassName() {
		return "capsis.extension.datarenderer.drgraph.DRGraph";
	}
 
	/**
	 * From AbstractDataExtractor.
	 */
	@Override
	public void setConfigProperties() {
 
		addIntProperty("SimqDETreeProfile.treeId", 1); // To choose the tree id
 
		addBooleanProperty("SimqDETreeProfile.hardwood", true);
		addBooleanProperty("SimqDETreeProfile.sapwood", true);
		addBooleanProperty("SimqDETreeProfile.bark", true);
	}
 
	/**
	 * From AbstractDataExtractor.
	 * 
	 * Computes the output data series. It works relatively to a particular Step
	 * (see init ()).
	 * 
	 * Return false if trouble while extracting.
	 */
	@Override
	public boolean doExtraction() {
		if (upToDate)
			return true;
 
		if (step == null)
			return false;
 
		try {
 
			listOfXYSeries.clear();
 
			SimqStand stand = (SimqStand) step.getScene();
 
			int treeId = getIntProperty("SimqDETreeProfile.treeId");
			SimqTree t = (SimqTree) stand.getTree(treeId);
			SimqTreeProfile treeProfile = t.getTreeProfile();
 
			if (isSet("SimqDETreeProfile.hardwood")) {
				XYSeries s = new XYSeries(Translator.swap("SimqDETreeProfile.yAxisVariableHardwood"),
						new Color(0, 220, 220));
				listOfXYSeries.add(s);
				SimqVerticalProfile hwp = treeProfile.getHardwoodDiameterProfile();
				for (int i = 0; i < hwp.size(); i++)
					s.addPoint(hwp.get(i).y * .5, hwp.get(i).x);
			}
 
			if (isSet("SimqDETreeProfile.sapwood")) {
				XYSeries s = new XYSeries(Translator.swap("SimqDETreeProfile.yAxisVariableSapwood"),
						new Color(220, 220, 0));
				listOfXYSeries.add(s);
				SimqVerticalProfile swp = treeProfile.getSapwoodDiameterProfile();
				for (int i = 0; i < swp.size(); i++)
					s.addPoint(swp.get(i).y * .5, swp.get(i).x);
			}
 
			if (isSet("SimqDETreeProfile.bark")) {
				XYSeries s = new XYSeries(Translator.swap("SimqDETreeProfile.yAxisVariableBark"), new Color(0, 220, 0));
				listOfXYSeries.add(s);
				SimqVerticalProfile bp = treeProfile.getBarkDiameterProfile();
				for (int i = 0; i < bp.size(); i++)
					s.addPoint(bp.get(i).y * .5, bp.get(i).x);
			}
 
		} catch (Exception exc) {
			Log.println(Log.ERROR, "SimqDETreeProfile2.doExtraction ()", "Exception: ", exc);
			return false;
		}
 
		upToDate = true;
		return true;
	}
 
	/**
	 * From DFListOfXYSeries.
	 */
	@Override
	public List<XYSeries> getListOfXYSeries() {
		return listOfXYSeries;
	}
 
	/**
	 * From DFListOfXYSeries.
	 */
	public List<String> getAxesNames() {
		List<String> v = new ArrayList<>();
		v.add(Translator.swap("SimqDETreeProfile.yLabel"));
		v.add(Translator.swap("SimqDETreeProfile.xAxisVariable"));
		return v;
	}
 
}

You must also provide translations for the following keys in a couple of files for english and french.

# simq/extension/dataextractor/DELabels_en.properties

SimqDETreeProfile.name = Profiles radius / Tree height
SimqDETreeProfile.treeId = Tree Id
SimqDETreeProfile.description = Profiles of stem compartments according to tree height
SimqDETreeProfile.hardwood = Hardwood
SimqDETreeProfile.sapwood = Sapwood
SimqDETreeProfile.bark = Bark
SimqDETreeProfile.yAxis = Height (m)
SimqDETreeProfile.hardwoodSerie = Hardwood
SimqDETreeProfile.sapwoodSerie = Sapwood
SimqDETreeProfile.barkSerie = Bark
SimqDETreeProfile.xAxis = Radius (in m)

# simq/extension/dataextractor/DELabels_fr.properties

SimqDETreeProfile.name = Rayon des profils / Hauteur de l'arbre
SimqDETreeProfile.treeId = Id de l'arbre
SimqDETreeProfile.description = Profils des compartiments de la tige en fonction de la hauteur
SimqDETreeProfile.hardwood = Duramen
SimqDETreeProfile.sapwood = Aubier
SimqDETreeProfile.bark = Ecorce
SimqDETreeProfile.yAxis = Hauteur (m)
SimqDETreeProfile.hardwoodSerie = Duramen
SimqDETreeProfile.sapwoodSerie = Aubier
SimqDETreeProfile.barkSerie = Ecorce
SimqDETreeProfile.xAxis = Rayon des profils (en m)

A distribution graph

Create a subclass of capsis.extension.dataextractor.DEDistribution in an “extension.dataextractor” package, then implement the abstract methods: give the name of the x Label ; calculate and return the value of the variable which distribution you are interested in:

This is a simple example:

package quergus.extension.dataextractor;
 
import jeeb.lib.util.Translator;
import quergus.model.QGModel;
import quergus.model.QGTree;
import capsis.extension.dataextractor.superclass.DEDistribution;
 
/**
 * Trees energy distribution.
 *
 * @author F. de Coligny - April 2012
 */
public class DEEnergyDistribution extends DEDistribution {
 
    static {
        Translator.addBundle("quergus.extension.dataextractor.DEEnergyDistribution");
    }
 
 
	public static boolean matchWith (Object referent) {
		return referent instanceof QGModel;
	}
 
	@Override
	public String getName() {
		return Translator.swap("DEEnergyDistribution.name");
	}
 
	@Override
	public String getAuthor() {
		return "F. de Coligny";
	}
 
	@Override
	public String getDescription() {
		return Translator.swap("DEEnergyDistribution.description");
	}
 
	@Override
	public String getVersion() {
		return "1.0";
	}
 
	@Override
	protected String getXLabel() {
		return Translator.swap("DEEnergyDistribution.energy");
	}
 
	@Override
	protected Number getValue(Object o) {
		return ((QGTree) o).getLightResult().getCrownEnergy();
	}
 
}

You must also provide translations for the following keys in a couple of files for english and french.

# quergus/extension/dataextractor/DEEnergyDistribution_en.properties

DEEnergyDistribution = N / Energy classes
DEEnergyDistribution.description = Distribution per energy classes
DEEnergyDistribution.energy = Energy (MJ)
# quergus/extension/dataextractor/DEEnergyDistribution_fr.properties

DEEnergyDistribution = N / Classes d'énergie
DEEnergyDistribution.description = Distribution par classes d'énergie
DEEnergyDistribution.energy = Energie (MJ)

A graph with a single data series at scene level over time

Create a subclass of capsis.extension.dataextractor.DETimeY in an “extension.dataextractor” package, then implement the abstract method: calculate and return the value for the given GScene at the given date.

This is a simple example:

package lsfmgm.extension.dataextractor;
 
import jeeb.lib.util.Translator;
import lsfmgm.model.LSFMModel;
import lsfmgm.model.LSFMStand;
import capsis.extension.dataextractor.superclass.DETimeY;
import capsis.kernel.GModel;
import capsis.kernel.GScene;
 
/**
 *Species Diversity versus Date.
 *
 * @author Chris Shen of CAF, 21th, Nov, 2009
 */
public class DETimeSpeciesDiversity extends DETimeY {
 
	// nb-14.08.2018
	static {
		Translator.addBundle("lsfmgm.extension.dataextractor.Labels");
	}
 
	static public boolean matchWith(Object referent) {
		if (!(referent instanceof LSFMModel)) {return false;}
		return true;
	}
 
	@Override
	public String getName() {
		return Translator.swap("DETimeSpeciesDiversity.name");
	}
 
	@Override
	public String getAuthor() {
		return "Xiangdon Lei, MaWu, Chris Shen";
	}
 
	@Override
	public String getDescription() {
		return Translator.swap("DETimeSpeciesDiversity.description");
	}
 
	@Override
	public String getVersion() {
		return "1.1";
	}
 
	@Override
	protected Number getValue(GModel m, GScene stand, int date) {
 
		return ((LSFMStand) stand).getSpeciesDiversity();
	}
 
}

You must also provide translations for the following keys in a couple of files for english and french.

# lsfmgm/extension/dataextractor/DETimeSpeciesDiversity_en.properties

DETimeSpeciesDiversity = SpeciesDiversity / Time
DETimeSpeciesDiversity.yLabel = SpeciesDiversity
DETimeSpeciesDiversity.description = SpeciesDiversity over time
# lsfmgm/extension/dataextractor/DETimeSpeciesDiversity_fr.properties

DETimeSpeciesDiversity = Diversité des espèces / Temps
DETimeSpeciesDiversity.yLabel = Diversité
DETimeSpeciesDiversity.description = Diversité des espèces en fonction du temps

A graph with several data series over time

Create a subclass of capsis.extension.dataextractor.DETimeYs (with an 's') in an “extension.dataextractor” package, then explicit the following:

Here is an example:

package woudyfor.extension.dataextractor;
 
import java.awt.Color;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
 
import capsis.defaulttype.plotofcells.SquareCell;
import capsis.extension.dataextractor.superclass.DETimeYs;
import capsis.kernel.GModel;
import capsis.kernel.GScene;
import jeeb.lib.util.Translator;
import woudyfor.model.WoudyModel;
import woudyfor.model.WoudyPlot;
import woudyfor.model.WoudyScene;
import woudyfor.model.WoudySeedling;
import woudyfor.model.WoudyShrub;
import woudyfor.model.WoudyShrubCell;
 
/**
 * A graph Number of seedlings over time.
 * 
 * @author Florian Delerue - March 2012
 */
 
public class WoudyTimeSeedlingN extends DETimeYs {
 
	static {
		Translator.addBundle("woudyfor.extension.dataextractor.WoudyGraphs");
	}
 
	private String standMean1 = Translator.swap ("WoudyTimeSeedlingN.standMean1");
	private String filledMean1 = Translator.swap ("WoudyTimeSeedlingN.filledMean1");
	private String max1 = Translator.swap ("WoudyTimeSeedlingN.max1");
 
	/**
	 * Extension framework compatibility method: compatible with OptimModel
	 * only.
	 */
	static public boolean matchWith(Object referent) {
		// compatible with OptimModel only
		return referent instanceof WoudyModel;
	}
 
	@Override
	public String getName() {
		return Translator.swap("WoudyTimeSeedlingN.name");
	}
 
	@Override
	public String getAuthor() {
		return "Florian Delerue";
	}
 
	@Override
	public String getDescription() {
		return Translator.swap("WoudyTimeSeedlingN.description");
	}
 
	@Override
	public String getVersion() {
		return "1.1";
	}
 
	@Override
	public void setConfigProperties() {
		addBooleanProperty(standMean1, true);
		addBooleanProperty(filledMean1, true);
		addBooleanProperty(max1, true);
	}
 
	@Override
	public String[] getYAxisVariableNames() {
		return new String[] { standMean1, filledMean1, max1 };
	}
 
	@Override
	protected Number getValue(GModel m, GScene scene, int date, int i) {
		WoudyScene ws = (WoudyScene) scene;
 
		if (i <= 0) {
			return isSet(standMean1) ? getStandMean(ws) : Double.NaN;
		} else if (i == 1) {
			return isSet(filledMean1) ? getFilledMean(ws) : Double.NaN;
		} else {
			return isSet(max1) ? getMax(ws) : Double.NaN;
		}
 
	}
 
	private double getStandMean(WoudyScene scene) {
 
		WoudyPlot plot = scene.getPlot();
		int nCell = plot.getCells().size();
 
		double n = 0;
		for (SquareCell cell : plot.getCells()) {
			WoudyShrubCell c = (WoudyShrubCell) cell;
			List<WoudySeedling> seedlings = c.getSeedlings();
 
			if (seedlings != null) {
				n += seedlings.size();
			}
 
		}
 
		return nCell == 0 ? 0 : n / nCell;
	}
 
	private double getFilledMean(WoudyScene scene) {
 
		WoudyPlot plot = scene.getPlot();
		int nCell = 0;
 
		double n = 0;
		for (SquareCell cell : plot.getCells()) {
			WoudyShrubCell c = (WoudyShrubCell) cell;
 
			// n cells with shrub
			Collection trees = c.getTrees();
			boolean cellWithShrub = false;
			if (trees != null) {
				for (Object o : trees) {
					if (o instanceof WoudyShrub) {
						cellWithShrub = true;
						break;
					}
				}
			}
			if (cellWithShrub) {
				nCell++;
 
				List<WoudySeedling> seedlings = c.getSeedlings();
				if (seedlings != null)
					n += seedlings.size();
			}
 
		}
 
		return nCell == 0 ? 0 : n / nCell;
	}
 
	private double getMax(WoudyScene scene) {
		int max = 0;
 
		WoudyPlot plot = scene.getPlot();
 
		for (SquareCell cell : plot.getCells()) {
			WoudyShrubCell c = (WoudyShrubCell) cell;
			List<WoudySeedling> seedlings = c.getSeedlings();
 
			int n = 0;
 
			if (seedlings != null) {
				n = seedlings.size();
			}
 
			max = Math.max(max, n);
 
		}
 
		return max;
	}
 
	/**
	 * Returns a color per curve: getCurves ().size () - 1.
	 */
	public Vector getColors() {
		// fc-28.11.2013 changed DFCurves into DFColoredCurves, provided this
		// default implementation
		// for getcolors () (no change). Subclasses can redefine this method to
		// return a different
		// color for each curve
		Vector v = new Vector();
		Color singleColor = getColor(); // see AbstractDataExtractor
		v.add(Color.GREEN.darker()); // standMean
		v.add(Color.BLUE); // filledMean
		v.add(Color.RED); // max
		return v;
	}
 
}

You must also provide translations for the following keys in a couple of files for english and french.

# woudyfor/extension/dataextractor/WoudyGraphs_en.properties

WoudyTimeSeedlingN = Number of seedlings / Time
WoudyTimeSeedlingN.description = Number of seedlings over time for non empty cells
WoudyTimeSeedlingN.yLabel = N/m2
# woudyfor/extension/dataextractor/WoudyGraphs_fr.properties

WoudyTimeSeedlingN = Nombre de plantules / Temps
WoudyTimeSeedlingN.description = Nombre de plantules en fonction du temps pour les cellules occupées
WoudyTimeSeedlingN.yLabel = N/m2

Alignment of labels in the right part of the curves

To align the labels on the curves, see the configuration panel:

Set different colors for the curves (optional)

To set different colors for the curves, you may redefine the getColors () method and return a vector of colors with n = getCurves ().size () - 1 entries. The default graph color (the color on the matching step button in the project manager) can be guessed with getColor (). The example below returns the default color for the mean curve and the red color for the max curve.

	/**
	 * Returns a color per curve: getCurves ().size () - 1.
	 */
	public Vector getColors () {
		// fc-28.11.2013 changed DFCurves into DFColoredCurves, provided this default implementation
		// for getcolors () (no change). Subclasses can redefine this method to return a different
		// color for each curve
		Vector v = new Vector ();
		Color singleColor = getColor (); // see AbstractDataExtractor
		v.add (singleColor); // mean
		v.add (Color.RED); // max
		return v;
	}

Optionally disable some data series

It is possible to add options in the graphs by redefining the setConfigProperties () method. These options may be used to optionally remove some data series by adapting the code below.

All possible configuration possibilities are explained in this doc.

Note: for better result, you may provide translations in english and french for 'mean' and 'max' in the labels files.

    @Override
    public void setConfigProperties() {
        addBooleanProperty("mean", true);
        addBooleanProperty("max", true);
    }
 
    @Override
    protected Number getValue(GModel m, GScene scene, int date, int i) {
        WoudyScene ws = (WoudyScene) scene;
 
        if (i <= 0) {
            return isSet ("mean") ? getMean (ws) : Double.NaN;
        } else {
            return isSet ("max") ? getMax (ws) : Double.NaN;
        }
 
    }

A graph with several data series over time, for one or several trees

This is a simple example:

package heterofor.extension.dataextractor;
 
import heterofor.model.HetModel;
import heterofor.model.HetScene;
import heterofor.model.HetTree;
import jeeb.lib.util.Translator;
import capsis.extension.dataextractor.superclass.DETimeYsTrees;
import capsis.kernel.GModel;
import capsis.kernel.GScene;
 
/**
 * A graph 'biomass for 5 compartments' over time for a given list of trees.
 *
 * @author F. de Coligny, M. Jonard - November 2013
 */
 
public class DETimeBiomass extends DETimeYsTrees {
 
	static {
		Translator.addBundle ("heterofor.extension.dataextractor.DELabels");
	}
 
	/**
	 * Extension dynamic compatibility mechanism. This matchwith method checks
	 * if the extension can deal (i.e. is compatible) with the referent.
	 */
	static public boolean matchWith (Object referent) {
		// compatible with HetModel only
		return referent instanceof HetModel;
	}
 
	@Override
	public String getName() {
		return Translator.swap("DETimeBiomass.name");
	}
 
	@Override
	public String getAuthor() {
		return "F. de Coligny, M. Jonard";
	}
 
	@Override
	public String getDescription() {
		return Translator.swap("DETimeBiomass.description");
	}
 
	@Override
	public String getVersion() {
		return "1.0";
	}
 
	@Override
	public String[] getYAxisVariableNames () {
		// the names of the variables
		return new String[] {Translator.swap ("leafBiomass"), Translator.swap ("branchBiomass"),
				Translator.swap ("stemBiomass"), Translator.swap ("rootBiomass"), Translator.swap ("fineRootBiomass")};
	}
 
	/**
	 * Returns the value for the 'i'th variable of the tree 'treeId'.
	 */
	@Override
	protected Number getValue (GModel m, GScene scene, int treeId, int date, int i) {
		HetScene sc = (HetScene) scene;
 
		HetTree t = (HetTree) sc.getTree (treeId);
 
		if (i <= 0) {
			return t.getLeafBiomass_kgC ();
		} else if (i == 1) {
			return t.getBranchBiomass_kgC ();
		} else if (i == 2) {
			return t.getStemBiomass_kgC ();
		} else if (i == 3) {
			return t.getRootBiomass_kgC ();
		} else if (i == 4) {
			return t.getFineRootBiomass_kgC ();
		} else {
			return 0;
		}
 
	}
 
}