Zum Inhalt springen

Workshop: Wir basteln uns einen Tradingbot (Lektion 6a)


Empfohlene Beiträge

Hallo zusammen

In @Jokins Lektion 6 habe ich zwei Charts verlinkt, die direkt mit dem Bot im Browser ausgegeben werden können. Wie man sowas macht, will ich euch nicht vorenthalten. Hier also die Anleitung dazu. Da das hier kein IT-Forum ist, weiß ich nicht, was ich programmiertechnisch als bekannt voraussetzen kann, also versuche ich mal es so anfängerfreundlich wie möglich zu halten.

Damit wir die Daten vom Bot im Browser als grafische Charts bewundern können, brauchen wir etwas JavaScript für den Browser und serverseitig (PHP oder was immer benutzt wird) eine Möglichkeit, JSON-Daten rauszuschreiben.

Das meiste JavaScript müssen wir nicht selber schreiben oder hosten, sondern einfach nur einbinden. Der Browser lädt es aus dem Web und hält es dann im Cache, d.h. er muss es nur einmalig aus dem Web holen, sofern der Cache nicht geleert wird, was aber auch kein Problem wäre.

An Scripts brauchen wir hauptsächlich jQuery, jQuery-UI und ChartJS, ausserdem Json5 und etwas von github.com/konklone/json. Diese Scripts sind alle Open-Source, d.h. meistens unter MIT-Lizenz frei zu benutzen. Trotzdem muss ich darauf hinweisen, dass ihr bitte die Lizenzbedingungen selber nachlest und beachtet. Die Suchmaschine eures Vertauens kann da helfen.

JSON

Wer JSON nicht kennt: Es ist ist ein sog. Austauschformat und steht für JavaScript Object Notation. Im Grunde ist es eine Zeichenkette (String) mit bestimmter Stuktur. Ein JSON-String kann enthalten:

  • Objekte in geschweifen Klammen {}
  • Arrays in Eckigen Klammern []
  • Schlüssel-Werte-Paare als sog. Objekteigenschaften in der Form "Schlüssel": Wert

Folgendes ist z.B. ein JSON-String, der ein Objekt mit der Eigenschaft Name enthält:
 

{
  "Name": "Balance Bot",
}

Mehrere Objekteigenschaften müssen durch Kommata getrennt sein. Am besten immer ein Komma dranhängen, denn es schadet nicht, wenn auch der letzten Eigenschaft ein Komma folgt.

Der Bot muss zwei verschiedene JSON-Strings erzeugen und zum Browser schicken:

  1. Die eigentlichen Daten
  2. Ein ChartJS-Konfigurationsobjekt

Die eigentlichen Daten

Was der Bot etwa in Abschnitt show_csv.php 4 ausgibt, schreibe ich lieber in eine Datenbank-Tabelle und hole es mir dann von dort für die Charts. Aber wie auch immer, statt CSV brauchen wir jedenfalls für die Charts einen entsprechenden JSON-String. Dieser JSON-String muss die Daten als Array von Zeilen-Objekten enthalten, mit zwei Zeilen z.B. so:

[
  {
   "Datum":"2019-02-11T12:39:48.5930396+01:00",
   "Portfolio ETH":1.0379599575557499,
   "Virtual ETH":0.29300478999999996,
   "Soll % ETH":40,
   "Ist % ETH":39.33186891787987,
   "Virtual NEO in ETH":0.29956303999999995,
   "Soll % NEO":40,
   "Ist % NEO":40.21222390910949,
   "Virtual BNB in ETH":0.22648173755575002,
   "Soll % BNB":30,
   "Ist % BNB":30.40206275752841,
   "Virtual ADA in ETH":0.21891038999999998,
   "Soll % ADA":30,
   "Ist % ADA":29.3857133333621},
 },
 {
   "Datum":"2019-02-11T13:39:48.8719637+01:00",
   "Portfolio ETH":1.035181532901,
   "Virtual ETH":0.29300478999999996,
   "Soll % ETH":40,
   "Ist % ETH":39.479112327706595,
   "Virtual NEO in ETH":0.29970223999999995,
   "Soll % NEO":40,
   "Ist % NEO":40.38151867013942,
   "Virtual BNB in ETH":0.22326027290099998,
   "Soll % BNB":30,
   "Ist % BNB":30.081820137387545,
   "Virtual ADA in ETH":0.21921423,
   "Soll % ADA":30,
   "Ist % ADA":29.53666119247303,
 }
]


Die äusseren eckigen Klammern sind das Array, und die beiden Zeilen stehen jeweils als Objekte in geschweiften Klammern mit ihren Schlüssel-Wertepaaren als Objekteigenschaften.
Diese Zeilen-Objekte im Array sind durch Komma getrennt, wie in Arrays üblich. Im Array darf dem letzten Eintrag aber kein Komma folgen, im Unterschied zu den Objekteigenschaften, wo das erlaubt ist.


Das ChartJS-Konfigurationsobjekt

Die Charts im Browser zeichnet dann das Script ChartJS. ChartJS erwartet ein Objekt in der Form:

{
    type: '',
    data: {
        labels: [],
        datasets: [{
            label: '',
            data: [],
            yAxisID: '',
            backgroundColor: [],
            borderColor: [],
            borderWidth: (Zahl)
        }]
    },
    options: {
        scales: {
            yAxes: []
        }
    }
}


Dabei stehen die ' ' jeweils für einen String, [] für ein Array, und (Zahl) soll eine Zahl sein.
Was da jeweils genau rein muss, war nicht ganz leicht herauszufinden, weil die Infos ziemlich verstreut in der Doku liegen.

Folgendes habe ich rausgefunden:

  • type bezeichnet der Typ von Chart, am besten 'line' für unsere Zwecke (Linien-Chart)
  • data ist einfach ein Container-Objekt für die Chart-Daten
  • data.labels ist ein Array von Strings für die Bezeichnungen auf der X-Achse (horizontal). Die sind wichtig.
  • data.datasets ist das Array von Linien, die wir zeichnen wollen. Ein einzelnes 'dataset' ist paktisch eine Linie.
  • dataset.label ist die Bezeichnung für die Linie, z.B. "Virtual ETH"
  • dataset.data ist das Array der einzelnen Werte für die Linie (Zahlen). Die sind wichtig.
  • dataset.yAxisID ist der Name zugehörigen der Y-Achse, z.B. "Achse1", wichtig bei mehreren Achsen (siehe unten)
  • dataset.backgroundColor und dataset.borderColor sind zwei Farben für die Linie (Umrandung und Füllung u.a. in der Legende)
  • dataset.borderWidth ist die Dicke der Linie in Pixel
  • options.scales.yAxes ist ein Array von y-Achsen (vertikal), wobei jede y-Achse wiederum ein Objekt ist mit der Form:
{
    id: '',
    type: '',
    position: ''
    stacked: (bool)
    ticks: {
        beginAtZero: (bool)        
    },
}


Dabei steht (bool) für einen ein booleschen Wert, also true oder false.

  • id ist der Name der y-Achse, z.B. "Achse1", wichtig  bei mehreren Achsen, siehe datasat.yAxisID oben
  • type ist die Art von Achse, z.B. 'linear', wichtig bei mehreren Achsen.
  • position kann 'left' oder 'right' sein, also ob die Achse links oder rechts erscheint
  • stacked gibt an, ob sich die Werte addieren: Wenn mit stacked === true für zwei Linien der Wert z.B. 1 ist, dann würde die zweite bei 2 auf der Y-Achse liegen.
  • ticks ist einfach ein Objekt mit der Eigenschaft 'beginatZero' (und noch anderen, die wir nicht unbedingt brauchen)

So, das war bis jetzt ziemlich abstrakt. Hier mal ein konkretes JSON-Konfigurationsobjekt für einen Chart mit zwei y-Achsen für die Prozentwerte:

{
    "type":"line",
    "data":{
        "labels":null,
        "datasets":[
            {
                "label":"Soll % ETH",
                "data":null,
                "yAxisID":"Percentage1",
                "borderColor":"rgba(255, 99, 132, 1)",
                "backgroundColor":"rgba(255, 99, 132, 1)",
                "fill":"",
            },
            {
                "label":"Ist % ETH",
                "data":null,
                "yAxisID":"Percentage1",
                "borderColor":"rgba(255, 51, 95, 1)",
                "backgroundColor":"rgba(255, 51, 95, 1)",
                "fill":"",
            },
            {
                "label":"Soll % NEO",
                "data":null,
                "yAxisID":"Percentage1",
                "borderColor":"rgba(75, 192, 192, 1)",
                "backgroundColor":"rgba(75, 192, 192, 1)",
                "fill":"",
            },
            {
                "label":"Ist % NEO",
                "data":null,
                "yAxisID":"Percentage1",
                "borderColor":"rgba(53, 151, 151, 1)",
                "backgroundColor":"rgba(53, 151, 151, 1)",
                "fill":""},
            {
                "label":"Soll % BNB",
                "data":null,
                "yAxisID":"Percentage2",
                "borderColor":"rgba(255, 206, 86, 1)",
                "backgroundColor":"rgba(255, 206, 86, 1)",
                "fill":"",
            },
            {
                "label":"Ist % BNB",
                "data":null,
                "yAxisID":"Percentage2",
                "borderColor":"rgba(255, 183, 0, 1)",
                "backgroundColor":"rgba(255, 183, 0, 1)",
                "fill":"",
            },
            {
                "label":"Soll % ADA",
                "data":null,
                "yAxisID":"Percentage2",
                "borderColor":"rgba(170, 128, 255, 1)",
                "backgroundColor":"rgba(170, 128, 255, 1)",
                "fill":"",
            },
            {
                "label":"Ist % ADA",
                "data":null,
                "yAxisID":"Percentage2",
                "borderColor":"rgba(136, 77, 255, 1)",
                "backgroundColor":"rgba(136, 77, 255, 1)",
                "fill":"",
            }
        ]
    },
    "options":{
        "scales":{
            "yAxes":[
                {
                    "id":"Percentage1",
                    "type":"linear",
                    "ticks":{"beginAtZero":false},
                    "stacked":false,
                    "position":"left",
                },
                {
                    "id":"Percentage2",
                    "type":"linear",
                    "ticks":{"beginAtZero":false},
                    "stacked":false,
                    "position":"right",
                }
            ]
        }
    }
}


Die Zeilenumbrüche und Einrückungen sind nur für uns zur Übersicht. Das ganze Ding kann auch als einzige Zeile ohne Leerzeichen oder so vom Bot ausgegeben werden. Am Ende muss es ja nur die Maschine lesen können.
Und noch ein Beispiel mit zwei Achsen für den Gesamt-Portfoliowert (links im Chart) und die virtuellen Werte vom Bot (rechts im Chart):

{
"type":"line",
"data":{
    "labels":null,
    "datasets":[
        {
            "label":"Portfolio ETH",
            "data":null,
            "yAxisID":"Virt.Balance Base",
            "borderColor":"rgba(255, 51, 95, 1)",
            "backgroundColor":"rgba(255, 51, 95, 1)",
            "fill":"",
        },
        {
            "label":"Virtual ETH",
            "data":null,
            "yAxisID":"Virt.Balance Coin",
            "borderColor":"rgba(255, 159, 64, 1)",
            "backgroundColor":"rgba(255, 159, 64, 1)",
            "fill":"",
        },
        {
            "label":"Virtual NEO in ETH",
            "data":null,
            "yAxisID":"Virt.Balance Coin",
            "borderColor":"rgba(75, 192, 192, 1)",
            "backgroundColor":"rgba(75, 192, 192, 1)",
            "fill":"",
        },
        {
            "label":"Virtual BNB in ETH",
            "data":null,
            "yAxisID":"Virt.Balance Coin",
            "borderColor":"rgba(255, 206, 86, 1)",
            "backgroundColor":"rgba(255, 206, 86, 1)",
            "fill":"",
        },
        {
            "label":"Virtual ADA in ETH",
            "data":null,
            "yAxisID":"Virt.Balance Coin",
            "borderColor":"rgba(170, 128, 255, 1)",
            "backgroundColor":"rgba(170, 128, 255, 1)",
            "fill":"",
        }]
    },
    "options":{
        "scales":{
            "yAxes":[
                {
                    "id":"Virt.Balance Base",
                    "type":"linear",
                    "ticks":{"beginAtZero":false},
                    "stacked":false,
                    "position":"left",
                },
                {
                    "id":"Virt.Balance Coin",
                    "type":"linear",
                    "ticks":{"beginAtZero":false},
                    "stacked":false,
                    "position":"right",
                }
            ]
        }
    }
}


Der aufmerksame Leser wird bemerkt haben, dass in beiden Konfigurationsobjekten die wichtigsten Werte noch fehlen bzw. einfach mit null initialisiert sind, nämlich labels für die X-Achse und v.a. datasets.data für die eigentlichen Werte. Darum muss sich der Bot aber nicht kümmern: Sie werden schliessich im Browser mit Hilfe unserer Scripts aus dem ersten JSON.Objekt (mit den Daten) gelesen und eingesetzt.

Wie ihr das nun serverseitig in PHP oder wie immer bewerkstelligt, dass der Bot die beschriebenen JSON-Strings bauen kann, muss ich euch überlassen. Ich habe ja @Jokins Bot in Go bzw. Golang nachprogrammiert, mit PHP kenne ich mich nicht wirklich aus. Ich würde jedenfalls davon abraten, solche Strings stückweise quasi zu Fuss zusammenzusetzen. Besser ist es, wenn man für die nötigen Objekte und Eigenschaften jeweils assoziative Arrays oder sonstige Strukturen anlegt, so dass man am Ende eine befüllte Struktur hat, die genau dem JSON-Objekt entspricht, und diese dann z.B. in PHP an json_encode() übergibt, um den fertigen JSON-String zu erhalten.

 

Das war's auch schon mit dem schwierigen Teil. Der Rest wird einfach, versprochen :).

 

(Fortsetzung folgt)

 

 

Bearbeitet von Herr Coiner
wegen Perfektionismus ;)
  • Love it 1
  • Thanks 2
  • Like 1
Link zu diesem Kommentar
Auf anderen Seiten teilen

Und weiter geht's...

Nachdem die Daten serverseitig soweit vorgekaut sind, kommen wir nun zum clientseitigen Teil (Browser).
Zunächst treffen wir noch folgende Vorbereitungen:

  1. Script-Ordner erstellen
  2. Script runterladen

Script-Ordner erstellen

Um Ordnung zu halten, erstellen wir einen Ordner namens JScript auf der gleichen Ebene, wo auch die Datei index.php des Bots liegt, bzw. eben im Hauptverzeichnis des Webservers. Darin speichern wir dann unsere Scripts als normale Textdateien.

JavaScript runterladen

Eines der nötigen Scripts aus dem Web müssen wir leider selber hosten, weil es keine brauchbare CDN-Version gibt, die man einfach einbinden könnte. Dazu laden wir zunächst alles von https://github.com/konklone/json runter (Button Clone or download -> Download ZIP) und speichern es in einen beliebigen Ordner, nur nicht gerade beim Bot, weil wir ja nicht alles davon brauchen, nur dieses Script:  jquery.csv.js

Wir kopieren also nur die runtergeladene Datei jquery.csv.js in unseren neuen Ordner JSCript.

Eigentlich brauchen wir auch site.js, aber in leicht geänderter Form. Ich will die kleinen Änderungen nicht extra beschreiben, sondern poste hier gleich das Resultat, das wir im Ordner JSCript unter dem Namen jsonhelper.js speichern. EDIT: Ich habe den Code nach ganz unten in einen Anhang verschoben, weil er hier nur den Lesefluss stört: Was das Script im einzelnen macht ist nicht wichtig. Im Prinzip enthält es Hilfsroutinen, um möglicherweise verschachtelte JSON-Objekte in eine flache Struktur zu bringen, aber wie gesagt: unwichtig. Hauptsache es tut.

Eigene Scripts

Nun brauchen wir nur noch zwei eigene Scripts als jQuery-Widgets, die ich mir erlaubt habe selber zu kreiern. Wir speichern sie ebenfalls im Ordner JScript:

  1. serverdata.widget.js – liest unseren ersten JSON-String mit den eigentlichen Daten
  2.  json2charts.widget.js – liest unseren zweiten JSON-String mit dem/den ChartJS-Konfigurationsobjekt(en) und übergibt alles an ChartJS zum Zeichnen

Was unser eigener Code im einzelnen macht, ist jeweils in den Kommentaren beschrieben und ich hoffe, dass es auch verständlich genug ist. Ansonsten könnt ihr den Code ja einfach so benutzen wie er ist. Ich erhebe keine Urheberrechsansprüche, und vorsichtshalber auch keine Haftung. Er sollte aber mit allen korrekt aufgebauten JSON-Daten funktionieren.

Zu speichern in serverdata.widget.js:
 

$.widget( 'bot.serverdata', {

    chartCount: 0,     // öffentliche Variable mit der Anzahl Charts
    chartObjects : {}, // öffentliches Objekt mti Chart-Konfigurationen(en)
    columnValues: {},  // öffentliches Objekt mit Spalten und ihren Daten

    // Erzeugt unser jQuery-Widget für ein DOM-Element (this.element)
    _create: function() {
        'use strict';

        // Variablen deklarieren und dabei gleich befüllen

        var element = this.element, // Unser DOM-Element für leichteren Zugriff (ohne das 'this')
            jsonelt = element.find('section.json'), // Das json-Element mit den Daten vom Server

            // Die Tabellendaten vom Server aus dem entsprechenden DOM-Element holen
            jsonCodeElt = !!jsonelt && jsonelt.find('pre.raw.json code'), // Das JSON-Code-Element,
            jsonCode = !!jsonelt && jsonCodeElt.text(),                   // dessen Inhalt als Text
            jsonValue = !!jsonCode && jsonHelper.jsonFrom(jsonCode),      // und dann als JS-Objekt.

            // Die folgenden zwei Zeilen sind adaptiert von https://github.com/konklone/json.
            // Es geht darum, das JSON-Objekt in ein Array zu verwandeln. Ähnlich wie für CSV
            // stehen dann die Spaltennamen in der 1.Zeile vom Array 'rowValues' und danach
            // folgen die eigentlichen Werte.
            rowObjects = (!!jsonValue) && jsonHelper.flatten(jsonHelper.arrayFrom(jsonValue)),
            rowValues = (!!rowObjects) && $.csv.fromObjects(rowObjects, {justArrays: true}),

            // ChartJS-Konfigurationsobjekt(e) aus dem entsprechenden DOM-Element lesen
            chartsCodeElt = !!rowValues && element.find("pre.raw.charts code"), // Das JSON-Code-Element,
            chartsCode = !!chartsCodeElt && chartsCodeElt.text(),               // dessen Inhalt als Text
            chartObjects = !!chartsCode && jsonHelper.jsonFrom(chartsCode);     // und dann als JS-Objekt.

        this._extractColumValues(rowValues);

        // Die so vorbereiteten Daten als öffentliche
        // Eigenschaften des Widgets verfügbar machen.
        this.chartObjects = chartObjects || {};
        this.chartCount = this.chartObjects.length || 0;
    },

    _extractColumValues: function(rowValues) {
        'use strict';

        var i, columnNames = rowValues.shift(),        // Spalten-Namen oben abschneiden (1.Zeile)
            columnValues = this._transpose(rowValues); // Werte spaltenweise als Zeilen rausholen

        // Für jede Spalte...
        for (i = 0; i < columnNames.length; i += 1) {

            // Dem Spalten-Objekt eine Eigenschaft mit dem
            // Spaltennamen und den zugehörigen Werten geben:
            this.columnValues[columnNames[i]] = columnValues[i];
        }
    },

    // Diese magische Funktion macht Spalten zu Zeilen. Sie nimmt ein Array
    // von Zeilen mit Werten und gibt ein Array aller Werte pro Spalte zurück.
    // Wir brauchen das um die eigentlichen Werte für jede Chartline aus dem
    // Array von DB-Zeilen zu holen, wo die Werte ja spaltenweise vorliegen.
    _transpose: function (a) { return a[0].map((_, c) => a.map(r => r[c])); },

});

 

Zu speichern in json2charts.widget.js:
 

$.widget( 'bot.json2charts', {

    // Erzeugt unser jQuery-Widget für ein DOM-Element (this.element)
    _create: function() {
        'use strict';

        var i, chartElt, chartObjects, chartContext,              // Ein paar Variablen...
            canvasHtml = '<canvas class="areas"></canvas>',       // Die Leinwand für einen Chart,
            seververData = this.element.data( 'bot-serverdata' ), // Daten vom Widget bot.serverdata
            chartCount = seververData.chartCount;                 // und die Anzahl Charts von dort.

        if (chartCount === 0) { return; } // Keine Charts anzuzeigen? Na, dann halt nicht.

        chartObjects = seververData.chartObjects; // Das Array mit Chart-Konfiguration(en) als JSON-Objekt(e)

        // Mit jeder unserer ChartJS-Konfigurationen:
        for (i = 0; i < chartCount; i += 1){

            // Das Konfigurationsobjekt mit Daten füllen
            this._getChartLinesData(chartObjects[i].data, seververData.columnValues);

            chartElt = $(canvasHtml).attr( 'id', 'balanceChart' + i ); // Die Leinwand mit einer ID erzeugen
            chartElt.insertBefore(this.element.first());               // und ganz oben ins HTMl-Element setzen.

            // Jetzt nur noch den ganzen Chart zeichnen. Zum Aufruf siehe
            // https://www.chartjs.org/docs/latest/getting-started/usage.html
            this._setChartDefaults();                     // Erst noch Standardwerte setzen.
            chartContext = chartElt[0].getContext('2d');  // Einen ChartJS-Kontext erzeugen.
            new Chart(chartContext, chartObjects[i]);     // Den Chart schließlich zeichnen.
        }
    },

    // Füllt ein ChartJS-Konfigurationsobjekt mit den eigentlichen Daten.
    // Der übergebene Parameter 'data' ist die data-Eigenschaft des Objekts,
    // welches wir später an ChartJS mit new Chart(ctx, objekt) übergeben.
    _getChartLinesData: function(data, columnValues) {
        'use strict';

        var i, k, colName, colIndex,                    // Zählvariablen, Spaltenname und Spaltennummer
            datasets = data.datasets,                   // Ein ChartJS-dataset entspricht einer unserer Chartlinien.
            labels = data.labels || (data.labels = []); // Die 'labels' der X-Achse. Eins für jeden Wert muss sein.

        // Mit jeder Chartlinie:
        for( i = 0; i < datasets.length; i += 1){

            colName = datasets[i].label;              // Die Bezeichnung holen (Spalten-Name)
            datasets[i].data = columnValues[colName]; // Die Werte aus dieser Spalte zuweisen

            // Es werden nur so viele Werte angezeigt, wie es
            // 'labels' gibt. Daher für jeden unserer Werte:
            for ( k = 0; k < datasets[i].data.length; k += 1){

                // Einfach mal ein leeres 'label' erzeugen. Natürlich wäre z.B. der
                // Kalendertag besser. Den könnte man aus columnValues['Datum'] holen.
                labels[k] = '';
            }
        }
    },

    // Ein paar globale Standardwerte für Charts setzen, z.B. die lästige
    // Animation abschalten, gerade Linien statt Kurven zeichnen, und und und.
    _setChartDefaults: function(){
        'use strict';

        var golbal = Chart.defaults.global,
            elements = golbal.elements,
            point = elements.point,
            line = elements.line;

        Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: false } });

        golbal.animation.duration = 0;

        point.radius = 0;
        point.style = 'line';

        line.tension = 0;
        line.borderWidth = 1;
    },
});

 

Und nun zur endgültigen Ausgabe:

Das Hauptnahrungsmittel für Browser ist bekanntlich HTML. Der Bot kann den Browser z.B. mit einer Datei balanceBot.charts.html bzw. mit PHP eben balanceBot.charts.php füttern, die wir im Hauptordner speichern:

<!Doctype html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>Balance Bot Charts</title>

  <style>
      .json .raw, .json .charts { display: none; }
  </style>
</head>
<body>
  <div class="botcharts">
    <section class="json">
      <pre class="raw json"><code> JSON-DATEN </code></pre>
      <pre class="raw charts"><code> JSON-CHARTS </code></pre>
    </section>
  </div>

  <!-- jquery, jquery-ui -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

  <!-- jquery-csv -->
  <!-- Diese Version 0.70 stammt von https://github.com/konklone/json und ist *nicht* identisch
       mit der CDN-Version https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/0.70/jquery.csv.js!
       Der Autor hat wohl noch daran gebastelt, also müssen wir sie leider selber hosten. -->
  <script src="JScript/jquery.csv.js"></script>

  <!-- JSON5 parser -->
  <script src="https://unpkg.com/json5@2.1.0/dist/index.min.js"></script>

  <!-- Leicht abgewandelte Datei 'site.js' von https://github.com/konklone/json
       Die ersten 15 Zeilen dort brauchen wir z.B. nicht. -->
  <script src="JScript/jsonhelper.js"></script>

  <!-- ChartJS -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>

  <!-- Balance Bot Eigenbau -->
  <script src="JScript/serverdata.widget.js"></script>
  <script src="JScript/json2charts.widget.js"></script>

  <script> // Es geht erst los...
  $(function() {

    // ...wenn alles geladen ist...
    $(document).ready(function () {

      // ...dann unsere jQuery-Wigets für alle Elemente <div class="botcharts"> starten
      //   (wir haben oben zwar nur eins, aber es dürften auch mehrere sein)
      $('div.botcharts').serverdata().json2charts()
    });
  });
  </script>
</body>
</html>

Nach all diesen Vorbereitungen sieht die Ordnerstuktur etwa so aus:

Bot-Ordner
|
|index.php
|...
|balanceBot.charts.php
|...
•--JScript
   |
   |jquery.csv.js
   |jsonhelper.js
   |serverdata.widget.js
   |json2charts.widget.js

 

Die HTML-Datei müsst ihr für PHP natürlich anpassen, also mit den PHP-typischen Zeichen <?php etc. dekorieren ;)

Unsere JSON-Strings muss der Bot dann an die Stellen JSON-DATEN bzw. JSON-CHARTS schreiben:
 - Statt JSON-DATEN im HTML-Element <pre class="raw json"><code>JSON-DATEN</code> schreibt er das Array von Datenzeilen
 - Statt JSON-CHARTS im HTML-Element <pre class="raw charts"><code>JSON-CHARTS</code> schreibt er das Array von JSON-Konfigurationsobjekten

Ach ja, was ich im ersten Post noch vergaß zu erwähnen: Auch wenn man nur einen Chart ausgeben will, also nur ein einziges ChartJS-Konfigurationsobjekt braucht, erwartet es unser Script trotzdem in einem Array, also zwischen eckigen Klammern außen rum. Das hat den Vorteil, das man mit den gleichen JSON-Daten gleich mehrere Charts hintereinander ausgeben kann, jeweils mit eigener Konfiguration.

Das war's auch schon. Wenn die Dateien wie beschrieben gespeichert sind und der Bot die JSON-Daten korrekt rausschreibt, dann zeichnet der Browser auch schöne Charts daraus.

Viel Spass damit :)

P.S.: Sollte ich was vergessen haben oder etwas unverständlich sein, meldet euch einfach hier.

----------------------------------------------------

Anhang:
Zu speichern in jsonhelper.js:
 

var jsonHelper = function(){

  // adapted from csvkit's recursive JSON flattening mechanism:
  // https://github.com/onyxfish/csvkit/blob/61b9c208b7665c20e9a8e95ba6eee811d04705f0/csvkit/convert/js.py#L15-L34

  // depends on jquery and jquery-csv (for now)

  function parse_object(obj, path) {
      if (path == undefined)
          path = "";

      var type = $.type(obj);
      var scalar = (type == "number" || type == "string" || type == "boolean" || type == "null");

      if (type == "array" || type == "object") {
          var d = {};
          for (var i in obj) {

              var newD = parse_object(obj[i], path + i + "/");
              $.extend(d, newD);
          }

          return d;
      }

      else if (scalar) {
          var d = {};
          var endPath = path.substr(0, path.length-1);
          d[endPath] = obj;
          return d;
      }

      // ?
      else return {};
  }

  // otherwise, just find the first one
  function arrayFrom(json) {
      var queue = [], next = json;
      while (next !== undefined) {
          if ($.type(next) == "array") {

              // but don't if it's just empty, or an array of scalars
              if (next.length > 0) {

                var type = $.type(next[0]);
                var scalar = (type == "number" || type == "string" || type == "boolean" || type == "null");

                if (!scalar)
                  return next;
              }
          } if ($.type(next) == "object") {
            for (var key in next)
              queue.push(next[key]);
          }
          next = queue.shift();
      }
      // none found, consider the whole object a row
      return [json];
  }

  function removeTrailingComma(input) {
    if (input.slice(-1) == ",")
      return input.slice(0,-1);
    else
      return input;
  }

  // Rudimentary, imperfect detection of JSON Lines (http://jsonlines.org):
  //
  // Is there a closing brace and an opening brace with only whitespace between?
  function isJSONLines(string) {
  return !!(string.match(/\}\s+\{/))
  }

  // To convert JSON Lines to JSON:
  // * Add a comma between spaced braces
  // * Surround with array brackets
  function linesToJSON(string) {
    return "[" + string.replace(/\}\s+\{/g, "}, {") + "]";
  }

  // todo: add graceful error handling
  function jsonFrom(input) {
    var string = $.trim(input);
    if (!string) return;

    var result = null;
    try {
      result = JSON.parse(string);
    } catch (err) {
      console.log(err);
    }

    // See json5.org for a definition, and tests/json5/canonical.json for
    // an example of most of what JSON5 looks for.
    if (result == null) {
      console.log("JSON parse failed, retrying as JSON5 (json5.org)...")
      try {
        result = JSON5.parse(string);
        console.log("Yep: it was JSON5.");
      } catch (err) {
        console.log(err);
      }
    }

    // Allow a trailing comma at the end of the string.
    if (result == null) {
      console.log("JSON5 parse failed, retrying after removing trailing commas...")
      var relaxed = removeTrailingComma(string);
      try {
        result = JSON.parse(relaxed);
        console.log("Yep: removing trailing commas worked!");
      } catch (err) {
        console.log(err);
      }
    }

    // Try to detect if it's a JSON-lines object - if so, we can parse this.
    //
    // However, this should be TRIED LAST, because this could also modify the
    // CONTENT of the strings (it's not precise enough to only target real
    // line breaks) so if the problem was actually something else, then we want to
    // fix that problem instead. (That said, the string content modification
    // would be minimal -- adding a comma between braces, so that's why I feel
    // okay taking this approach.)
    if ((result == null) && isJSONLines(string)) {
      console.log("Parse failed. Looks like it might be JSON lines, retrying...")
      var lines = linesToJSON(string)
      try {
        result = JSON.parse(lines)
        console.log("Yep: it was JSON lines!")
      } catch (err) {
        console.log(err);
        if (lines.length < 5000) console.log(lines);
      }
    }

    if (result == null)
      console.log("Nope: that didn't work either. No good.")

    return result;
  }

  function flatten(inArray) {

    var row, outArray = [];
    for (row = 0; row < inArray.length; row += 1){
    
        outArray.push(parse_object(inArray[row]));
    }
    return outArray;
  }

  return { jsonFrom, arrayFrom, flatten };

}();

 

Bearbeitet von Herr Coiner
wegen notorischem Perfektionismus
  • Like 1
Link zu diesem Kommentar
Auf anderen Seiten teilen

11 hours ago, Herr Coiner said:

In @Jokins Lektion 6 habe ich zwei Charts verlinkt, die direkt mit dem Bot im Browser ausgegeben werden können. Wie man sowas macht, will ich euch nicht vorenthalten. Hier also die Anleitung dazu.

Super gemacht! Danke für deine Mühe und deine ausführliche Darstellung!

Ähnliche Probleme werde ich demnächst auch lösen müssen, da kann ich das gut gebrauchen.

Bisher war ich reiner Backend-Programmierer und habe um CSS/JS/... und alles Frontend-Gedöns einen weiten Bogen gemacht. :ph34r:

Link zu diesem Kommentar
Auf anderen Seiten teilen

Nachtrag: Gruselig ist der Umfang, den man benötigt, um den Bot um die Charts aufzupeppen. Quasi mehr Aufwand für die Erweiterung als für den ganzen "eigentlichen Bot".

Ich weiß schon, warum ich mich bisher immer von Frontends ferngehalten habe ... :wub:

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 4 Stunden schrieb PeWi:

Nachtrag: Gruselig ist der Umfang, den man benötigt, um den Bot um die Charts aufzupeppen. Quasi mehr Aufwand für die Erweiterung als für den ganzen "eigentlichen Bot".

Ich weiß schon, warum ich mich bisher immer von Frontends ferngehalten habe ... :wub:

Wegen jQuery, jQuery-UI und allem? Naja, das finde ich nicht schlimm. Ich habe da wenig Mitleid mit dem Browser. Der soll ruhig auch etwas arbeiten. Es geht natürlich auch ohne das Zeug, aber dann ist man nicht mehr so flexibel, muss mehr Code selber schreiben und vor allem: pflegen. Nach meiner Erfahrung fährt man am besten, wenn man die Arbeit irgendwelchen Tools überlässt, die man nicht selber schreiben muss. Dann kümmern sich nämlich andere darum, dass es immer funktioniert.

Letztlich beschränkt sich ja der eigene Code auf die paar Zeilen in den zwei Scripten. Das ist nun wirklich nicht viel, und man kann sie natürlich auch auch in einer einzigen Datei haben. Wenn man noch einen Minifier drüber lässt, liegt der eigene Code wohl weit unter 1 KB, so what?

Und der Bot, so wie er ist, braucht zwar wenig eigenen Code, aber dafür ist er auch ziemlich unflexibel. Wenn man XAMP mit einbezieht, was man fairerweise tun muss, dann ist der Umfang ja auch gewaltig. Wehe, man will etwas ändern an der Strategie oder an der DB, dann hat man jedesmal ne Menge Arbeit vielen Stellen. In meiner Go-Version muss ich z.B. keine DB-Tabelle händisch anlegen, nicht mal eine Datenbank installieren und auch keinen Webserver. Das ist alles im Programm mit drin und braucht weit unter 10 MB im Arbeitsspeicher. Das sollte sollte jeder noch so kleine Raspi schaffen :).

Bearbeitet von Herr Coiner
Link zu diesem Kommentar
Auf anderen Seiten teilen

Nachtrag:
Wegen des Umfangs muss ich noch sagen, dass das, was ich hier beschrieben habe, natürlich nur ein Weg ist. Es ist nicht der einzige. Wer nur ChartJS ohne etwas drumherum benutzen will, der kann das machen, indem er einfach nur die ChartJS-API bedient mit dem "Konfigurationsobjekt", wie ich es nenne. Der Bot kann ja die Daten jeweils direkt ins entspechende data-Array schreiben. Dann braucht man den ersten JSON-String nicht, der praktisch nur die DB-Tabelle darstellt, und man braucht dann auch die Scripts nicht, die daraus lesen um die data-Arrays zu erstellen.

In meiner Version brauche ich halt die DB-Daten noch anderweitig im Browser, weil ich nicht nur die Charts ausgebe (die sind eher Nebenprodukt), sondern alle Daten auch als richtige Tabelle, in der man blättern kann (die Charts zeigen jeweils den Ausschnitt, den man in der Tabelle wählt), und ausserdem kann man alles noch als CSV anzeigen (zum Rauskopieren) oder einfach runterladen, wenn man CSV für weitere Analysen haben will. Deshalb eben zwei JSON-Strings und etwas mehr Scripts drumrum als unbedingt nötig für die Charts.

Bearbeitet von Herr Coiner
Link zu diesem Kommentar
Auf anderen Seiten teilen

On 2/23/2019 at 1:31 PM, Herr Coiner said:

In meiner Go-Version [...] ist alles im Programm mit drin und braucht weit unter 10 MB im Arbeitsspeicher.

Mir sind die Vorzüge von Go durchaus bewusst. :cool:

Das hat jetzt nichts mit Go zu tun - ich hadere nur gerne mit der großzügigen Einbindung  und Nutzung vieler Bibliotheken, weil ich das Programmieren vor 35 Jahren auf einem Commodore C64 mit sagenhaften 64 KByte gelernt habe. Da hat man trotz Assembler mit jeden Bit gegeizt - das prägt einfach ... ;)

Bearbeitet von PeWi
Link zu diesem Kommentar
Auf anderen Seiten teilen

Am 25.2.2019 um 10:17 schrieb PeWi:

Das hat jetzt nichts mit Go zu tun - ich hadere nur gerne mit der großzügigen Einbindung  und Nutzung vieler Bibliotheken, weil ich das Programmieren vor 35 Jahren auf einem Commodore C64 mit sagenhaften 64 KByte gelernt habe. Da hat man trotz Assembler mit jeden Bit gegeizt - das prägt einfach ... ;)

Verstehe. Meine Anfänge waren von 33 Jahren... da gab's schon die ersten IBM-PCs. Habe immer dafür plädiert, dass Software-Entwickler auf möglichst schwachbrüstigen Rechnern programmieren sollten. So ist man gezwungen effiziente Programme ohne Verschwendung von Ressourcen zu entwickeln, die dann natürlich auf den besseren Rechnern der Anwender richtig gut laufen.🤩

  • Like 2
Link zu diesem Kommentar
Auf anderen Seiten teilen

Hmm ... bei diesem teil meckert er bei mir:

 

var i, columnNames = rowValues.shift(),        // Spalten-Namen oben abschneiden (1.Zeile)
            columnValues = this._transpose(rowValues); // Werte spaltenweise als Zeilen rausholen

"shift()" wäre ein nicht deklarierte Funktion.

Leider bin ich in JQuery absolut unfit ... muss ich mir also nochmal die Tage anschauen, dass ich das vielleicht noch irgendwie hinbekomme.

Ansonsten bleib ich doch erstmal bei Excel.

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 4 Stunden schrieb Jokin:

Hmm ... bei diesem teil meckert er bei mir:

 


var i, columnNames = rowValues.shift(),        // Spalten-Namen oben abschneiden (1.Zeile)
            columnValues = this._transpose(rowValues); // Werte spaltenweise als Zeilen rausholen

"shift()" wäre ein nicht deklarierte Funktion.

Leider bin ich in JQuery absolut unfit ... muss ich mir also nochmal die Tage anschauen, dass ich das vielleicht noch irgendwie hinbekomme.

Ansonsten bleib ich doch erstmal bei Excel.

shift() ist eine Standard-Methode von Arrays, hat nichts mit jQuery zu tun. Es wird das erste Element im Array entfernt und zurückgegeben, im Unterschied zu pop(), wo das letzte Element entfernt wird.

Wenn rowValues.shift() nicht gültig ist, dann ist rowValues kein Array.

Der Grund kann z.B. sein, dass nicht alle Scripts geladen wurden, dein JSON nicht sauber ist oder nicht im richtigen HML-Element steht. Die CSS-Namen sind wichtig. Mit ihrer Hilfe findet jQuery die richtigen Elemente.

Du kannst im Firefox-Debugger mal einen Haltepunkt setzen auf die Zeile

this._extractColumValues(rowValues);

Dann kannst du feststellen, was los ist. Vermutlich wird dein JSON nicht gefunden und rowValues ist null oder so.

P.S.:

Oder besser den Haltepunkt auf

var element = this.element,

Dann kannst du jede Variablenzuweisung einzeln kontrollieren durch schrittweises Weitergehen. Ich habe solche möglichen Fehler extra nicht abgefangen, weil man sonst keinen Anhaltspunkt hätte, warum nichts auf dem Schirm erscheint. So bekommt man wenigstens vom Debugger die Meldung, was wo nicht passt.

 

Bearbeitet von Herr Coiner
  • Thanks 1
Link zu diesem Kommentar
Auf anderen Seiten teilen

Erstelle ein Benutzerkonto oder melde Dich an, um zu kommentieren

Du musst ein Benutzerkonto haben, um einen Kommentar verfassen zu können

Benutzerkonto erstellen

Neues Benutzerkonto für unsere Community erstellen. Es ist einfach!

Neues Benutzerkonto erstellen

Anmelden

Du hast bereits ein Benutzerkonto? Melde Dich hier an.

Jetzt anmelden
×
×
  • Neu erstellen...

Wichtige Information

Wir haben Cookies auf Deinem Gerät platziert. Das hilft uns diese Webseite zu verbessern. Du kannst die Cookie-Einstellungen anpassen, andernfalls gehen wir davon aus, dass Du damit einverstanden bist, weiterzumachen.