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:
- static matchWith (): return true only if the tool is compatible with the given referent object. For data extractors (see below), the referent is the model object (extends GModel) connected to the project (e.g. QGModel).
- getName(): the name of the tool, translated, e.g. Translator.swap(“CepsTimeN.name”) (translation, see below))
- getDescription(): the description (1 sentence) of the tools, e.g. Translator.swap(“CepsTimeN.description”)
- getAuthor(): your name (not translated)
- getVersion(): some String, e.g. “1.0”
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 :
- setConfigProperties(): declare configuration properties to tune the graph (see lower and here)
- init (): synchronizes the graph on a given step of a simulation
- doExtraction(): main method: prepares the data series from the current simulation step
- getDefaultDataRendererClassName(): return the class name of the favourite data renderer for this extractor, e.g. capsis.extension.datarenderer.drgraph.DRGraph
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:
- getListOfXYSeries(): returns a List<XYSeries>
- getAxesNames(): returns a List<String> (two 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:
- getXLabel(): returns the name of the x axis
- getValue(Object o): return the value of the variable for the given object
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.
- getValue(GModel m, GScene stand, int date): 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:
- getYAxisVariableNames(): the names of the various variables you plan to calculate
- getValue(GModel m, GScene scene, int date, int i): calculate and return the values matching the 'i'th parameter (same order than the getYAxisVariableNames() method upper) for the given GScene at the given date
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
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; } } }