y-Achsenskalierung bei SVG-Plots

Begonnen von xenos1984, 28 Juni 2020, 14:18:32

Vorheriges Thema - Nächstes Thema

xenos1984

Da ich inzwischen einige SVG-Plots angelegt habe, sind mir noch ein paar weitere Möglichkeiten aufgefallen, an denen man vielleicht etwas bei der Skalierung der y-Achse verbessern könnte. Zwei davon beziehen sich speziell auf logarithmische Darstellung, eine auf sehr große bzw. sehr kleine numerische Werte.




1. Wertebereiche über 20.000.000 bzw. unter 0,001 werden nicht richtig abgebildet.

Die Funktion, die Ober- und Untergrenze für die y-Achse auswählt, vergleicht den Wertebereich mit einer fest eingestellten Liste. Hier die Liste in der 98_SVG.pm:

1333   my @limit = (0.001, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50,
1334                100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000,
1335                200000, 500000, 1000000, 2000000);





Falls gewünscht, kann ich mich gerne an einem Patch versuchen. Für Punkt 1 habe ich mal eine Funktion geschrieben, zwar nicht in Perl, aber das sollte ich hinbekommen. Punkt 3 ist sicher auch leicht zu beheben. Punkt 2 braucht wahrscheinlich etwas mehr Nachdenken...

Wenn man nun einen sehr kleinen Wertebereich hat, wird immer der erste Wert der Liste zurückgegeben. D.h., selbst wenn man Zahlenwerte darstellen möchte, die im Bereich < 0,001 liegen, wird die Achse immer auf 0,001 skaliert, und der Graph damit "zusammengestaucht". Hat man dagegen Werte > 20.000.000, liefert die Liste gar keinen Treffer und skaliert die Achse stattdessen auf 1. Damit landet der Graph dann um einen Faktor 1.000.000 außerhalb des Plots und der User wundert sich, warum der Plot leer ist (ging mir jedenfalls so).

Man kann natürlich den User darauf hinweisen, dass die zu plottenden Zahlenwerte in dem angegebenen Bereich liegen sollten (falls das schon in der Doku stehen sollte, habe ich es übersehen), und man ggf. die Einheit anpassen sollte (in meinem Fall, Speicher in MB oder GB statt in kB anzeigen lassen). Oder (was mein Vorschlag wäre), man ersetzt die fixe Liste durch eine Funktion, die dynamisch die Achse an den Wertebereich anpasst.




2. Bei logarithmischer y-Achse wird die Untergrenze nicht optimal gesetzt.

Das Problem geht in eine ähnliche Richtung, bezieht sich nämlich auf die gleiche Funktion zur Skalierung der y-Achse, bzw. Festlegung von deren Grenzen. In meinem konkreten Anwendungsbeispiel habe ich einige Plots, bei denen Arbeitsspeicher bzw. Netzwerkauslastung logarithmisch dargestellt werden, weil die Werte naturgemäß stark schwanken (z.B. ein paar 100 MB bei einem RPi bis ein paar 10GB bei einem Arbeitsrechner - und wenn man da linear auf 10GB skaliert, sieht man nicht mehr, wenn sich beim RPi der genutzte Speicher von 100MB auf 200MB erhöht). Die Funktion, die Ober- und Untergrenze des Plots festlegt, berücksichtigt aber nicht, dass die Achse logarithmisch ist, sondern tut das, was bei linearer Skalierung sinnvoll ist: Wenn die Obergrenze 10GB ist, legt sie die Untergrenze auf 0, weil die 100MB Offset nur 1% ausmachen würden und im Plot ohnehin nicht zu sehen wären. Bei logarithmischer Skalierung wird dann aber diese 0 zu einer 1 bereinigt, und der Bereich von 1MB bis 100MB ist dann genau so groß wie der von 100MB bis 10GB. Anders gesagt, sämtliche Daten liegen in der oberen Hälfte 100MB-10GB des Plots, währen die untere 1MB-100MB komplett ungenutzt ist.

Besser wäre es, wenn die Funktion zur Skalierung der y-Achse berücksichtigen würde, das logarithmisch skaliert werden soll, und entsprechend die Untergrenze anders wählen - in dem Fall 100MB.




3. Wenn nur eine y-Achse genutzt wird und diese logarithmisch skaliert wird, wird auf der anderen Seite des Plots eine Achse mit gleichem Bereich, aber linearer Skalierung angezeigt.

Hier wird zwar bei Vorhandensein nur einer Achse der Wertebereich kopiert:

1735   #-- just in case we have only one data line, but want to draw both axes
1736   $hmin{x1y1}=$hmin{x1y2}, $hmax{x1y1}=$hmax{x1y2} if(!defined($hmin{x1y1}));
1737   $hmin{x1y2}=$hmin{x1y1}, $hmax{x1y2}=$hmax{x1y1} if(!defined($hmin{x1y2}));


...aber nicht, ob es eine lineare oder logarithmische Skala ist.




Falls gewünscht, kann ich mich an einem Patch versuchen.

rudolfkoenig

ZitatFalls gewünscht, kann ich mich an einem Patch versuchen.
Fuer #2 und #3 gerne (insb. weil ich fuer die Tests noch Daten und Anzeige basteln muesste).
#1 wuerde ich per Doku (bzw. Vorschlag, passende Einheiten zu waehlen) loesen, weil (neben Anzeigeproblemen) einfacher ist, kleinere Zahlen zu verstehen.

xenos1984

Hm... Ich schaue mal, ob ich #2 ohne #1 gelöst bekomme ;) Ich hatte da ursprünglich an eine Funktion gedacht, die je nach Skalierung (logarithmisch oder linear) Ober- und Untergrenze aus den Plot-Werten berechnet - aber das, was ich da programmieren wollte, vergleicht dann eben nicht mehr mit der Liste. #3 ist sicher schnell erledigt.

xenos1984

#3
Und da stellt sich mir schon das nächste Rätsel...

201 sub
202 SVG_log10($)
203 {
204   my ($n) = @_;
205
206   return 0.0000000001 if( $n <= 0 );
207
208   return log(1+$n)/log(10);
209 }


So wie ich das sehe, soll die Funktion negative Werte bereinigen, d.h. auf eine Art Minimalwert setzen. Und der Rest der Funktion scheint dafür zu sorgen, dass alle positiven Werte über diesem Minimalwert liegen, d.h. statt den Logarithmus von $n zu berechnen, wird der von $n + 1 berechnet. Sicher, für große Werte von $n fällt die 1 nicht ins Gewicht, aber wenn man kleine Zahlen darstellen will, die deutlich unter 1 liegen, ist die Darstellung nicht mehr logarithmisch, sondern näherungsweise linear. Dadurch wird dann z.B. 0,001 .. 0,01 nicht auf -3 .. -2 abgebildet, wie man erwarten würde, sondern wieder auf log10($n + 1) ≈ $n / log(10). Das ist dann zwar positiv bzw. größer als der Ersatzwert für negative Zahlen, aber nicht mehr wirklich logarithmisch.

Ich vermute mal, das Verhalten ist so gewollt und der User ist auch hier angehalten, größere Zahlenwerte zu benutzen? Das zu berücksichtigen macht die Aufgabe einer automatischen Skalierung etwas komplexer, als ich angenommen hatte...

Das Anzeigeproblem kann ich durchaus nachvollziehen - Zahlen wie 0.00000001 oder 200.000.000 als Achsenbeschriftung sind ja nicht wirklich schön. Auch das Verständnisproblem, da bei einer Darstellung wie 2e-12 oder 2 * 10^9 sicher nicht jeder weiß, was gemeint ist.

Ich persönlich würde das über eine Option in der .gplot Datei lösen wollen, mit der man das Anzeigeformat festlegen könnte, z.B. als printf-Format, mit einem sinnvollen Vorgabewert, z.B. %g. Wenn man dann zu große oder zu kleine Werte hat, die zu lang und unlesbar werden, hat man als User die Wahl, die Daten in einen anderen Wertebereich zu konvertieren oder eine andere Darstellung zu wählen.

Oh, ich sehe jetzt erst, das gibt es ja schon, wenn auch nicht dokumentiert:

1893         my $name = ($axis==1 ? "y":"y$axis")."sprintf"; # Forum #88460
1894         my $txt = sprintf($conf{$name} ? $conf{$name} : "%g", $i);


http://forum.fhem.de/index.php/topic,88460.0/

Just my 2 cents ;)

rudolfkoenig

Den Raetsel muesste der Log-Patch-Autor aufloesen, ich habe dabei nichts gedacht :)
Siehe den Ursprungs-Thread: https://forum.fhem.de/index.php/topic,53487

xenos1984

#5
Na gut, dann nehme ich das mal so hin,und schaue, ob man es nicht noch anders lösen kann, und auch kleine Zahlenwerte sinnvoll logarithmisch darstellen ;) Mein Ansatz würde darin bestehen, für 0 bzw. negative einen Ersatzwert zu liefern, der gerade unter dem Minimum der positiven Werte liegt.

Wie erwartet ist #3 am einfachsten zu lösen, indem man einfach diese beiden Zeilen

1736   $hmin{x1y1}=$hmin{x1y2}, $hmax{x1y1}=$hmax{x1y2} if(!defined($hmin{x1y1}));
1737   $hmin{x1y2}=$hmin{x1y1}, $hmax{x1y2}=$hmax{x1y1} if(!defined($hmin{x1y2}));


in der 98_SVG.pm durch das hier ersetzt:

if(!defined $hmin{x1y1})
{
$hmin{x1y1} = $hmin{x1y2};
$hmax{x1y1} = $hmax{x1y2};
$conf{yrange} = $conf{y2range} if defined $conf{y2range};
$conf{ytics} = $conf{y2tics} if defined $conf{y2tics};
$conf{yscale} = $conf{y2scale} if defined $conf{y2scale};
$conf{ysprintf} = $conf{y2sprintf} if defined $conf{y2sprintf};
}

if(!defined $hmin{x1y2})
{
$hmin{x1y2} = $hmin{x1y1};
$hmax{x1y2} = $hmax{x1y1};
$conf{y2range} = $conf{yrange} if defined $conf{yrange};
$conf{y2tics} = $conf{ytics} if defined $conf{ytics};
$conf{y2scale} = $conf{yscale} if defined $conf{yscale};
$conf{y2sprintf} = $conf{ysprintf} if defined $conf{ysprintf};
}


Damit werden nicht nur die Grenzen des Wertebereichs kopiert, wenn diese für eine Achse fehlen, sondern auch die Eigenschaften der Achse. Die Achsenbeschriftung (ylabel / y2label) wird schon vorher ausgewertet, deshalb würde es hier nichts bringen, sie auch zu kopieren.

(Um diese kleine Änderung vorzunehmen braucht es wahrscheinlich keinen Patch, vermute ich mal.)

Für die Achsenskalierung und sinnvolle Wahl von angezeigten Werten habe ich testweise einen kleinen Skript geschrieben, der die Werte ausgibt, wie ich sie mir in etwa vorgestellt hatte. Dabei habe ich mich an der 98_SVG.pm orientiert (dort werden bei linearer Skalierung max. 7 Achsenmarkierungen ausgegeben, bzw. das Intervall so gewählt) und bei logarithmischer Skalierung eine halbwegs gleichmäßige Einteilung gebastelt. Idee dahinter:


  • Die Achsenmarkierungen werden an Zehnerpotenzen ausgerichtet.
  • Falls der Wertebereich mehr als 7 Zehnerpotenzen umfasst, werden Schritte von n Zehnerpotenzen gemacht, wobei n die kleinste Zahl ist, die maximal 7 Schritte liefert.
  • Falls der Wertebereich höchstens 3 Zehnerpotenzen umfasst, werden Zwischenschritte eingeführt. Deren Anzahl hängt vom tatsächlichen Wertebereich ab.
  • Wenn die Grenzen des Wertebereichs so dicht beisammen liegen, dass sich logarithmische und lineare Skalierung nur wenig unterscheiden, werden die Achsenmarkierungen wie im linearen Fall gewählt.

Minimum und Maximum der Achse sind jeweils der erste und letzte Wert. Aufgerufen wird der Skript mittels

./scale.pl <min> <max>

Ein paar Beispiele zum Test:

$ ./scale.pl 120 9000
Logarithmic scale:
100
200
500
1000
2000
5000
10000
Linear scale:
0
2000
4000
6000
8000
10000


$ ./scale.pl -120 9000
Linear scale:
-2000
0
2000
4000
6000
8000
10000


$ ./scale.pl 27 28
Logarithmic scale:
27
27.2
27.4
27.6
27.8
28
Linear scale:
27
27.2
27.4
27.6
27.8
28


$ ./scale.pl 0.0027 28000
Logarithmic scale:
0.0001
0.01
1
100
10000
1e+06
Linear scale:
0
4000
8000
12000
16000
20000
24000
28000


Rudi, falls du mit dem Code so einverstanden wärst, würde ich versuchen, den in 98_SVG.pm einzubauen und einen Patch zu erstellen. (Die Variante erscheint mir einfacher, als den Code mit der derzeitigen Implementierung als Liste zu "verheiraten".) In der Doku könnte / sollte man natürlich trotzdem darauf hinweisen, dass es Sinn macht, den Wertebereich bei Plots brauchbar zu wählen, wenn man keine 1e+12 als Beschriftung haben will. Das kann ich bei der Gelegenheit auch gerne einbauen.

Apropos Doku: Die Option ysprintf / y2sprintf würde ich auch gerne dokumentieren. So weit ich sehen kann, liegt die gnuplot Syntax Dokumentation wohl in docs/commandref_frame.html (und deren _DE Version), da kann ich also auch einen Patch erstellen.

rudolfkoenig

ZitatWie erwartet ist #3 am einfachsten zu lösen, indem man einfach diese beiden Zeilen...in der 98_SVG.pm durch das hier ersetzt:
Diese Werte kann man im .gplot oder im Frontend auch direkt eingeben, auf der anderen Seite gibt es mit dem Patch keine Moeglichkeit mehr, die Werte zu entfernen.
#3 Ist fuer mich ein Schoenheitsfehler, den man einfach durchs Setzen des Hakens im Frontend "fixen" kann.

ZitatFür die Achsenskalierung und sinnvolle Wahl von angezeigten Werten habe ich testweise einen kleinen Skript geschrieben, der die Werte ausgibt, wie ich sie mir in etwa vorgestellt hatte.
Bei den logarithmischen Werten bin ich leidenschaftslos, da log-scale nicht von mir stammt, und ich bisher keine Gedanken darueber gemacht habe.
Bei den linearen Werten will ich an den bisherigen nichts aendern, aus vielen Gruenden.

Gib mir Zeit, ich versuche das Problem selbst zu loesen.
Es wuerde mir aber helfen, wenn Du mir Werte liefern wuerdest, die du gerne sinnvoll dargestellt haettest.

xenos1984

Zitat von: rudolfkoenig am 29 Juni 2020, 23:41:14
Diese Werte kann man im .gplot oder im Frontend auch direkt eingeben, auf der anderen Seite gibt es mit dem Patch keine Moeglichkeit mehr, die Werte zu entfernen.
Kann man die Werte nicht entfernen, indem man in der .gplot-Datei eine leeren String als Eintrag setzt?

Zitat
#3 Ist fuer mich ein Schoenheitsfehler, den man einfach durchs Setzen des Hakens im Frontend "fixen" kann.
Das stimmt natürlich. Mich hatte nur gestört, dass standardmäßig, wenn man keine zweite y-Achse definiert, trotzdem eine gezeichnet wird (was ja in Ordnung ist), dass diese zweite Achse aber "falsch", d.h. linear skaliert ist, und damit etwas irreführend. Insofern fände ich es wünschenswert, wenn die Achse schon automatisch und ohne Anfrage des Users erscheint, dass diese auch "richtig" skaliert wird.

Zitat
Bei den logarithmischen Werten bin ich leidenschaftslos, da log-scale nicht von mir stammt, und ich bisher keine Gedanken darueber gemacht habe.
Bei den linearen Werten will ich an den bisherigen nichts aendern, aus vielen Gruenden.
Klar, kein Thema - ist ja nur ein Vorschlag bzw. Angebot ;) So weit ich sehe, kann man ja auch mit plotReplace + Perl eigene yrange und ytics erstellen, wenn man das flexibler haben möchte (z.B. wenn man auf einem bildschirmfüllenden Plot mehr Skalenstriche haben möchte).

Zitat
Gib mir Zeit, ich versuche das Problem selbst zu loesen.
Es wuerde mir aber helfen, wenn Du mir Werte liefern wuerdest, die du gerne sinnvoll dargestellt haettest.
Ich habe mal eine Demo-Datei mit 6 Datenreihen (1 Device, 6 Readings) zur Veranschaulichung gebastelt. Meine Daten liegen in ähnlichen Bereichen, aber an den Demo-Daten kann man besser erkennen, wie die logarithmische Skalierung bei Werten < 1 verzerrt ist, bzw. wie die Daten nur den halben Plot nutzen oder ganz außerhalb liegen. Im Idealfall sollte jede der 6 Kurven, bei logarithmischer Skalierung und lines als Plot-Format, eine gerade Linie ergeben, die den gesamten y-Bereich ausfüllt.

Derzeit sind Reading 2 und 3 verzerrt, Reading 2 und 4 nutzen nur einen Teil des Wertebereichs und alle anderen werden gar nicht dargestellt.

define testlog FileLog data.log testdevice:reading.* readonly
define svgtest0 SVG testlog:testlog:CURRENT
attr svgtest0 plotReplace num="0"
attr svgtest0 room Plots
define svgtest1 SVG testlog:testlog:CURRENT
attr svgtest1 plotReplace num="1"
attr svgtest1 room Plots
define svgtest2 SVG testlog:testlog:CURRENT
attr svgtest2 plotReplace num="2"
attr svgtest2 room Plots
define svgtest3 SVG testlog:testlog:CURRENT
attr svgtest3 plotReplace num="3"
attr svgtest3 room Plots
define svgtest4 SVG testlog:testlog:CURRENT
attr svgtest4 plotReplace num="4"
attr svgtest4 room Plots
define svgtest5 SVG testlog:testlog:CURRENT
attr svgtest5 plotReplace num="5"
attr svgtest5 room Plots


set terminal png transparent size <SIZE> crop
set output '<OUT>.png'
set xdata time
set timefmt "%Y-%m-%d_%H:%M:%S"
set xlabel " "
set title 'Reading <num>'
set yscale log

#testlog 4:testdevice.reading%num%::

plot "<IN>" using 1:2 axes x1y1 title 'Reading' ls l0 lw 1 with lines

rudolfkoenig

ZitatKann man die Werte nicht entfernen, indem man in der .gplot-Datei eine leeren String als Eintrag setzt?
Theoretisch ja, man muss es aber vermutlich durchtesten, damit !defined und ! ueberall passend verwendet wird.

Zitatwenn man keine zweite y-Achse definiert, trotzdem eine gezeichnet wird
Konsequent waere es mAn "nomirror" als Voreinstellung zu haben, und ein "mirror" Flag einzufuehren, um was zu zeichnen. Aprops: es gibt bis zu 8 y Achsen, im PlotEditor werden aber nur 2 unterstuetzt.

rudolfkoenig

#9
Danke fuer die Daten.
Ich habe jetzt ziemlich viel Zeit investiert, nur um ein paar gerade Linien zu malen :), siehe Anhang.

Ich habe dabei die Log-Darstellung deutlich vereinfacht, hoffentlich habe ich keine wichtigen Features entfernt.
Als Nebeneffekt habe ich in svg.js den Offset des "Display plot value" Fensters mit oder ohne embed gefixt, bei plotReplace 0 als Wert, und in FileLog Zahlen mit Exponentialdarstellung erlaubt. Was noch unbefriedigend ist: bei log ist die Anzeige nicht abgeschnitten, ich weiss aber nicht, wie ich das in JavaScript machen soll.

Da ich viel umgebaut habe: bitte testen und Feedback geben.


Nachtrag: die letzte Linie sollte jetzt auch in der Ecke starten: min habe ich bei der Suche von 99999999 auf 9e+30 geaendert.

xenos1984

Zitat von: rudolfkoenig am 30 Juni 2020, 10:10:02
Konsequent waere es mAn "nomirror" als Voreinstellung zu haben, und ein "mirror" Flag einzufuehren, um was zu zeichnen. Aprops: es gibt bis zu 8 y Achsen, im PlotEditor werden aber nur 2 unterstuetzt.
Ah, das wusste ich gar nicht - sehr schön, das habe ich gleich mal in einem anderen Plot ausprobiert.

Zitat von: rudolfkoenig am 05 Juli 2020, 15:13:19
Ich habe jetzt ziemlich viel Zeit investiert, nur um ein paar gerade Linien zu malen :), siehe Anhang.
Super, vielen Dank!
Zitat
Da ich viel umgebaut habe: bitte testen und Feedback geben.
Das werde ich morgen gleich mal ausführlich mit Praxisdaten testen. Mit den Testdaten sieht es schon mal sehr gut aus, genau wie ich es mir vorgestellt hatte.

xenos1984

Mit den Testdaten klappt es bei mir jetzt auch einwandfrei, auch mit den realen Praxisdaten, genau wie ich es mir vorgestellt hatte. Danke nochmal! :)

noansi

#12
Hallo Rudolf,

zwei Bugs sind mir in 98_SVG.pm aufgefallen:

Zeile 1554
          my $xmul = $w/($xmax-$xmin) if($xmax-$xmin > 0 );

->
          my $xmul = ($xmax-$xmin > 0 ) ? $w/($xmax-$xmin) : 0;

Es würde auch reichen, $xmul so nur einmal in Zeile 1511 zu berechnen.
Ob 0 oder undef bei range <= 0 hängt davon ab, ob Du in den Fällen Warnings provozieren möchtest.

Für die beiden $tmul Berechnungen vermute ich, dass die Berechnung mit den Zeiten ebenso gedacht sein könnte statt nur auf != zu prüfen? Oder wolltest Du "vertauschte" $tosec/$fromsec zulassen?

Zeile 1947
        $ly = $x1; $ly = $y1;

->
        $lx = $x1; $ly = $y1;


Gruß, Ansgar.

rudolfkoenig

Danke, habe die Vorschlaege uebernommen, und xmul nur einmal berechnet.