diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 794becbdae28340857498d66f7ee7f82a5332d62..7b1ed1fce5c57e850596ba4cd2a75ddd8df6737d 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -91,6 +91,7 @@ import { ModulesDiagramComponent } from "./components/modules-diagram/modules-di
 import { MacrorugoCompoundResultsTableComponent } from "./components/macrorugo-compound-results/macrorugo-compound-results-table.component";
 import { MacrorugoCompoundResultsComponent } from "./components/macrorugo-compound-results/macrorugo-compound-results.component";
 import { JetResultsComponent } from "./components/jet-results/jet-results.component";
+import { JetTrajectoryGraphComponent } from "./components/jet-trajectory-graph/jet-trajectory-graph.component";
 
 import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component";
 import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-close-calc/dialog-confirm-close-calc.component";
@@ -195,6 +196,7 @@ const appRoutes: Routes = [
     JalhydModelValidationMaxDirective,
     JalhydModelValidationStepDirective,
     JetResultsComponent,
+    JetTrajectoryGraphComponent,
     LogComponent,
     LogEntryComponent,
     ModulesDiagramComponent,
diff --git a/src/app/components/fixedvar-results/fixedvar-results.component.ts b/src/app/components/fixedvar-results/fixedvar-results.component.ts
index 3944501316aa65f5892429ea348f9c925ebb212e..211c44c80a9ce04385ac6e12c3f9d5f9b7299a62 100644
--- a/src/app/components/fixedvar-results/fixedvar-results.component.ts
+++ b/src/app/components/fixedvar-results/fixedvar-results.component.ts
@@ -22,13 +22,13 @@ export class FixedVarResultsComponent extends ResultsComponent implements DoChec
     /**
      * résultats non mis en forme
      */
-    private _fixedResults: FixedResults;
-    private _varResults: VarResults;
+    protected _fixedResults: FixedResults;
+    protected _varResults: VarResults;
 
     /**
      * true si les résultats doiventt être remis à jour
      */
-    private _doUpdate = false;
+    protected _doUpdate = false;
 
     @ViewChild(FixedResultsComponent, { static: false })
     private fixedResultsComponent: FixedResultsComponent;
@@ -116,7 +116,7 @@ export class FixedVarResultsComponent extends ResultsComponent implements DoChec
      * met à jour l'affichage des résultats
      * @returns true si les résultats ont pu être mis à jour
      */
-    private updateResults() {
+    protected updateResults() {
         const fixedUpdated = this._fixedResults !== undefined && this.fixedResultsComponent !== undefined;
         if (fixedUpdated) {
             this.fixedResultsComponent.results = this._fixedResults;
diff --git a/src/app/components/jet-results/jet-results.component.html b/src/app/components/jet-results/jet-results.component.html
index ca214f920cfd71834ec5f8aca81925b90cd94624..5b726da9770d5c6630bc867189ae8155f91ad364 100644
--- a/src/app/components/jet-results/jet-results.component.html
+++ b/src/app/components/jet-results/jet-results.component.html
@@ -4,7 +4,7 @@
 
     <results-graph *ngIf="showVarResults"></results-graph>
 
-    <trajectory-graph></trajectory-graph>
+    <jet-trajectory-graph *ngIf="hasResults"></jet-trajectory-graph>
 
     <div>
         <!-- table des résultats fixés -->
diff --git a/src/app/components/jet-results/jet-results.component.ts b/src/app/components/jet-results/jet-results.component.ts
index 4bbf4a2d08cdf6c3b8b4beca5d8aaaa969796828..8703b78e7f7f3528e8294e9def0553e95c24a948 100644
--- a/src/app/components/jet-results/jet-results.component.ts
+++ b/src/app/components/jet-results/jet-results.component.ts
@@ -1,6 +1,7 @@
-import { Component } from "@angular/core";
+import { Component, ViewChild } from "@angular/core";
 
 import { FixedVarResultsComponent } from "../fixedvar-results/fixedvar-results.component";
+import { JetTrajectoryGraphComponent } from "../jet-trajectory-graph/jet-trajectory-graph.component";
 
 @Component({
     selector: "jet-results",
@@ -11,4 +12,46 @@ import { FixedVarResultsComponent } from "../fixedvar-results/fixedvar-results.c
 })
 export class JetResultsComponent extends FixedVarResultsComponent {
 
+    /** graphique de trajectoire */
+    @ViewChild(JetTrajectoryGraphComponent, { static: false })
+    private jetTrajectoryGraphComponent: JetTrajectoryGraphComponent;
+
+    public get hasResults(): boolean {
+        return (
+            (this._fixedResults !== undefined && this._fixedResults.hasResults)
+            ||
+            (this._varResults !== undefined && this._varResults.hasResults)
+        );
+    }
+
+    public updateView() {
+        if (this.jetTrajectoryGraphComponent) {
+            this.jetTrajectoryGraphComponent.results = undefined;
+        }
+        super.updateView();
+    }
+
+    /**
+     * met à jour l'affichage des résultats
+     * @returns true si les résultats ont pu être mis à jour
+     */
+    protected updateResults() {
+        const superUpdated = super.updateResults();
+
+        let trajectoryGraphUpdated: boolean;
+        trajectoryGraphUpdated = this.jetTrajectoryGraphComponent !== undefined;
+
+        if (trajectoryGraphUpdated) {
+            // draw chart whether params are variating or not,
+            // hence different Results object for each case
+            if (this._varResults && this._varResults.hasResults) {
+                this.jetTrajectoryGraphComponent.results = this._varResults;
+            } else {
+                this.jetTrajectoryGraphComponent.results = this._fixedResults;
+            }
+            this.jetTrajectoryGraphComponent.updateView();
+        }
+
+        return superUpdated && trajectoryGraphUpdated;
+    }
 }
diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..6ef01d5045c3b0203bb619d213b5a1357261885e
--- /dev/null
+++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.html
@@ -0,0 +1,22 @@
+<div class="graph-results-container" #graphProfile fxLayout="row wrap" fxLayoutAlign="center center">
+    <div fxFlex="1 1 100%">
+        <div class="graph-profile-buttons">
+            <button mat-icon-button (click)="resetZoom()" [disabled]="! zoomWasChanged" [title]="uitextResetZoomTitle">
+                <mat-icon color="primary">replay</mat-icon>
+            </button>
+            <button mat-icon-button (click)="exportAsImage(graphProfile)" [title]="uitextExportImageTitle">
+                <mat-icon color="primary">image</mat-icon>
+            </button>
+            <button mat-icon-button *ngIf="! isFullscreen" (click)="setFullscreen(graphProfile)" [title]="uitextEnterFSTitle">
+                <mat-icon color="primary" class="scaled12">fullscreen</mat-icon>
+            </button>
+            <button mat-icon-button *ngIf="isFullscreen" (click)="exitFullscreen()" [title]="uitextExitFSTitle">
+                <mat-icon color="primary" class="scaled12">fullscreen_exit</mat-icon>
+            </button>
+        </div>
+
+        <div *ngIf="! displayChart" class="fake-chart"></div><!-- trick to avoid blinking effect due to forceRebuild -->
+        <chart *ngIf="displayChart" type="scatter" [data]="graph_data" [options]="graph_options" #graphChart>
+        </chart>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..19266623bd9da70c907c1db956b30eeecb2c443a
--- /dev/null
+++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.scss
@@ -0,0 +1,34 @@
+.graph-results-container{
+    display: block;
+    background-color: white;
+}
+
+.graph-profile-buttons {
+    padding-right: 10px;
+    padding-top: 4px;
+    margin-bottom: -30px;
+    text-align: right;
+    background-color: white;
+
+    button {
+        margin-left: 3px;
+        width: auto;
+
+        mat-icon {
+            &.scaled12 {
+                transform: scale(1.2);
+            }
+        }
+
+        &:disabled {
+            mat-icon {
+                color: #bfbfbf;
+            }
+        }
+    }
+}
+
+.fake-chart {
+    width: 100%;
+    padding-top: 50%;
+}
diff --git a/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..50a014d992e004a1b611d7008a5af524fcdc24a4
--- /dev/null
+++ b/src/app/components/jet-trajectory-graph/jet-trajectory-graph.component.ts
@@ -0,0 +1,268 @@
+import { Component, ViewChild, ChangeDetectorRef } from "@angular/core";
+
+import { ChartComponent } from "angular2-chartjs";
+
+import { I18nService } from "../../services/internationalisation.service";
+import { ResultsComponent } from "../fixedvar-results/results.component";
+import { IYSeries } from "../../results/y-series";
+import { FixedResults } from "../../results/fixed-results";
+import { VarResults } from "../../results/var-results";
+import { fv } from "../../util";
+
+import { Jet } from "jalhyd";
+
+@Component({
+    selector: "jet-trajectory-graph",
+    templateUrl: "./jet-trajectory-graph.component.html",
+    styleUrls: [
+        "./jet-trajectory-graph.component.scss"
+    ]
+})
+export class JetTrajectoryGraphComponent extends ResultsComponent {
+
+    @ViewChild(ChartComponent, { static: false })
+    private chartComponent;
+
+    private _results: FixedResults | VarResults;
+
+    private _zoomWasChanged = false;
+
+    private _varValuesLists: any = {};
+
+    /** used to briefly destroy/rebuild the chart component, to refresh axis labels (@see bug #137) */
+    public displayChart = true;
+
+    /*
+     * config du graphe
+     */
+    public graph_data: { datasets: any[] };
+    public graph_options: any = {
+        responsive: true,
+        maintainAspectRatio: true,
+        aspectRatio: 1.5,
+        animation: {
+            duration: 0
+        },
+        legend: {
+            display: true,
+            position: "bottom",
+            reverse: false
+        },
+        title: {
+            display: true,
+            text: this.intlService.localizeText("INFO_JET_TITRE_TRAJECTOIRE")
+        },
+        elements: {
+            line: {
+                tension: 0
+            }
+        }
+    };
+
+    public constructor(
+        private intlService: I18nService,
+        private cd: ChangeDetectorRef
+    ) {
+        super();
+        // do not move following block out of constructor or scale labels won't be rendered
+        this.graph_options["scales"] = {
+            xAxes: [{
+                type: "linear",
+                position: "bottom",
+                ticks: {
+                    precision: ResultsComponent.CHARTS_AXIS_PRECISION
+                },
+                scaleLabel: {
+                    display: true,
+                    labelString: this.intlService.localizeText("INFO_LIB_ABSCISSE")
+                }
+            }],
+            yAxes: [{
+                type: "linear",
+                position: "left",
+                ticks: {
+                    precision: ResultsComponent.CHARTS_AXIS_PRECISION
+                },
+                scaleLabel: {
+                    display: true,
+                    labelString: this.intlService.localizeText("INFO_LIB_ALTITUDE")
+                }
+            }]
+        };
+        // enable zoom and pan (using "chartjs-plugin-zoom" package)
+        const that = this;
+        this.graph_options["plugins"] = {
+            zoom: {
+                pan: {
+                    enabled: false, // conflicts with drag zoom
+                    mode: "xy",
+                },
+                zoom: {
+                    enabled: true,
+                    drag: { // conflicts with pan; set to false to enable mouse wheel zoom,
+                        borderColor: "rgba(225,225,225,0.3)",
+                        borderWidth: 1,
+                        backgroundColor: "rgba(0,0,0,0.25)"
+                    },
+                    mode: "xy",
+                    // percentage of zoom on a wheel event
+                    // speed: 0.1,
+                    onZoomComplete: function(t: any) { return function() { t.zoomComplete(); }; }(that)
+                }
+            }
+        };
+        // format numbers in tooltips
+        this.graph_options.tooltips = {
+            displayColors: false,
+            callbacks: {
+                label: (tooltipItem, data) => {
+                    return "(" + fv(Number(tooltipItem.xLabel)) + ", " + fv(Number(tooltipItem.yLabel)) + ")";
+                }
+            }
+        };
+    }
+
+    /** forces Angular to rebuild the chart @see bug #137 */
+    private forceRebuild() {
+        this.displayChart = false;
+        const that = this;
+        setTimeout(() => { // trick
+            that.displayChart = true;
+        }, 10);
+    }
+
+    public set results(r: FixedResults | VarResults) {
+        this.forceRebuild(); // used for (de)activating legend in generateScatterGraph()
+        this._results = r;
+
+        if (this._results) {
+            const nub = this._results.result.sourceNub as Jet;
+            const length = nub.variatingLength();
+            // extract variable values list for legend
+            if (nub.resultHasMultipleValues()) {
+                for (const p of nub.parameterIterator) {
+                    if (p.hasMultipleValues) {
+                        this._varValuesLists[p.symbol] = [];
+                        if (nub.calculatedParam === p) { // calculated
+                            for (let i = 0; i < length; i++) {
+                                this._varValuesLists[p.symbol].push(nub.result.resultElements[i].vCalc);
+                            }
+                        } else { // variating
+                            const iter = p.getExtendedValuesIterator(length);
+                            while (iter.hasNext) {
+                                const nv = iter.next();
+                                this._varValuesLists[p.symbol].push(nv.value);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public zoomComplete() {
+        this._zoomWasChanged = true;
+        this.cd.detectChanges();
+    }
+
+    public get zoomWasChanged(): boolean {
+        return this._zoomWasChanged;
+    }
+
+    public updateView() {
+        this.generateScatterGraph();
+    }
+
+    /**
+     * génère les données d'un graphe de type "scatter"
+     */
+    private generateScatterGraph() {
+        const ySeries = this.getYSeries();
+
+        // hide legend when there is only 1 series
+        this.graph_options.legend.display = (ySeries.length > 1);
+
+        this.graph_data = {
+            datasets: []
+        };
+
+        // build Y data series
+        for (const ys of ySeries) {
+            if (ys.data.length > 0) {
+                // push series config
+                this.graph_data.datasets.push({
+                    label: ys.label,
+                    data: ys.data,
+                    borderColor: ys.color, // couleur de la ligne
+                    backgroundColor: "rgba(0,0,0,0)",  // couleur de remplissage sous la courbe : transparent
+                    showLine: "true"
+                });
+            }
+        }
+    }
+
+    public exportAsImage(element: HTMLDivElement) {
+        const canvas: HTMLCanvasElement = element.querySelector("canvas");
+        canvas.toBlob((blob) => {
+            saveAs(blob, "chart.png");
+        }); // defaults to image/png
+    }
+
+    public resetZoom() {
+        this.chartComponent.chart.resetZoom();
+        this._zoomWasChanged = false;
+    }
+
+    public get uitextResetZoomTitle() {
+        return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_RESET_ZOOM");
+    }
+
+    public get uitextExportImageTitle() {
+        return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_EXPORT_IMAGE");
+    }
+
+    public get uitextEnterFSTitle() {
+        return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_ENTER_FS");
+    }
+
+    public get uitextExitFSTitle() {
+        return this.intlService.localizeText("INFO_GRAPH_BUTTON_TITLE_EXIT_FS");
+    }
+
+    private getYSeries(): IYSeries[] {
+        const ret: IYSeries[] = [];
+        const palette = ResultsComponent.distinctColors;
+        const nub = (this._results.result.sourceNub as Jet);
+        const trajectories = nub.generateTrajectories();
+
+        for (let i = 0; i < trajectories.length; i++) {
+            const traj = trajectories[i];
+            ret.push({
+                label: trajectories.length === 0 ? "" /* legend is hidden */ : this.getLegendForSeries(i),
+                color: palette[i % palette.length],
+                // map to IYSeries format
+                data: traj.map((t) => {
+                    return {
+                        x: t[0],
+                        y: t[1]
+                    };
+                })
+            });
+        }
+        console.log("Y series", ret.length, ret);
+        return ret;
+    }
+
+    /**
+     * Returns a label showing the boundary conditions values for
+     * the given iteration
+     * @param n index of the variating parameter(s) iteration
+     */
+    private getLegendForSeries(n: number): string {
+        return Object.keys(this._varValuesLists).map((symbol) => {
+            const values = this._varValuesLists[symbol];
+            const val = fv(values[n]);
+            return `${symbol} = ${val}`;
+        }).join(", ");
+    }
+}
diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts
index 5bfbb27c0261373f8f23e5786b8059d6f1544afe..9dc42709448f856e7651565605c6bd39fbac08bb 100644
--- a/src/app/services/formulaire.service.ts
+++ b/src/app/services/formulaire.service.ts
@@ -327,7 +327,6 @@ export class FormulaireService extends Observable {
      * @param calculatorName nom du module, à afficher dans l'interface
      */
     public createFormulaire(ct: CalculatorType, nub?: Nub, calculatorName?: string): Promise<FormulaireDefinition> {
-        console.log(">> Create form !!", ct);
         // Crée un formulaire du bon type
         const f: FormulaireDefinition = this.newFormulaire(ct);
         this._formulaires.push(f);
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index 57ac348e1a51d651789b08bb4c52a81c7ffcaabe..94cc1aa887ac81393e811c59fda0b880c860fcfc 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -156,8 +156,11 @@
     "INFO_WALL_REMOVED": "Wall #%s removed",
     "INFO_WALLS_AND_DEVICES_REMOVED": "%s wall(s) and %s device(s) removed",
     "INFO_WALLS_REMOVED": "%s wall(s) removed",
+    "INFO_JET_TITRE_TRAJECTOIRE": "Trajectory",
     "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.",
     "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon",
+    "INFO_LIB_ABSCISSE": "Abscissa (m)",
+    "INFO_LIB_ALTITUDE": "Altitude (m)",
     "INFO_LIB_LENGTHS": "Every length",
     "INFO_LIB_WIDTHS": "Every width",
     "INFO_LIB_SLOPES": "Every slope",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index 69ae2bef5cb9c5fbca1aba7898d3498847734904..68507047464f004ca83ff95fa62c47385de6966c 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -158,6 +158,9 @@
     "INFO_WALLS_REMOVED": "%s cloison(s) supprimée(s)",
     "INFO_LECHAPTCALMON_TITRE_COURT": "Lechapt-C.",
     "INFO_LECHAPTCALMON_TITRE": "Lechapt-Calmon",
+    "INFO_JET_TITRE_TRAJECTOIRE": "Trajectoire",
+    "INFO_LIB_ABSCISSE": "Abscisse (m)",
+    "INFO_LIB_ALTITUDE": "Altitude (m)",
     "INFO_LIB_LENGTHS": "Toutes les longueurs",
     "INFO_LIB_WIDTHS": "Toutes les largeurs",
     "INFO_LIB_SLOPES": "Toutes les pentes",