Vorwort
In diesem Dokument sind die relevanten Konzepte für den Einsatz der MeisterCharts-Komponente dokumentiert.
1. Philosophie
1.1. HTML-First
HTML ist die erste Zielplattform. JavaFX nehmen wir mit - und dient als schnelle Entwicklungsplattform.
Daher werden Begriffe, Algorithmen und Drehrichtungen im Zweifel an HTML5 angelehnt. Wo die JavaFX-Philosophie abweicht, muss diese sich der HTML5-Idee unterordnen.
Wir übernehmen aber sehr gerne Konzept aus JavaFX, wo diese kein (oder ein unzureichendes) Gegenpart in HTML haben.
1.2. Bottom up / Basics first
Bei der Entwicklung wird immer zunächst das "Kleine" erstellt. Aus diesen Bausteinen können dann Schritt für Schritt höhere Layer zusammen gefügt werden.
1.3. Demos + Unit Tests
Alle Layer und Berechnungen werden durch Unit Tests bzw. Demos abgesichert.
1.3.1. Demos: Alles interaktiv
Um sicher zu stellen, dass später die einzelnen Properties auch tatsächlich verändert werden können, erlauben unsere Demos von Anfang an das Einstellen aller Properties - auch der Styles.
2. Begriffe
Hier sind die Begriffe definiert
2.1. Gestalt
Ein ChartGestalt fasst verschiedene Layer und andere Features zusammen und stellt diese unter einer einheitlichen Sicht zusammen.
2.2. Wizard
Ein Wizard bietet dem Benutzer gewissen Automatismen/Magie an, welche die Benutzung einer Gestalt vereinfacht.
Ein Wizard umfasst z.B. automatische Berechnungen bezüglich Orientation und davon abhängiger Margins.
2.3. Layer
Ein Layer befindet sich "auf" der Canvas und kann Malen sowie Events verarbeiten.
2.4. Configuration
Layer und Gestalt besitzen genau ein Configuration-Objekt (Marker @ConfigurationDsl).
Es hält die gesamte konfigurierbare Oberfläche: Daten, Verhalten und Darstellung.
Der Konstruktor nimmt optional ein additionalConfiguration-Lambda, das die Defaults überschreibt.
2.5. Style
Painter und Paintable besitzen ein Style-Objekt (Marker @StyleDsl, Lambda styleConfiguration).
Ein Style enthält ausschließlich visuelle Properties (Farben, Linienbreiten, Fonts, Abstände), in aller Regel als var.
Daten und Verhalten gehören nie in einen Painter/Paintable-Style, sondern in die Configuration des besitzenden Layers bzw.
der besitzenden Gestalt.
2.6. Paintable
Ein Paintable umfasst ein Feature, welches sich selbst zeichnen kann. Damit ist es möglich einzelne Aspekte wiederverwendbar zu gestalten.
| Nicht mit Painter verwechseln |
2.7. Painter
Painter sind nur ein Konzept - es gibt kein Painter-Interface. Ein Painter ist ein Weg um Painting-Code zu verallgemeinern.
| Nicht mit Paintables verwechseln |
3. Plattform-(Un)Abhängigkeit
Für jede unterstützte Plattform (aktuell JS und JavaFX) sind plattformabhängige Implementierungen notwendig. Diese spezifischen Implementierungen sollen über möglichst wenige Einstiegspunkte zu erreichen sein.
3.1. Initialisierung
Um die jeweiligen Factories und die Umgebung zu initialisieren, ist es notwendig diesen Code aufzurufen:
MeisterChartsPlatform.init()
Damit werden die jeweiligen plattformabhängigken Factories usw. registriert. Danach sollte es eigentlich möglich sein, nur noch mit allgemeinen Klassen zu arbeiten.
Ausnahme: Das Einfügen der erzeugten Komponente in den DOM bzw. Scene Graph.
3.2. MeisterChartsFactory
Die Klasse MeisterchartFactory dient zum erzeugen aller benötigten, plattformabhängigen Instanzen.
Klasse mit dem Suffix "JS" bzw "FX" müssen typischerweise nicht benutzt werden. Statt dessen sollte die Factory genutzt werden.
Der Zugriff auf die aktuelle Factory ist einfach über MeisterChartsFactoryAccess.factory möglich.
Einzige Ausnahme an dieser Stelle sind die Subklassen von MeisterchartBuilder.
Dort gibt es jeweils eine Implementierung für JS (MeisterchartBuilderJS) und JavaFX (MeisterchartBuilderFX).
Einzige Besonderheit: Diese liefern in der Build-Methode bereits den gecasteten Typ zurück.
Damit ist dann eine Einbettung in die Plattform ohne weiteres möglich.
|
4. Architektur
4.1. WebComponents (*Chart)
Die WebComponents sind die eigentliche Schnittstelle für den Anwender. Entsprechend erhalten diese auch die "besten" Namen.
Eine WebComponent definiert die API zum Anwender hin.
4.1.1. (Nullable) Properties
Alle Properties erhalten daher Namen und Typen, welche aus Benutzersicht einfach und leicht zu verwenden sind.
Alles Properties sind immer mit Default-Werten besetzt und optional. Der Anwender kann diese setzen, muss aber nicht.
4.1.2. Default Model
Jede WebComponent bringt ein einfaches (aber ansehliches) Default-model mit. Damit wird erreicht, dass eine WebComponent komplett ohne Konfiguration verwendet werden kann.
4.1.3. Nomenklatur: setData() und setStyle()
Jede WebComponent hat zwei Methoden. Über diese kann - getrennt voneinander - das Modell sowie alles die Darstellung betreffende gesetzt werden.
4.2. Gestalt
Ein ChartGestalt fasst verschiedene Layer und andere Features zusammen und stellt diese zusammen.
Die Gestalt dient dabei als Facade, welche die Benutzung einfacher macht.
Properties der Gestalt können sich auf unterschiedliche Layer auswirken. So kann z.B. das Setzen einer Margin an mehrere Layer (z.B. Achse, BarChart und Grid) weitergegeben werden.
Eine Gestalt wiederholt nur die Properties, welche Auswirkungen auf mehrere Layer hat.
Es werden explizit nicht alle Properties aller Layers (bzw. deren Configuration) wiederholt.
Im Gegensatz dazu wiederholen die WebComponents zumindest etliche dieser Properties.
|
4.2.1. Configuration
Eine Gestalt hält genau ein Configuration-Objekt.
Darin liegen sowohl die Modellwerte (in aller Regel vom Typ Observable*) als auch die Properties, die sich auf mehrere Layer auswirken (z.B.
eine gemeinsame Margin).
Der Benutzer muss markAsDirty() aufrufen, wenn Werte geändert wurden.
Die Gestalt tut dies nicht von sich aus.
4.2.2. Änderbarkeit
Da die Gestalt im Zweifel auch als WebComponent veröffentlicht werden soll, muss darauf geachtet werden, dass alle relevanten Dinge nachträglich gesetzt werden können.
Hintergrund: Aus JavaScript heraus ist das Setzen der Style/Data-Properties immer erst nach Instantiierung möglich.
4.2.3. Keine Magie
Eine Gestalt enthält nur die Funktionalität um die Layer auf technischer Ebene zu verknüpfen.
Eine Gestalt enthält explizit keine "Magie", welche die Benutzung vereinfacht. Diese Vereinfachungen sind in Wizards ausgelagert.
4.3. Wizard
In vielen Fällen möchte man die Benutzung einer Gestalt vereinfachen. So ergibt sich z.B. aus der Position der Achsen eventuelle Margins.
Diese Automatismen funktionieren nie in allen Fällen. Deshalb sind diese immer optional.
Wizards erlauben eine bequeme, automagische Konfiguration einer Gestalt. Diese Wizards nutzen dafür eine bestehende Gestalt und konfigurieren diese.
Da die Wizards immer optional sind, können diese beliebige Annahmen machen und auf einzelne Use Cases optimiert sein.
4.4. Layer
Ein Layer befindet sich "auf" der Canvas und kann:
-
Malen
-
Events verarbeiten
Ein Layer ist eine technische Sicht auf ein Feature, welches sich auf das komplette Canvas bezieht. Typischerweise werden Layer nicht vom End-User direkt benutzt. Stattdessen wird der User eine Gestalt verwenden.
Typischerweise hat ein Layer ein Configuration-Objekt, über das seine Properties konfiguriert werden.
4.4.1. Unabhängige Properties für den einzelnen Layer
Properties gelten nur für den einzelnen Layer. Daher kommt es durchaus vor, dass sich Properties in verschiedenen Layern sehr ähneln. Diese müssen (von der Gestalt) trotzdem immer einzeln gesetzt werden. Die Layer müssen möglichst unabhängig voneinander sein.
| Über Extension-Methods können diese durchaus (berechnete) Werte eines anderen Layers benutzen. Sie sollten aber nur in Ausnahmefällen Konfigurationswerte eines anderen Layers automatisch übernehmen. |
Damit wird sicher gestellt, dass die Layer einfach zu testen und zu verwenden bleiben.
4.4.2. Configuration
Jeder Layer hält genau ein Configuration-Objekt, das im Konstruktor übergeben und optional per additionalConfiguration-Lambda angepasst wird.
Es vereint die konfigurierbare Oberfläche des Layers: Daten und Darstellung.
Daten werden typischerweise als Lambdas/Provider (val) gehalten — der Layer hält selbst keine Modellwerte.
Rein visuelle Werte dürfen dagegen direkt als var liegen.
4.5. Painter
Painter sind nur ein Konzept - es gibt kein Painter-Interface.
Ein Painter dient dazu die Darstellung bestimmer Funktionalitäten wiederverwendbar zu gestalten. Jeder Painter hat dabei eine unterschiedliche Signatur, da unterschiedliche Werte übergeben werden müssen.
Painter sind daher stark an ihren Einsatz-Zweck gebunden. So verhält sich ein Line-Painter eben völlig unterschiedlich als z.B. ein Kompass-Painter.
Ein Painter kann ein Style-Objekt besitzen (siehe oben) — dieses enthält ausschließlich visuelle Properties.
4.6. Paintable
Ein Paintable umfasst ein Feature, welches sich selbst zeichnen kann. Damit ist es möglich einzelne Aspekte wiederverwendbar zu gestalten.
Die Paintable implementieren dabei ein gemeinsames Interface, womit diese in generischem Code verwendet werden können.
5. Berechnungen
Es gibt verschiedene Arten von Werten in MeisterCharts. Diese müssen an verschiedenen Stellen umgerechnet werden.
5.1. Begriffe
5.1.1. Domain
Der Domain-Wert stellt einen absoluten, fachlichen (i.d.R. gemessenen oder beobachteten) Wert dar. Dies kann z.B. ein von einem Sensor gelieferter Messwert sein.
5.1.2. Value Range
Die Value-Range beschreibt den Wertebereich, welcher auf einer Achse (in der Content-Area) dargestellt werden soll. Die Value-Range entspricht z.B. dem Wertebereich des Sensors.
| Es ist möglich über die eigentlichen Grenzen der Content-Area hinaus zu zeichnen. D.h. auch Werte außerhalb der eigentlichen Value-Range — und damit Content-Area — zu zeichnen. Dies ist insbesondere dann hilfreich, wenn z.B. die Grenzen gar nicht einfach festgelegt werden können - z.B. bei einer Zeit-Achse. |
5.1.3. Domain Relative
Dies entspricht dem Prozentwert innerhalb der Value Range.
Bildlich gesprochen die relative Position an der Achse.
5.1.4. Axis Orientation
Die Achsen-Ausrichtung beschreibt die Richtung in der Werte an einer Achse aufgetragen werden. Typischerweise werden an der Y-Achse die Werte aufsteigend von unten nach oben dargestellt.
Unter bestimmten Umständen (z.B. Sensor ist an der Decke montiert) ist aber auch die umgekehrte Richtung denkbar.
5.1.5. Content Area Relative
Beschreibt Werte, welche relativ zur technischen Ausrichtung der Achse dargestellt werden. Diese entsprechen ohne Zoom und Translation direkt den auf dem Bildschirm dargestellten Werten.
5.1.6. Zoom
Der Zoom - welcher als Faktor definiert ist. Ein Zoom-Faktor von 1.0/1.0 bedeutet dabei, dass Content Area Relative-Werte 1:1 auf dem Bildschirm dargestellt werden.
Die Zoom-Faktoren können für X und Y unterschiedlich sein.
5.1.7. Zoomed
"Zoomed" Werte berücksichtigen bereits dem Zoom-Faktor. Diese werden also in genau dieser Größe dann auf dem Bildschirm dargestellt. Distanzen entsprechen bereits den exakten Pixel-Abständen auf dem Monitor.
5.1.8. Translation
Beschreibt die Verschiebung (Panning) der Darstellung auf dem Bildschirm.
Anschaulich
Eine Translation von 50/100 Pixeln kann wie folgt interpretiert werden: * der Inhalt nach rechts unten verschoben ist * das Fenster (der sichtbare Ausschnitt) nach links oben verschoben wurde (als -x/-y der Translation)
5.1.9. Window (nur Koordinaten)
Window-Koordinaten entsprechen den exakten Koordinaten innerhalb der Canvas.
5.2. Umrechnungen
5.2.1. Domain → Domain Relative
5.2.2. Domain Relative → Content Area Relative
5.2.3. Content Area Relative → Zoomed
5.2.4. Zoomed → Window
6. Content Area
6.1. Veranschaulichung der Content Area Size
Wenn der Zoom Faktor 1.0/1.0 ist, dann wird der Domain-Relative Bereich von 0..1 genau in dieser Größe gerendert. Das ist unabhängig von der Window-Size. D.h. egal wie groß das Window ist, das Chart wird mit entsprechenden Dimensionen gerendert.
6.1.1. Beispiel-Rechnungen
Content Area Size: 1024px x 768px
Y-Achse
Domain Relative 0.75 → 768px * 0.75 = 576px im Window
X-Achse
Domain Relative 0.66 → 1024 * 0.75 = 675,84px im Window
6.1.2. In einem Satz I
Die Content-Area-Size definiert, wie groß der DomainRelative-Bereich bei einem Zoom-Faktor von 1.0 gerendert wird.
6.1.3. In einem Satz II
Die Content-Area-Size definiert, wieviele Pixel 100% des DomainRelative-Bereichs bei einem Zoom-Faktor von 1.0 umfassen.
6.2. Zusammenhang mit WindowSize
Grundsätzlich ist die Window-Size von der ContentAreaSize unabhängig. Diese Größen können aneinander gebunden werden, müssen aber nicht.
6.3. Strategien um die ContentArea-Size zu setzen
Die Größe der ContentArea kann aufgrund unterschiedlicher Werte berechnet werden. Dabei können X und Y-Achse unterschiedlichen Strategien folgen.
6.3.1. Strategie 1: Anhand der Window-Size
Die Größe des Windows ergibt die Content-Area-Size.Dies hat den Effekt, dass beim Zoom-Faktor von 1.0 der gesamte Werte-Bereich im Window sichtbar ist.
Diese Strategie benutzen, wenn…
... es wichtiger ist den gesamten (@DomainRelative) Bereich zu sehen, als einzelne Details zu erkennen.
6.3.2. Strategie 2: "Sinnvolle" Werte (fachlich motiviert)
Der fachliche Hintergrund kann die Größe der Content-Area vorgeben.
Diese Strategie benutzen, wenn…
... es wichtiger ist die einzelnen Datenpunkte / einzelne Details zu erkennen, als den gesamten Bereich zu sehen.
Beispiel: Anzahl/Abstand Datenpunkte
-
Ein Sensor liefert alle 100ms einen Datenpunkt (10 Punkte/s).
-
In der Darstellung soll ein Datenpunkt 5 Pixel umfassen (um "bequem" gerendert werden zu können)
-
Die ValueRange der Zeit umfasst eine Minute
Daraus ergibt sich: * 10 Punkte/s * 60s = 600 Punkte * \* 5 Pixel/Punkt = 3000 Pixel
Anschaulich
Bei einem Zoom-Faktor von 1.0 zeigt das Diagramm einen Datenpunkt alle 5 Pixel an.Unabhängig von der Fenstergröße so viele eben Platz haben.
Damit alle Datenpunkte für 1 Minute bei einem Zoom-Faktor von 1.0 sichtbar sind, müsste das Window mindestens 3000 Pixel groß sein.
Misch-Strategien (eventuell hilfreich)
Es wäre denkbar, dass eine gewisse Mindestgröße durch die fachlichen Anforderungen vorgegeben wird. Darüber hinaus könnte die Content-Area sich an das Window anpassen.
Dies kann sinnvoll sein, um bei extrem kleine Fenster bestimmte Dinge zu erkennen.
6.3.3. Diagramm-Typen / Strategien
Folgende Strategien sind notwendig, um die ContentArea-Size zu berechnen/zu aktualisieren
6.3.4. X + Y-Achse gebunden
Für: "Chart ohne Zoom + Pan"
-
Zoom ist auf 1.0/1.0 fixiert
-
Window Translation ist auf 0.0/0.0 fixiert
Bei diesen Diagrammen möchte man immer alle Werte sehen.
Deshalb muss bei einem Resize des Windows der Inhalt entsprechend angepasst werden.Dies wird erreicht, indem die ContentAreaSize direkt an die WindowSize gebunden wird.
6.3.5. Y-Achse gebunden, X-Achse fachlich vorgegeben
Für: "Zeit-Chart"
-
Zoom + Pan werden unterstützt
Wir möchten im Diagramm die einzelnen Datenpunkte auf der Zeitachse gut erkennen können.Daher ist die Breite der Content-Area vorgegeben (z.B. 5 Pixel pro Datenpunkt).
Die Y-Achse soll sich dagegen der Window-Size anpassen, damit jeweils der gesamte Wertebereich sichbar wird.
6.3.6. Y-Achse fachlich vorgegeben, X-Achse gebunden
Siehe oben. Zeit-Achse befindet sich an Y.
6.3.7. Y- und X-Achse fachlich vorgegeben
Für: "Graph-Darstellung"
Für: "Koordinaten-basierter Chart" - z.B. Maps
Wir möchten einen Graph darstellen, dabei sollen die einzelnen Knoten immer im selben Abstand/Relation zueinander dargestellt werden.
Daher haben Änderungen der Window-Size keine Auswirkungen auf die Content-Area-Size.
6.4. Beispiel-Code
//Content area has the same size as the window
meisterChartBuilder.contentAreaSizingStrategy = BindContentAreaSize2ContentViewport() (1)
//Content area has a fixed width
//Height is bound to the window height
meisterChartBuilder.contentAreaSizingStrategy = FixedContentAreaWidth(1000.0) (2)
//Content area has a fixed size
meisterChartBuilder.contentAreaSizingStrategy = FixedContentAreaSize(Size(1024.0, 768.0)) (3)
| 1 | Content Area ist immer gleich groß wie das Window. Diese Strategie ist sinnvoll für Charts, bei denen nicht gescrollt wird. |
| 2 | Die Breite ist fix, die Höhe der Content Area wird an das Window angepasst. Diese Strategie ist für Charts mit einer (endlosen) Zeit-Achse sinnvoll. |
| 3 | Die Content Area hat eine fixe Größe. Diese Strategie ist für Charts im Stile von Maps sinnvoll, die über beide Achsen gescrollt werden. |
6.5. FAQ
6.5.1. "Welche Länge in Pixeln hat meine Linie von z.B @DomainRelative 0.45?"
Die Länge der Linie ist von der Content Area Size + Zoom Faktor abhängig. Sie ist nicht von der Window-Size abhängig.
Formel:
Content-Area-Size * Domain-Relative-Wert * Zoom-Faktor
Aber: Manchmal ist die ContentAreaSize an die Window-Size gebunden.
6.5.2. "Was passiert, wenn ich die Window Size verkleinere/vergrößere?"
An der Darstellung des Diagrams selbst nichts!
Allein der sichtbare Ausschnitt wird verkleinert/vergrößert!
Aber manchmal doch:
In manchen/vielen Szenarien ist es sinnvoll, die ContentAreaSize (bzw. einzelne Achsen davon) an die Window-Size zu binden.
D.h. durch die Änderung der Window-Size ändert sich die ContentAreaSize.Und dadurch wird dann die Darstellung des Diagramms doch skaliert.
6.5.3. "Wie kann eine Linie (mit fixer @DomainRelative Länge) länger gerendert werden?"
Durch die Änderung von:
-
Zoom Faktor
-
ContentAreaSize
6.5.4. "Wie groß sollte die fachlich definierte ContentAreaSize sein?"
"Ich habe einen Datenpunkt all 100 ms. Diese sollen mit einem Abstand von 5 Pixeln dargestellt werden"
Einen Entfernung von 5 Pixeln für 100ms kann durch eine beliebige Kombination von ContentAreaSize + DomainRelative Bereich erreicht werden.
Aus Gründen der Nachvollziehbarkeit ist es hilfreich, wenn sich die ContentAreaSize im Bereich von "üblichen/verständlichen" Bildschirmauflösungen bewegt.Technisch notwendig ist das aber nicht.
Mögliche ContentArea-Breiten für verschiedene Zeiträume bei 5 px pro 100 ms
Beispiel-Rechnungen, um zu zeigen, wie @DomainRelative und ContentAreaSize zusammen spielen.
| Zeitraum (@DomainRelative 0..1) | Zeitraum (@DomainRelative 0..1) | Anzahl Datenpunkte | Breite @ContentArea |
|---|---|---|---|
1 s |
1 s |
10 |
50 px |
5 s |
5 s |
50 |
250 px |
10 s |
10 s |
100 |
500 px |
1 min |
60 s |
600 |
3000 px |
1 h |
3,600 s |
36,000 |
180,000 px |
Soll also der @DomainRelative-Bereich z.B. eine Minute umfassen, so muss die ContentArea-Breite auf 3000 Pixel gesetzt werden.
7. Content Area: Szenarien
7.1. Bar Chart (vertikale Bars)
Bei einem Bar-Chart möchten wir die gesamte Window-Höhe für den Wertebereich der Bars verwenden. Wir möchten alle Bars nebeneinander sehen.
7.1.1. Size:
Content-Area-Size wird an den Content Viewport (== WindowsSize - contentViewportMargin) gebunden.
7.1.2. Zoom
Bei einem Zoom von 1/1 nutzen die Bars in ihrem Wertebereich die gesamte Content-Viewport-Höhe
Es sind alle Bars nebeneinander sichtbar. Die Breite der Bars muss entsprechend berechnet werden.
7.1.3. Höhenänderung Window
Wenn sich die Höhe des Windows ändert, ändert sich die Höhe der Content Area. Die Bars werden proportional zur Höhe gezeichnet und passen sich daher an.
7.1.4. Breitenänderung Window
Wenn sich die Breite ändert, wird die ContentArea angepasst. Die Breite der Bars wird darauf angepasst berechnet.
7.2. Timeline Chart (Zeit auf der X-Achse)
Bei einem Timeline-Chart möchten wir die gesamte Content-Viewport-Höhe für den Wertebereich verwenden. Der Abstand der Punkte auf der X-Achse soll konstant sein. Je nach Zoom-Faktor könnten z.B. 5 Pixel einer Sekunde entsprechen.
7.2.1. Size:
-
Die Höhe der Content Area wird an den Content Viewport (== WindowsSize - contentViewportMargin) gebunden.
-
Die Breite der Content Area hat einen fixen Wert (z.B. 1000px)
7.2.2. Zoom
Bei einem Zoom von 1/1 nutzen die Kurven die gesamte Content-Viewport-Höhe für ihren Wertebereich. In der Breite haben wir einen fixen Faktor zwischen Zeit und Pixeln. Dadurch können wir alle Punkte entsprechend dem Zoomfaktor erkennen.
7.2.3. Höhenänderung Window
Wenn sich die Höhe des Windows ändert, ändert sich die Höhe der Content Area. Die Kurven werden proportional zur Höhe gezeichnet und passen sich daher an.
7.2.4. Breitenänderung Window
Die Content-Area bleibt konstant. Durch die Veränderung der Breite des Windows wird nur mehr bzw. weniger Inhalt sichtbar. Der Abstand der Punkte auf der X-Achse bleibt konstant.
7.3. Floorplan bzw. Karte
Dabei werden Bilder der Karte bzw. des Floorplans gerendert. Entsprechend muss die Aspect Ratio berücksichtigt werden!
TODO: ==== Idee 1: Size an Content-Viewport binden: Aspect Ratio berücksichtigen
Die Höhe der Content Area wird an den Content Viewport (== WindowsSize - contentViewportMargin) gebunden. Dabei wird aber die Aspect Ratio berücksichtigt. Die Content Area wird also in den Content-Viewport eingepasst, wobei die Aspect Ratio erhalten bleibt.
Vorteil: Zoom-Faktor von 1 führt dazu, dass der gesamte Inhalt im Window sichtbar ist.
7.3.1. Idee 2: Größe an Pixel-Anzahl des Bildes binden
Das Bild hat eine Anzahl an Pixeln. Diese dient als Basis für die Größe der Content Area. Die Content Area hat damit eine fixe Größe.
Vorteil: Eine Umrechnung von Domain auf Content-Area ist immer gleich.
Nachteil: der (Default)-Zoom-Faktor hängt von der Größe des Windows an. Diese muss bei der Berechnung berücksichtigt werden.
8. Größen / Resizing / (Default) Zoom / Translation
In diesem Kapitel wird erklärt, wie die einzelnen Größen zusammen hängen, wie das Resizing funktioniert und wie der Default-Zoom und die Default-Translation bestimmt werden.
8.1. Relevante Szenarien
Es gibt drei relevante Szenarien, die teilweise mit unterschiedlichen Strategien abgehandelt werden.
Diese Szenarien sind technisch sehr unterschiedlich, für den Benutzer erschließt jedoch intuitiv ein sehr enger Zusammenhang.
Deshalb müssen diese Strategien eng aufeinander abgestimmt sein, um eine konsistente User-Experience zu ermöglichen.
8.1.1. Initiale Darstellung
Wenn ein MeisterCharts geöffnet wird, erwartet der User, dass initial die Daten sinnvoll dargestellt werden. Die gleiche Ansicht soll auch durch die "Rücksetz"-Funktion, z.B. über die Toolbar, erreicht werden.
Diese Darstellung wird durch die Klasse com.cedarsoft.charting.algorithms.impl.ZoomAndTranslationDefaults definiert.
Oftmals ist bei der initialen Darstellung das Datenmodell des Charts noch leer.
Dieser Zustand muss in den entsprechenden Klassen unterstützt werden.
Bei zeitbasierten Charts bietet sich die Verwendung von nowMillis() zur Bestimmung eines sinnvollen Zeitpunktes bei leerem Datenmodell an.
|
8.1.2. Rücksetzen auf Standard-Werte ("Home"-Button)
Sobald Zooming und/oder Panning verfügbar sind, ist es notwendig dem User eine Rest-Möglichkeit zu bieten. Dies kann z.B. durch einen Home-Button auf der Toolbar erfolgen.
Nach dem Rücksetzen entspricht die Darstellung in der Regel dem der initialen Darstellung unter Berücksichtigung der nun vergangenen Zeit bzw. des veränderten (gefüllten) Datenmodells.
Das Rücksetzen erfolgt über den ZoomAndTranslationSupport.
8.1.3. Resize des Window
Wenn die Fenstergröße durch eine User-Interaktion verändert wird, sind je nach Anwendungsfall unterschiedliche Verhaltensweisen gewünscht.
Dabei sind zwei Dinge relevant, die zusammen spielen:
Anpassung der ContentArea-Size
Die Implementierung com.cedarsoft.charting.canvas.ContentAreaSizingStrategy entscheided, ob und wie die ContentArea bei einem Resize angepasst wird.
Dabei wird der Zoom und die Window-Translation noch nicht angepasst.
Resize-Behavior
Bei jedem Resize des Window oder der ContentArea wird immer auch das
WindowResizeBehavior aufgerufen.
Diese Klasse sorgt für die Anpassung des Zoom-Faktors und der Window-Translation.
8.2. Implementierungen
Es gibt für die einzelnen Interfaces verschiedene Default-Implementierungen.
8.2.1. WindowSizeBindingStrategy
Reagiert auf (Plattform-)Änderungen der Größe der Komponente.
Passt die WindowSize im ChartState an.
ImmediateWindowSizeBindingStrategy
Updated die Größe sofort
DelayedWindowSizeBindingStrategy:
Updated die Größe mit einem gewissen Delay.
Dieses Verhalten ist hilfreich, wenn das Neuberechnen relativ teuer ist. Insbesondere im Kontext von Tiling, bei denen die Werte-Achse an der Window-Size hängt.
8.2.2. ContentAreaSizingStrategy
Diese Strategie aktualisiert die Größe der ContentArea. In der Regel erfolgt die Neuberechnung bei jeder Änderung der WindowSize.
BindContentArea2WindowSize:
Die ContentAreaSize hängt direkt an der WindowSize - eventuelle Margins werden berücksichtigt. Diese Strategie wird bei "normalen"/einfachen Charts wie z.B. BarChart verwendet.
FixedContentArea(Height/Width):
Die Höhe und/oder Breite der ContentArea hat einen fixen Wert.
Verwendung z.B. bei zeitbasierte Charts oder Maps.
8.3. Beispiele
8.3.1. Bar Chart
Ein einfacher Bar Chart unterstützt kein Zooming oder Panning.
Daraus folgt, dass die Content-Area jeweils an die Window-Size gebunden wird (BindContentAreaSize2ContentViewport).
Da Platz für Legenden, Achsen und Titel benötigt wird, bietet sich die Verwendung der Implementierung
com.cedarsoft.charting.algorithms.impl.FittingWithMargin für die initiale Ansicht und das Rücksetzen an.
Als ResizeBehavior wird ResetToDefaultsOnWindowResize verwendet.
8.4. Beispiel-Code
8.4.1. Bar Chart
example: Ein Bar Chart, bei welchem die Content-Area (welche die Bars enthält) immer vollständig angezeigt werden soll. An den Seiten steht noch Platz für Achsen, Legende und Titel zur Verfügung.
//Content area has the same size as the window
meisterChartBuilder.contentAreaSizingStrategy = BindContentAreaSize2ContentViewport() (1)
//Reset to default on resize - zooming and panning not supported
meisterChartBuilder.configure {
chartSupport.windowResizeBehavior = ResetToDefaultsOnWindowResize (2)
}
//On reset use insets
meisterChartBuilder.zoomAndTranslationDefaults {
FittingWithMargin(Insets(10.0, 70.0, 25.0, 25.0)) (3)
}
| 1 | Content Area ist immer gleich groß wie das Window. |
| 2 | Beim Resize werden Zooming und Panning jeweils zurück gesetzt. Dadurch wird immer auf die Defaults (siehe 3) zurück gestellt |
| 3 | Die ContentArea wird mit Rändern fixer Größe (10 Pixel oben, 70 Pixel rechts usw.) dargestellt. Damit bleibt Platz für Achsen und Legende. |
8.5. Abläufe / Events / Zusammenfassung
In diesem Diagram sind alle Events und Abläufe zusammen gefasst, die im Kontext von ContentArea-/Window-Size relevant sind.
8.5.1. BarChart: Content Area Binding / Resize Behavior / Reset to Defaults
In diesem Beispiel werden folgende Implementierungen verwendet:
-
windowSizeBindingStrategy: ImmediateWindowSizeBindingStrategy
-
contentAreaSizingStrategy: BindContentArea2WindowSize
-
resizeBehavior: KeepCenterOnResize
-
zoomAndTranslationDefaults: ZoomAndTranslationDefaults.noTranslation
Dies entspricht den Standard-Implementierungen, welche ohne weitere Konfiguration gewählt werden.
Dieses Konfiguration wird bei einfachen Charts, wie z.B. dem BarChart verwendet.
8.5.2. TimelineChart: Content Area Binding / Resize Behavior / Reset to Defaults
In diesem Beispiel werden folgende Implementierungen verwendet:
-
windowSizeBindingStrategy: DelayedWindowSizeBindingStrategy
-
contentAreaSizingStrategy: FixedContentAreaWidth
-
resizeBehavior: KeepLocation(0.8): Position des CrossWires - z.B. bei 80%
-
zoomAndTranslationDefaults: MoveDomainValueToLocation
Diese Konfiguration wird z.B. bei einem Timeline-Charts mit horizontaler Zeit-Achse verwendet.
9. I18n
9.1. Pro Component
Generell wird die Übersetzung pro Component konfiguriert. Es gibt keine globalen/statischen Objects.
9.2. Formattierung vs. Übersetzung
Es können zwei Locales gesetzt werden. Eine wird für die Übersetzung der Texte genutzt, die andere wird für die Formattierung der Zahlen und Daten eingesetzt.
Beide Locales können unabhängig voneinander konfiguriert werden.
9.3. Text Service
Der TextService enthält eine Anzahl an TextResolver die der Reihe nach abgefragt werden.
Das erste Resultat wird zurück geliefert.
Folgende Reihenfolge ist per default eingestellt:
-
Text ohne Locale
-
Text mit gesetzter Locale
-
Text mit Fallback-Locale
-
Default Text
TextResolver9.3.1. Optional: Text-Key Output
Für Entwicklungszwecke kann der TextKeyTextResolver registriert werden, welcher den Text-Key selbst zurück liefert.
10. Fonts
Es gibt zwei grundsätzliche Font-Klassen:
10.1. FontDescriptor
Ein FontDescriptor beschreibt einen Font vollständig.
Alle Eigenschaft (z.B. FontFamily und FontSize) sind gesetzt.
Auf jedem CanvasRenderingContext ist zu jedem Zeitpunkt ein FontDescriptor gesetzt - dafür sorgt die Methode #applyDefaults()
10.2. FontDescriptorFragment
Ein FontDescriptorFragment besitzt die selben Eigenschaften wie in Fontdescriptor.
Allerdings sind diese Nullable.
Durch das Setzen eines
FontDescriptorFragment s kann man einzelne Aspekte eines Fonts setzen.
So kann z.b. nur die FontSize oder das FontWeight überschrieben werden.
11. CanvasRenderingContext
Der CanvasRenderingContext ist das "Herz" auf dem gepainted wird. Es gibt jeweils eine Implementierung für HTML und JavaFX.
11.1. Texte + Anchor
Ein typisches Szenario ist das Painten eines Textes (oder Bildes) an eine bestimmte Stelle. Je nachdem an welcher Seite des Bildschirms gerendert wird, muss von rechts/links/oben/unten aus gerechnet werden.
Um nicht an allen Stellen diese Berechnungen wiederholen zu müssen, bringt der CanvasRenderingContext Methoden mit, die das übernehmen.
Um den Code übersichtlich und bugfrei zu halten, sollten diese Methoden auch verwendet werden.
11.1.1. Anchoring = Anchor-Location + Richtung + Gap
Um einen Text zu zeichnen benötigt man einen Ort (Anchor) und eine Richtung (Direction) (vertikal + horizontal) sowie einen Abstand (gap) in welche der Text von diesem Punkt aus gezeichnet wird.
Ein Anchoring beinhaltet diese Aspekte.
11.1.2. Shape + Direction -→ Anchor
Man kann aus einer Shape (aktuell nur Rectangle) und einer Richtung eine Koordinate berechnen (z.B. rechte, obere Ecke). Diese Koordinate kann dann wiederum als Anchor dienen.
11.1.3. Anchor-Gap
Die Anchor-Gap besteht aus zwei Werte: - anchorGapHorizontal - anchorGapVertical
Diese werden - je nach Direction - unterschiedlich implementiert. Sie geben als positiver Wert jeweils den Abstand zum Anchor-Point an. D.h. abhängig von der Direction kann ein positiver Anchor-Gap-Wert nach rechts oder links auf der Achse abgetragen werden.
11.2. Lines
Linien werden in JavaFX immer zentriert auf der angegebenen Position gezeichnet. D.h. die Breite einer Linie wird jeweils zur Hälfte auf beiden Seiten addiert.
Dies ist insbesondere zu berücksichtigen, wenn:
-
Formen mit Rand gezeichnet werden (Fill + Stroke).
-
Linien pixelgenau auf dem Bildschirm dargestellt werden sollen (Anti-Aliasing)
11.3. Rectangles
11.3.1. Malen mit Ankern
Es gibt Extension-Methods, welche das Zeichnen von Rectangles mit Anchor-Direction und Gap erlaubt. Diese Methoden erleichtern das Zeichnen, wenn ein Rect mit einer Ecke oder dem Zentrum ausgerichtet werden soll.
Die Methoden stehen sowohl für stroke() als auch fill() zur Verfügung.
11.3.2. Bounds: fill vs. stroke
Beim Füllen eines Rectangles wird exact die angegebene Form gefüllt. Konkret: Eine Rechteck mit der Größe von 100x50 Pixeln hat auf dem Bildschirm auch exakt diese Größe.
Stroke zeichnet eine Linie um das angegebene Rechteck. Dabei liegt die Hälfte der Linie innerhalb der angegebenen Bounds. Die andere Hälfte der Linie befindet sich dagegen außerhalb der Bounds.
Konkret: Ein Rechteck mit der Größe von 100x50 Pixeln hat auf dem Bildschirm eine Größe von 100+(LineWidth)x50+(LineWidth) Pixeln. Dabei ist jeweils die halbe LineWidth links und rechts bzw. oben und unten dazu gekommen.
Zuerst fill, dann stroke
Bei einem Rect mit Rand muss daher zuerst fill() und dann stroke() aufgerufen werden, da ja stroke() die halbe Linie über den gefüllten Inhalt nach innen zeichnet.
Stroke im Hintergrund
Wenn z.B. Swimlanes oder ähnliches gezeichet werden (im Hintergrund), muss die Linie entsprechend eingepasst werden. Dafür gibt es entsprechende Extension-Methods.
Dadurch wird vermieden, dass der sichtbare Bereich über die definierte Größe hinausragt.
11.4. Baseline
Generell ist es eine gute Idee, Texte jeweils von der Baseline aus zu zeichnen. Damit gibt es dann auch reproduzierbare und gut aussehende Resultate.
11.5. Farben
Es gibt zwei Sichtweisen auf Farben: Zum einen die Verwendung aus dem Painter heraus um das Canvas zu konfigurieren (CanvasPaint).
Zum zweiten bei der Konfiguration in einem Style (FlatColor bzw. ColorDescriptor).
Diese Unterscheidung ist notwendig, da die Gradienten absolute Koordinaten zum painten auf dem Canvas benötigen, diese im Style aber natürlich nicht vorgegeben werden können/sollen.
11.5.1. CanvasPaint - für Canvas/Layers
CanvasPaint ist die Oberklasse für alle "farblichen" Klassen, welche direct auf einem Canvas gemalt werden können.
Dies sind Color sowie die Gradienten-Klassen
Color
Entspricht einer durchgehenden Farbe (im Gegensatz zu Gradienten). Diese kann direkt auf dem Canvas gesetzt werden.
Canvas*Gradient
Die Gradienten-Implementierungen enthalten die absoluten Koordinaten welche vom Canvas benötigt werden.
| Der Gradient wird dabei immer vom aktuellen Ursprung der GC aus gezeichnet. Unabhängig von den Koordinaten der Zeichnung. Entsprechend muss entweder der Gradient angepasst oder der Ursprung verschoben werden. |
11.5.2. ColorDescriptor - für Styles
In den Styles für die Layer soll es bequem möglich sein die Gradienten zu definieren.
Deshalb werden dort die Subklassen von
ColorDescriptor verwendet.
Diese enthalten keine absoluten Koordinaten.
Es ist daher dem Layer überlassen die entsprechenden Konvertierungen durchzuführen.
Color - für die meisten Fälle
Da in den meisten Fällen ein Gradient nicht notwendig ist, wird typischerweise direkt eine Color im Style definiert. Eine Color kann ohne Conversion direkt an den GC übergeben werden.
11.6. Winkel
11.6.1. Drehrichtung: Uhrzeigersinn
Genau wie bei HTML drehen positive Winkel im Uhrzeigersinn.
11.6.2. 0(rad) entsprechen 3 Uhr
Die Winkel beginnen bei 3 Uhr.
12. Best Practices Layer
12.1. Layer umfasst den gesamten Bereich
Ein Layer malt grundsätzlich auf den gesamten Canvas.
Falls eine Platzierung notwendig ist, gibt es zwei Ansätze:
-
Konfiguration über einen Wert (z.B. Crosswire)
-
Layout außerhalb (z.B. ValueAxis)
12.1.1. Margin
Um Abstände vom Rand zu ermöglichen, besitzt ein Layer typischerweise die Eigenschaft "margin". In diese kann ein Inset konfiguriert werden (auch wenn nur ein Wert für eine Seite benötigt wird).
12.2. Translate mit String-Parameter
Es ist beim Lesen des Codes nicht immer einfach nachzuvollziehen, wo die Summe der Translationen gerade hinzeigt. Aus diesem Grund ist eine Dokumentation des aktuelle Zustands absolut notwendig.
Um die notwendigen Kommentare zu vereinfachen, gibt eine translate-Methode welche einen String-Parameter akzeptiert.
Dieser Parameter ist allein zu Dokumentations-Zwecken gedacht.
Damit ist sofort klar, wohin der aktuelle RenderingContext translatiert wurde.
Empfohlene Konvention: Die Beschreibung sollte mit "to " beginnen. Z.B. "to bottom of tick label".
Damit ist beim Review / Refactoring einfacher zu erkennen, was der aktuelle Zustand ist
12.3. Translate benutzen anstatt zu rechnen
Es macht den Code sehr viel einfacher, wenn der RenderingContext jeweils mit "translate" auf eine sinnvolle Position gesetzt wird.
|
Vor jedem Methodenaufruf wird der Context translatiert.
Eine Methode weiß (möglichst) nichts über ihre absolute Position auf dem Canvas. Dadurch ist der Code deutlich leichter erweiterbar. |
Dadurch werden die einzelnen Painting-Teile unabhängig von der absoluten Position.
So kann entsprechend leichter die Reihenfolge geändert werden oder neue Element eingeführt werden.
12.3.1. Beispiel: ValueAxis
Von links nach rechts hat die Value Axis folgende Elemente:
-
Titel + Gap (optional)
-
Achse
-
Ticks + Gap
-
Label
bzw. bei anderer TickOrientation:
-
Titel + Gap (optional)
-
Label
-
Gap + Ticks
-
Achse
Da jetzt jeder einzelne Teil relativ zu einem lokalen Wert paintet, ist dieser unabhängig von der Reihenfolge. → Die Achse wird von der immer gleichen Methode aufgerufen.
Diese unterscheidet sich auch dann nicht, wenn der Titel nicht sichtbar ist oder die TickOrientation geändert wird.
12.4. Painting Variables in (anonymem) Objekt halten.
Manchmal ist es hilfreich/notwendig einige Variablen zu speichern, die man während des Painting-Vorgangs benötigt. Um nicht zu viele Parameter übergeben zu müssen, bietet es sich an diese halten.
Wenn diese als "private val" gemerkt werden, "verschmutzen" diese allerdings den Namespace. Deshalb empfiehlt sich folgender Code:
private val paintingVariables = object {
@px var spaceForTitle: Double = 0.0
@px var someOtherPaintingVariable: Double = 0.0
}
Damit sind die Werte an einer Stelle zusammen gefasst. Der Compiler ist auch ohne Name in der Lage die Zugriffe zu ermöglichen.
| Aktuell funktionieren anonyme Objekte in JavaScript (noch) nicht. Daher muss für die Übergangszeit einen private Klasse angelegt werden. |
12.5. Config-Objekte und Lambdas
Dieser Abschnitt beschreibt das Lambda-Konfigurationsmuster generisch am Beispiel eines Style.
Es gilt identisch für das Configuration-Objekt eines Layers/Gestalt (Marker @ConfigurationDsl, Lambda additionalConfiguration) und das Style-Objekt eines Painters/Paintable (Marker @StyleDsl, Lambda styleConfiguration).
Welches der beiden Objekte ein Typ hält, ist in architecture/config-objects.md geregelt.
|
12.5.1. Default-Konfiguration
Jedes Config-Objekt hat eine Default-Konfiguration.
Die Teile der Konfiguration die individuell eingestellt werden sollen, lassen sich über Lambdas konfigurieren, die an das entsprechende Objekt übergeben werden. Damit lässt sich beim Aufruf gleich sehr bequem der Style konfigurieren.
class MyChart(
styleConfiguration: Style.() -> Unit = {} (1)
) {
val style: Style = Style().also(styleConfiguration) (2)
/**
* The style for this chart
*/
open class Style {
var backgroundColor: ColorProvider = Color.white (3)
var foregroundColor: ColorProvider = Color.orange
}
}
| 1 | Optionales Style-Konfigurations-Lambda wird übergeben |
| 2 | Und direkt für den mit einer Default-Konfiguration versehenen Style ausgeführt. |
| 3 | Jedes Element in einem Style hat einen Standard-Wert |
MyChart() {
backgroundColor = Color.blue (1)
}
| 1 | Es können einzelne (oder alle) Properties konfiguriert werden. |
12.5.2. Configuration von Sub-Styles
Mit Lambdas (zur Vermeidung von Duplicate Code)
Um nun den Style eines Styles zu konfigurieren, werden innerhalb eines Styles wiederum Lambdas gehalten. Dies ermöglicht es z.B. Fonts an weitere Unter-Styles zu delegieren ohne vars duplizieren zu müssen.
class MyChartWithSubStyles(
styleConfiguration: Style.() -> Unit = {} (1)
) {
val style: Style = Style().also(styleConfiguration) (2)
//somewhere the other components are instantiated
init {
MyChart(style.myChartConfiguration) (4)
}
/**
* The style for this chart
*/
open class Style {
var bg: ColorProvider = Color.white (5)
(3)
var myChartConfiguration: MyChart.Style.() -> Unit = {
backgroundColor = bg //<5> Attention. Shadowing!
}
}
}
| 1 | Übergabe der Style-Konfiguration (s.o.) |
| 2 | Anwenden der Style-Konfiguration (s.o.) |
| 3 | Im Style selbst ist ein Lambda konfiguriert, welches wiederum den Substyle konfiguriert |
| 4 | Das Sub-Style-Konfigurations-Lambda wird angewandt |
| 5 | Vorsicht wenn Properties den selben Namen haben: Shadowing-Gefahr |
MyChartWithSubStyles() {
bg = Color.blue (1)
(2)
myChartConfiguration = myChartConfiguration.and {
this.foregroundColor = Color.burlywood (3)
}
}
| 1 | Wie oben können Properties auf dem Style selbst direkt gesetzt werden |
| 2 | Das Default-Konfigurations-Lambda kann neu gesetzt werden |
| 3 | Durch die Extension-Method and kann das alte Lambda ebenfalls ausgeführt werden.
Damit ist es möglich nur einzelne Teile der Konfiguration zu überschreiben.
In diesem Fall wird durch das "Super-Lambda" die Hintergrundfarbe gesetzt (s.o.).
Und zusätzlich an dieser Stelle der Vordergrund. |
| Diese Lambdas müssen natürlich zur rechten Zeit aufgerufen werden. Eventuell in/vor jedem Paint! |
Mit Referenzierung der Sub-Styles
Wenn mehrere Styles aus unterschiedlichen Layers zusammen gefasst werden (z.B. in *Gestalt), dann ist es wenig praktikabel diese immer per Lambda zu konfigurieren.
In diesen Fällen ist es sinnvoll, die Sub-Styles als Field im eigenen Style zu halten und zu konfigurieren.
class MyChartWithReferencedSubStyles(
styleConfiguration: Style.() -> Unit = {} (1)
) {
val myChart = MyChart() (2)
val style: Style = Style(myChart.style).also(styleConfiguration) (3)
/**
* The style for this chart
*/
open class Style(
val myChart: MyChart.Style (4)
) {
var bg: ColorProvider = Color.white (5)
}
}
| 1 | Aufruf mit einer StyleConfiguration, die dann direkt ausgeführt wird. |
| 2 | Zunächst werden die Objekte instantiiert, deren Style später referenziert werden soll |
| 3 | Der eigene Style erhält als Konstruktor-Argument den Sub-Style. |
| 4 | Der Style wird als val referenziert und steht somit zur Konfiguration zur Verfügung. |
| 5 | Selbstverständlich können weitere (eigene) Properties definiert werden |
So kann dann der referenzierte (Sub-)Style konfiguriert werden:
MyChartWithReferencedSubStyles() {
bg = Color.blue (1)
myChart.backgroundColor = Color.burlywood (2)
myChart.foregroundColor = Color.beige (3)
}
| 1 | Wie oben können Properties auf dem Style selbst direkt gesetzt werden |
| 2 | Ein Property des Sub-Styles wird gesetzt. |
| 3 | Siehe <2> |
Wann Referenzen? Wann Lambdas?
Sub-Styles sollten direkt referenziert werden, wenn diese direkt so verwendet werden sollen. Dies ist z.B. der Fall, wenn eine Gestalt einen oder mehrere Layer verwendet.
Lambdas sind insbesondere dann sinnvoll, wenn Dinge mehrfach instantiiert werden (z.B. Painter).
12.6. Layer.layout() und Layer.paint()
Es gibt zwei Methoden im Layer die direkt nacheinander aufgerufen werden.
Zunächst wird layout() aufgerufen.
In dieser Methode können alle relevanten Größen (z.B. PaintingVariables) berechnet werden.
Direkt anschließend wird paint() aufgerufen.
12.6.1. Use Case
Manche Layer sind von den Layout-Informationen anderer Layer abhängig (z.B. Grid von ValueAxis). Deshalb ist es notwendig, dass die Layer ihre Berechnungen in der richtigen Reihenfolge durchführen.
Um aber z.B. das Grid auch unterhalb der Achse anzeigen zu können, muss die Berechnung des Layouts vom eigentlichen Painten getrennt werden.
12.6.2. Regel: Allokationsfreier Hot-Path
layout() und paint() laufen bei animierten/interaktiven Charts mit bis zu 60 fps.
Damit der Garbage Collector nicht zu sichtbaren Rucklern führt, gilt:
-
Per-Element-Zwischenergebnisse (Tick-Positionen, Bounds, Koordinaten, Labels) werden in
layout()in wiederverwendbare Buffer geschrieben (com.meistercharts.canvas.layout.buffer:DoubleMultiBuffer,CoordinatesMultiBuffer,BoundsMultiBuffer,StringMultiBuffer,LayoutVariablesObjectBuffer, …). Muster: einpaintingVariables-Objekt mitcalculate(). -
paint()liest nur — keine Collection-Allokation (listOf,map,filter,toList,sortedBy), keine Objekt-Konstruktion pro Element (Coordinates,Rectangle,Size,Insets,BoxStyle), kein String-Building pro Element. -
Formatter (
decimalFormat(…)) und abgeleitete Styles (LineStyle,BoxStyle,withAlpha) werden gecacht, nicht pro Frame neu erzeugt — sonst wird auch derCachedNumberFormat-String-Cache wirkungslos. -
Double?-Properties in Painting-Variablen boxen bei jeder Zuweisung — stattdessen primitivesDoublemitDouble.NaNals Reset-Wert (sieheLayoutVariable). -
Logging im Hot-Path nur über die Lambda-Form (
logger.trace { … }) — die String-Overload baut den Text bei jedem Durchlauf. -
gc.saved { }istinline(keine Allokation);gc.delayed { }ist es nicht — Lambdas dafür nicht pro Element erzeugen.
Konstruktions-, Konfigurations- und Event-Handler-Code ist von dieser Regel nicht betroffen.
13. Paintables
Dieser Abschnitt erklärt was Paintables sind und wie sie benutzt werden können.
13.1. Idee: Bilder und mehr
Im Canvas möchte man immer wieder grafische Elemente darstellen: Z.B. Bilder oder Icons oder auch dynamische Komponente (z.B. einen Bar eines BarCharts).
Während Bilder/Icons relativ einfach zu zeichnen sind, gibt es Use Cases, in welchen das Ergebnis dynamisch berechnet werden soll.
13.1.1. Dynamische Resources
Abhängig vom Use Case möchte man grafische Elemente z.B. von der aktuellen Locale (falls Texte enthalten sind) oder anderen Eigenschaften abhängig machen.
13.2. Eigenschaften von Paintables
Ein Paintable zeichnet sich an einer vorgegebenen Stelle auf den Canvas. Wie und mit welcher Größe dies erfolgt, wird durch das Paintable selbst konfiguriert.
| Es ist also nicht möglich die Größe eines Paintables von außen zu beeinflussen. Ausnahmen bestätigen die Regel. Daher müssen grundlegende Eigenschaften des Paintables bereits zum Zeitpunkt der Erzeugung gesetzt werden. |
13.2.1. paint-Methode
Die Paint-Methode zeichnet das Paintable an der übergebenen Stelle. Dabei entscheidet das Paintable selbst, in welche Richtung gezeichnet wird.
So werden z.B. Bilder typischerweise von der übergebenen Koordinate nach rechts unten gezeichnet. Ein Bar wird dagegen nach oben gezeichnet. Und ein Marker könnte vermutlich in alle Richtungen gezeichnet werden.
Informationen über die Richtung ergeben sich aus der Bounding Box des Paintables.
13.2.2. Bounding-Box
Jedes Paintable kennt seine Bounding-Box. Die Bounding-Box beschreibt den Bereich, der von dem Paintable gefüllt wird.
| Die Bounding-Box umfasst den "wichtigen" Bereich des Paintables. Es ist kein Clipping-Bereich. So können z.B. Labels oder andere "nicht-essentielle" Dinge auch außerhalb der Bounding-Box platziert werden. |
Die Bounding-Box dient dazu, dass Paintables aligned werden können. Dies kann z.B. bei einer Toolbar wichtig sein. Dort sollen die Paintables nebeneinander ausgerichtet sein. In den allermeisten Fällen wird die Bounding-Box bei der Verwendung der Paintables nicht berücksichtigt.
Alignment Point
Der Alignment Point eines Paintables entspricht der Origin der Bounding Box. Der Alignment Point wird beim normalen Zeichnen eines Paintables auf die übergebene Koordinate gelegt.
Der Alignment Point entspricht also dem "interessanten" Punkt eines Paintables (z.B. bei einem Marker das Zentrum).
paintInBoundingBox
Die Methode paintInBoundingBox kann genutzt werden, um ein Paintable ausgerichtet zu zeichnen.
Dies kann z.B. in einer Toolbar relevant sein.
Bei Verwendung der normalen Paint-Methode entscheidet das Paintable selbst, in welche Richtung es zeichnet.
Die paintInBoundingBox-Methode nutzen die Informationen aus der BoundingBox des Paintables und erlauben so die Positionierung des Paintables von außen.
14. Resizable Paintables (TBD)
Jedes Paintable hat eine Größe (durch die Bounding-Box geliefert). Manchmal ist es aber sinnvoll, wenn die Größe eines Paintables verändert werden kann.
Dies ist z.B. sinnvoll, wenn ein Paintable in vorgegebenen Platz eingefügt werden soll.
Dafür ist das Interface ResizablePaintable gedacht.
14.1. Use Case
Z.B. soll ein Kompass-Paintable den gesamten, verfügbaren Platz des Canvas nutzen (eventuell abzüglich einer Margin).
Dafür kann der ResizablePaintableLayer benutzt werden, der automatisch die Größe des Paintables an den verfügbaren Platz anpasst.
15. Default Icons
Unter resources/icons findet man alle potentielle Icons und auch die Skripte zum Herunterladen derselben.
Unter internal/closed/charting/meistercharts-canvas/src/icons sind alle Icons, die bereits in Meistercharts integriert sind und über com.cedarsoft.resources.Icons referenziert werden.
15.1. Schritte zum Hinzufügen neuer Default-Icons
-
Kopiere die gewünschte SVG-Bild-Datei nach
internal/closed/charting/meistercharts-canvas/src/icons -
gradle createIconDeclarationsausführen -
das Bild im Code an der gewünschten Stelle über
com.cedarsoft.resources.Iconsreferenzieren
Hinweis: die Klasse SvgPaintableProviders sorgt dafür, dass zur Laufzeit das Bild aus der Icons-Klasse geladen wird.
16. PaintableCalculator
Wenn @DomainRelative-Werte in einem Paintable gezeichnet werden sollen, kann der normale ChartCalculator nicht verwendet werden, da dieser immer die gesamte Breite/Höhe der ContentArea als Basis für Berechnungen nutzt.
Bei Paintables soll jedoch jeweils nur die Breite/Höhe des Paintables selbst als Berechnungsgrundlage benutzt werden.
Der PaintableCalculator kann im Paintable direkt instantiiert werden.
16.1. Annotation @PaintableArea
Diese Annotation beschreibt Werte, die relativ zum Ursprung eines Paintables sind.
17. Painters
17.1. Definition
Ein Painter ist ein Stück Code, welches einen einzelnen Aspekt des Zeichnens übernimmt.
Dabei sollte ein Painter möglichst leichtgewichtig sein und auch möglichst geringe Anforderungen an das Model stellen.
17.1.1. Abgrenzung zu Layer
Ein Layer "füllt" die gesamte Fläche des Canvas. Er kann Interaktionen verarbeiten und hat in der Regel ein etwas komplexeres Modell.
Idealerweise werden die einfachen in einem Layer verwendeten Zeichen-Operationen durch Painter erledigt.
17.2. Existierende Painter
17.2.1. LabelPainter
Zeichnet eine Liste von Labels (LayoutedLabel). Unterstützt die Anordnung rechts oder links des Anchor-Points.
Verwendet von
-
CrossWireLayer
-
DomainLabelPainter
-
OpacityLabelsCanvas (old)
-
17.2.2. DomainLabelPainter
Zeichnet ein Liste von Labels (DomainRelativeLabel). Verwendet dazu den LabelPainter.
Verwendet von
-
OpacityLabelsCanvas (old)
17.2.3. AreaPainter
Paints an area (rectangle)
Verwendet von
-
BlankAreaCanvas (old)
-
FovCanvas (old)
17.2.4. BinaryPainter
Zeichnet ein Kurve mit true/false Werten
Verwendet von
-
QIntDiagram (old)
17.2.5. ChartingStateDebugPainter
Visualisiert den aktuellen Zustand des Charting States
17.2.6. ContentAreaPainter
Zeichnet ein Rechteck in der Größe der Content Area. Dabei ist konfigurierbar welche Seiten gezeichnet werden.
Verwendet von
-
ContentAreaLayer
-
LimitsLayer
-
OpacityDiagram (old)
17.2.7. XyLinePainter: DefaultXyLinePainter / DomainXyLinePainter
Zeichnet eine Linie durch X/Y-Koordinaten
Verwendet von
-
QIntDiagram
17.2.8. XyAreaPainter
Zeichnet eine Fläche unter eine Reihe von Punkten (X/Y-Koordinaten).
Ähnlich wie XYLinePainter, allerdings wird die Fläche darunter ausgefüllt.
18. Tiling
Die Idee hinter Tiling liegt darin, dass in vielen Szenarien nur wenige Daten geändert werden.
Wenn das Rendering nun nicht direkt auf den Canvas, sondern in Bilder erfolgt, können diese Bilder dann verhältnismäßig schnell gepaintet werden.
18.1. Einsatz-Szenario
Tiling macht dann Sinn, wenn alle/einige dieser Kriterien erfüllt sind:
18.1.1. Viele Daten / Zeichnen dauert lange / Datenaufbereitung dauert
Wenn nur wenige Daten gezeichnet werden, lohnt sich der Overhead und die zusätzliche Komplexität des Tilings nicht.
Nur wenn so viele Daten gezeichnet werden sollen, dass die Performance nicht ausreicht, um alle zu zeichnen, spielt Tiling seine Vorteile aus.
18.1.2. Seltene Updates der Daten und/oder Updates weniger Daten
Tiling setzt voraus, dass sich entweder nur wenige Daten ändern - z.B. an einer Seite ("now") werden Daten hinzugefügt - oder aber Daten ändern sich nur selten (z.B. Kartendarstellung).
18.1.3. Häufige Repaints finden statt (z.B. Panning oder Interaktionen)
Tiling ist dann sinnvoll, wenn Repaints sehr häufig notwendig sind. Dies ist insbesondere dann der Fall, wenn Interaktionen stattfinden - z.B. durch Panning oder Mouse-Over-Effekte. Ein anderes Szenario ist eine automatische Translation bei der die aktuelle Ansicht z.B. über die Zeit mitscrollt.
18.1.4. Daten werden relativ zur Content Area gezeichnet - unabhängig vom Window
Die Berechnung der Tiles funktioniert unabhängig vom aktuellen Bildausschnitt. Beim Erstellen eines Tiles sind nur die Grenzen in Relation zur Content Area bekannt.
Daher können auch nur Elemente auf Tiles gezeichnet werden, die von der Window-Position unabhängig sind.
Achsen oder andere Elemente, die an einer festen Window-Position "kleben", können und sollten nicht durch Tiles gelöst werden.
18.2. Trennung statischer und dynamischer Teile
Oftmals gibt es statische und dynamische Teile. So könnte z.B. ein Mouse-Over oder eine Achse eine häufigere Änderung erfordern, als andere Daten.
In diesem Fall bietet sich eine Mischung aus Tiles (z.B. die Daten) und normalen Layers (z.B. die Achse und die Overlays) an.
18.3. Konzept
18.3.1. Tile
Ein Tile bezeichnet ein Element, welches sich selbst auf einer Canvas zeichnen kann. Dies ist typischerweise ein vorgerendertes Bild - aber auch andere Implementierungen sind denkbar.
Tile-Size
Jedes Tile hat eine fixe Größe (@Zoomed) - Höhe und Breite können sich allerdings unterscheiden. Alles Tiles sind immer gleich groß.
TileIndex
Der TileIndex beschreibt die Lage eines Tiles in Relation zum Ursprung der ContentArea. Die linke obere Ecke des Tiles mit dem Index 0/0 befindet sich direkt auf 0/0 der Content Area.
Mit Hilfe der Tile-Size und des Zoom-Faktors können Koordinaten auf einer Tile dann nach @ContentArea umgerechnet werden.
TileIdentifier
Der TileIdentifier identifiziert ein Tile eindeutig.
Der TileIdentifier besteht dazu aus einem TileIndex sowie dem Zoom-Faktor.
Der TileIdentifier dient dazu Tiles im Cache eindeutig zu identifizieren.
TileCalculator
Der TileCalculator bietet hilfreiche Umrechnungen beim Zeichnen eines Tiles.
Ähnlich dem ChartCalculator sind dort alle notwendigen Konvertierungsmethoden vorhanden.
18.4. TileProvider
Ein TileProvider ist dafür verantwortlich die Tiles zu liefern.
Verschiedene mehrfach genutzte Funktionen sind dabei bereits implementiert und können durch entsprechendes Wrappen der TileProvider wiederverwendet werden.
18.4.1. CachedTileProvider (unbedingt verwenden)
Dieser stellt einen Cache zur Verfügung, der die Tiles cacht und damit ein performantes Zeichnen überhaupt erst ermöglicht.
Wichtig ist, dass der Cache bei den Updates der Daten korrekt gelöscht wird. Idealerweise werden nur die Tiles aus dem Cache entfernt, die durch die Updates der Daten auch betroffen sind.
Bei Erreichen der Maximal-Größe des Caches werden die ältesten Tiles automatisch gelöscht. Es ist daher nur notwendig manuell Tiles zu löschen, sofern diese aufgrund von Änderungen der Daten nicht mehr aktuell sind.
18.4.2. CanvasTileProvider
Dieser TileProvider sollte genutzt werden, wenn die Tiles mithilfe einer Canvas erstellt werden. Dieser Provider ist der am häufigsten verwendete Provider, wenn Tiles gezeichnet werden.
Der CanvasTileProvider nutzt einen CanvasTilePainter, um die Tiles zu erzeugen.
| Ein CanvasTileProvider arbeitet mit einer fixen physikalischen Größe. D.h. die Größe eines CanvasTiles ist immer gleich groß - unabhängig von der Device Pixel Ratio. Die Umrechnung auf eine logische TileSize erfolgt automatisch im CanvasTileProvider und ist für den TilePainter transparent. |
18.4.3. Typischer Aufbau
TilesLayer ⇨ CachedTileProvider ⇨ CanvasTileProvider ⇨ TilePainter (Custom Klasse)
D.h. für den typischen Use-Case ist nur notwendig einen TilePainter zu implementieren.
| Wichtig - nicht das Clearen des Caches vergessen |
18.4.4. Edge Cases / Besonderheiten / typische Fehler
Tile-Ränder
Generell muss beachtet werden, dass z.B. Linien auch über die Ränder eines Tiles hinweg gezeichnet werden. Damit ist sichergestellt, dass z.B. Linien auch über die Ränder hinweg korrekt sichtbar sind.
| Jeweils einen Datenpunkt mehr abfragen, als für dem rechten bzw. linken Rand entsprechen. Damit ist sichergestellt, dass korrekt gezeichnet wird. |
Updates / Cache-Clear
Dies ist auch beim Clearing der Caches zu berücksichtigen. Es müssen auch die Tiles neu erstellt werden, für welche diese Punkte zum Zeichnen benötigt werden - auch wenn diese nicht selbst auf dem Tile liegt.
Dashed Lines
Bei Dashed Lines sind die Ränder der Tiles relativ leicht zu erkennen, da die Dashes nicht korrekt durch gezeichnet werden.
| Eine mögliche Lösung besteht darin, die Dashed Line jeweils an einem Punkt beginnen zu lassen - und nicht durch alle Punkte zu zeichnen. Dadurch ist die Positionierung der Dashes reproduzierbar. Allerdings ist diese Darstellung an den Punkten nicht optimal - insbesondere wenn eine Linie optisch "durch" einen Punkt führt und/oder der Punkt sehr klein oder gar nicht dargestellt wird. |
@PhysicalPixels vs @ContentArea
Der CanvasTileProvider unterscheidet zwischen der physikalischen Auflösung und den "logischen" Pixeln.
Um die bestmögliche Qualität bei der Darstellung zu erreichen, wird ein CanvasTile immer 1:1 dargestellt - d.h. die Device Pixel Ratio wird nicht berücksichtigt.
Um trotzdem eine korrekte Darstellung zu erreichen, wird die Skalierung automatisch durchgeführt, bevor der CanvasTilePainter aufgerufen wird.
Die Device Pixel Ratio wird korrekt berechnet und muss bei der Implementierung eines CanvasTilePainter nicht berücksichtigt werden.
|
18.5. Diagnose-Hilfen
Um Fehler beim Tiling auf die Spur zu kommen, gibt es einige vorgefertigte Layer, welche die Diagnose erleichtern:
18.5.1. TilesDebugLayer
Dieser Layer kann einfach hinzugefügt werden und zeichnet die Tile-Grenzen.
| Dieser Layer ist insbesondere hilfreich, wenn seltsame Lücken auftreten. Mit Hilfe des Layers kann erkannt werden, ob diese Lücken an den Tile-Grenzen liegen (s.o.). |
19. Slippy Maps
MeisterCharts nutzt die Slippy Maps (https://wiki.openstreetmap.org/wiki/Slippy_Map) von openstreetmap zur Darstellung von Karten in Layern.
Der folgende Text ist eine Adaption von https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames.
Die Idee von Slippy Maps ist eine Karte in gleich große Kacheln (bei openstreetmap 256x256 Pixel) aufzuteilen. Außerdem gibt es verschiedene Zoomstufen (bei openstreetmap von 0 bis 19).
Jede Kachel ist durch eine X/Y-Koordinate adressierbar.
-
X geht von 0 (linker Rand ist 180° W) bis 2Zoom - 1 (rechter Rand ist 180 °E)
-
Y geht von 0 (oberer Rand ist 85.0511° N) bis 2Zoom - 1 (unterer Rand ist 85.0511 °S) in einer Mercator Projektion
Alle Kacheln ergeben ein Quadrat (deshalb sind die Grenzen auf der Y-Achse so "krumm" gewählt).
D.h. man kann aus jeder Kombination von Längengrad und Breitengrad plus Zoom die X/Y-Koordinate der zugehörigen Kachel berechnen.
Aus X-, Y- und Zoom-Wert ergibt sich letzten Ende die URL unter der das Kachelbild von einem Slippy Maps Server abgerufen werden kann.
19.1. SlippyMapLayer
Der Layer SlippyMapLayer malt die Kacheln von openstreetmap.
Intern implementiert er das TileProvider Interface.
19.1.1. TileIndex
Dabei wird der TileIndex aus dem TileIdentifier 1:1 übernommen.
Man muss nur berücksichtigen, dass der Index in den entsprechen Grenzen liegt (X und Y jeweils in [0..2Zoom-1]).
Ist eine Koordinate kleiner als 0, so beginnt man wieder bei 2Zoom-1, ist sie größer als 2Zoom-1, so beginnt man wieder bei 0.
D.h. der rechte Rand einer Karte grenzt stets an den linken Rand der nächsten Karte und die Karten werden unendlich oft wiederholt.
19.1.2. Zoom
Der Zoom aus dem TileIdentifier bedarf einer Umrechnung, denn Slippy Maps unterscheidet nicht zwischen einem X- und Y-Zoomfaktor.
Desweiteren bedeutet bei MeisterCharts ein Zoomfaktor von 1.0 kein Zoom.
Bei Slippy Maps gibt es Zoomstufen von 0 bis 18 (bei openstreetmap von 0 bis 19).
Für die Umrechnung wird dem Zoomfaktor 1.0 die Slippy Map Zoomstufe 9 zugeordnet.
Für das Hinauszoomen bedeutet das, dass Zoomfaktor 0.5 die Zoomstufe 8 zugeordnet wird, 0.25 wird zur Zoomstufe 7 usw.
Für das Hineinzoomen bedeutet das, dass Zoomfaktor 2.0 die Zoomstufe 10 zugeordnet wird, 4.0 wird zur Zoomstufe 11 usw.
Die Rechenvorschrift lautet also:
ZoomstufeSlippyMap = 9 + log2(Zoomy)
Damit log2(Zoomy) ein ganzzahliger Wert ist, muss zoomChangeFactor auf 2.0 gesetzt werden (standardmäßig ist es die Wurzel aus 2.0).
Des weiteren empfiehlt sich die Verwendung von ZoomAndTranslationModifiersBuilder.withSlippyMapZoom(), damit der Zoom in X- und Y-Richtung stets derselbe ist und der Zoomfaktor nicht die openstreetmap Zoomstufen unter- bzw. überschreitet.
19.1.3. ContentArea
Die contentAreaSizingStrategy von MeisterCharts muss auf FixedContentAreaSize(SlippyMapContentAreaSize) gesetzt sein.
So ist die Content-Area unabhängig von der Window-Größe, nämlich die Anzahl der Kacheln beim Standard-Zoom (2SlippyMapDefaultZoom x 2SlippyMapDefaultZoom) mal 256 Pixel.
19.1.4. Rechtliches
openstreetmap ist mit der ODbL lizenziert (https://www.openstreetmap.org/copyright/en).
Verwendet man es, so sollte man dies irgendwo kenntlich machen, entweder auf dem Canvas selbst (z.B. mit einem MessageLayer) oder
auf der Website.
Desweiteren sind die Kachelserver von openstreetmap nicht für den produktiven Einsatz gedacht (https://operations.osmfoundation.org/policies/tiles/). Für eine Demo sollte ihre Verwendung in Ordnung sein.
20. Painting Performance
Der Painting-Code wird 60x pro Sekunde aufgerufen. Idealerweise hat unser Kunde mehrere Komponente auf einer Seite. Daher ist die Performance des Painting-Codes von entscheidender Bedeutung.
Bei 100 Komponenten und 60fps ergeben sich 6.000 Repaints pro Sekunde.
20.1. Relevante Performance
20.1.1. CPU-Performance
Es muss darauf geachtet werden, dass möglichst nur notwendige Operationen durchgeführt werden. Ansonsten wird es langsam und der Kunden unzufrieden.
20.1.2. Speicherverbrauch
Jedes instantiierte Objekt bedeutet Aufwand beim Anlegen und beim Abräumen. Deshalb ist es wichtig im Painting-Code möglichst keine (oder wenigstens möglichst wenige) Objekte anzulegen.
Ansonsten wird der Browser nach gewisser Zeit und in regelmäßigen Abständen ruckeln - und der Kunde ist unzufrieden.
20.2. Grundsätzliche Vorgehensweise
Profiling, Profiling, Profiling!
| Bitte nur das optimieren, was im Profiler auch tatsächlich auftaucht! |
20.2.1. Optimieren auf den Worst-Case
Im Worst-Case wird 60x pro Sekunde neu gepainted: Z.B. bei Animationen über die Zeit. Daher optimieren wir für diesen Fall (60x pro Sekunden ein Repaint mit veränderte Werten - auch Translation und/oder Zoom).
In allen anderen Fällen ist die Performance sowie besser - daher sind Optimierungen für diese nicht entscheidend. Sie können durchgeführt werden, wenn die Lesbarkeit des Codes darunter nicht leidet.
Optimierungen machen dann keinen Sinn, wenn sie davon ausgehen, dass…
-
… die Translation die gleiche bleibt
-
… der Zoom-Faktor gleich bleibt
-
… das Modell gleich bleibt
-
… die Sprache gleich bleibt (hier eventuell Ausnahmen möglich)
Es macht also keinen Sinn Zwischenergebnisse für den nächsten Paint-Vorgang zu cachen.
Es lohnt sich dagegen sehr wohl, Zwischenergebnisse zu cachen, die innerhalb des selben Paints wieder genutzt werden.
| Special Case: Interaktionen: Im Falle von Interaktionen - z.B. bei MouseMove-Events, ist es notwendig/hilfreich die Layout-Ergebnisse zu speichern. Damit ist es erst möglich auf entsprechende Interaktionen zu reagieren. |
markAsDirty() ist bereits implementiert
Ein Repaint findet nur statt, wenn sich die Daten auch tatsächlich (zumindest potentiell) geändert haben.
Falls keine solchen Änderungen vorhanden sind, wird auch markAsDirty() nicht aufgerufen und der Repaint unterbleibt.
Daher profitieren wir bereits - ohne weiteres zutun - wenn sich Werte nicht ändern.
20.2.2. Szenario: 100x auf einer Seite + MarkAsDirtyLayer
Für Tests wird die entsprechende Komponenten 100x auf einer Seite eingebunden und durch Einfügen einer MarkAsDirtyLayer ein wiederholter Repaint geforct.
20.3. Best Practices
20.3.1. For-Schleifen (statt forEach/map/…)
Um das Anlegen von Iterables zu vermeiden, bitte im Painting-Code direkt traditionelle For-Schleifen benutzen.
20.3.2. Enum#values vermeiden
Dieser Code benutzt im Hintergrund clone(). Im Zweifel lieber eine manuelle #get-Methode einfügen
20.3.3. Vorsicht beim Boxing
Lambdas + primitive Datentypen
Lambdas werden in Kotlin als Function0..n repräsentiert.
Diese enthalten Generics als Parameter + Rückgabewert.
Bei primitiven Datentypen daher selbst ein Interface definieren.
Inline Classes
Wenn diese z.B. in einem Cache als Key verwendet werden, ist ein Boxing (wegen equals/hashCode) notwendig.
Caches + primitive Datentypen
Wie bei Java auch, werden primitive Datentypen bei der Verwendung in Caches geboxt.
20.3.4. Instantiieren von Lambdas vermeiden (inline-Methode nutzen)
Im Painting-Code möglichst keine/wenig Instanzen von Lambdas anlegen.
Entsprechende Methoden eventuell als inline markieren.
20.3.5. Werte nur berechnen, wenn wirklich benötigt (Stichwort when)
Manchmal werden z.B. Textbreiten nur bei bestimmten AnchorDirections benötigt. In diesen Fällen sollte die Text-Breite auch wirklich nur in den entsprechenden Fällen berechnet werden.
21. History
21.1. Grundlegende Ideen
21.1.1. Timestamps + Grenzen
Die Grenzen werden durch mathematische Rundung auf "runde" Werte gelegt. Diese Berechnung erfolgt gegen die Basis der Timestamps (1.1.1970).
Zeitzonen
Es gibt alle möglichen "krummen" Zeitzonen. Daher ist es (sowieso) nicht möglich alles oberhalb von Minuten-Werte sinnvoll direkt zu nutzen. Es sind immer Konvertierungen und andere Zusammenfassungen möglich.
Daher ist es egal wo in den größeren Buckets (Stunde, Tag etc) die Grenzen liegen bzw. ob diese mit echten, kalendarischen Grenzen überein stimmen.
Die Berechnungen erfolgen im Rahmen der Enum HistoryBucketRange.
21.1.2. Downsampling
Sobald Zooming unterstützt wird, wird es notwendig das Zusammenfassen von Datenpunkten zu unterstützten.
Verbesserte Übersicht
Auf einem Monitor können nur so viele Datenpunkte angezeigt werden, wie auch Pixel zur Verfügung stehen. Je weiter herausgezoomt wird, desto wichtiger wird die Aufbereitung der Daten, um die Wahrnehmbarkeit zu verbessern.
Durchschnitt
Wichtigster Wert ist dazu das arithmetische Mittel, welches in den entsprechenden Zoom-Stufen angezeigt wird.
Min/Max
Um Extrem-Fälle nicht zu verstecken, werden außerdem Min und Max berechnet. Diese können dann (z.B. als "Stäbchen") angezeigt werden.
Performance
Mindestens so wichtig sind Performance-Aspekte. Es sollen möglichst wenige Punkte gezeichnet bzw. beim Zeichnen berücksichtigt werden müssen.
22. Essentielle Klassen
22.1. SamplingPeriod
Beschreibt die Auflösung (im Sinne von Abstand zwischen Datenpunkten).
Hat eine 1:1-Beziehung mit der HistoryBucketRange.
22.2. HistoryBucketRange
Beschreibt einen Zeitbereich, welcher von einem Bucket abgedeckt wird. Enthält keinen Start/Endzeitpunkt.
Hat eine 1:1-Beziehung mit der SamplingPeriod.
22.3. HistoryBucketDescriptor
Identifiziert eindeutig einen HistoryBucket.
Enthält dazu die HistoryBucketRange (und dadurch auch eine Länge) und einen "Index".
Dieser Index definiert welcher Bucket seit 1970 gemeint ist.
Durch diese Informationen sind Start und Ende des Buckets eindeutig definiert.
22.4. HistoryBucket (High Level)
Ein Bucket hat klar definierte Ränder, die auf gerade Grenzen liegen.
Ein HistoryBucketDescriptor enthält alle Informationen, um einen Bucket zu beschreiben.
Ein Bucket enthält seine Daten in Form eines Chunks.
Objekte dieser Klasse sollten möglichst immer verwendet werden, sobald Berechnungen oder ähnliches durchgeführt werden.
22.5. Chunk (Low Level)
Ein Chunk entspricht "rohen" Historien-Daten welche nicht (unbedingt) auf "geraden" Grenzen liegen. Diese werden z.B. von einem Sensor übertragen.
Ein Chunk wird auch von einem Bucket gehalten - in diesem Fall dann natürlich "zufällig" auf gerade Grenzen.
23. Technische Funktion Downsampling
23.1. Grundkonzept
23.1.1. Möglichst selten, möglichst wenig neu berechnen
Das Berechnen des Downsampling ist relativ CPU-aufwändig. Aus diesem Grund soll möglichst selten das Downsampling neu berechnet werden.
Gleichzeitig sollen möglichst nur die Bereiche neu berechnet werden, bei welchen auch tatsächlich ein Update der zugrunde liegenden Daten erfolgt ist.
23.2. Funktionsweise Übersicht
Zunächst werden automatisch jene Bereiche als "dirty" markiert, welche ein Update der Daten erfahren haben.
Jedes mal wenn z.B. Daten mit einer SamplingPeriod von 100 ms eingetragen werden, wird dieser exakte Zeitbereich für die nächst höhere SamplingPeriod (1 Sekunde) als dirty markiert. Die entsprechenden Zeitbereiche werden dann zusammen gefasst, so dass idealerweise bei regelmäßigen Updates ein größerer Dirty-Zeitbereich entsteht.
Zum geeigneten Zeitpunkt wird dann der entsprechende Bereich im 1-Sekunden-Bereich neu berechnet und in die History-Storage eingestellt. Durch dieses Update wird wiederum der entsprechende Bereich der nächsten Ebene als dirty markiert und so fort.
23.2.1. HistoryUpdateInfo
Die Klasse HistoryUpdateInfo enthält den exakten Zeitbereich, welcher sich geändert hat.
Dieser Bereich wird auch beim Berechnen des Downsampling und aktualisieren des HistoryStorage beibehalten.
Dadurch wird erreicht, dass wirklich nur die minimalen Zeitbereich auch tatsäclich als dirty markiert werden.
23.3. Funktionsweise detailliert
23.3.1. Dirty Ranges Collection
Die Klasse DownSamplingDirtyRangesCollector sammelt die Bereiche ein, für welche ein Downsampling neu berechnet werden muss.
Dazu wird die Methode observe aufgerufen und der Collector damit an einem HistoryStorage registriert.
Die Dirty Ranges werden automatisch zusammen gefügt, so dass im Normalbetrieb nur einzelne, große TimeRanges entstehen.
23.3.2. Downsampling-Berechnung
Der DownSamplingService übernimmt die Berechnungen des Downsamplings.
Der Service nutzt einen HistoryService zum Lesen der Daten und zum Schreiben der neu berechneten Downsampling-Werte.
Die Berechnung des Downsamplings wird zum richtigen Zeitpunkt vom TimerSupport aufgerufen.
Diese wird durch downSamplingService.scheduleDownsampling aktiviert.
24. FAQs
24.1. Value Axis
24.1.1. Wie kann die Value-Axis auf die Content-Area-Grenze gelegt werden?
//Content area has the same size as the window
meisterChartBuilder.contentAreaSizingStrategy = BindContentAreaSize2ContentViewport() (1)
val passpartoutMargin = Insets(20.0, 10.0, 80.0, 120.0)
meisterChartBuilder.zoomAndTranslationDefaults {
FittingWithMargin(passpartoutMargin) (2)
}
meisterChartBuilder.configure {
//layers.add(...) //add the content layer
//Clip the content area layer
val contentLayer = EmptyLayer //the real content layer is initialized
layers.addLayer(contentLayer.clipped(passpartoutMargin)) (3)
layers.addLayer(ValueAxisLayer(ValueAxisLayer.Configuration(valueRangeProvider = { ValueRange.linear(0.0, 123.0) })) {
titleProvider = { _, _ -> "The Value Axis [m²/h]" }
side = Side.Left //Other side work exactly the same
tickOrientation = Vicinity.Outside (4)
size = passpartoutMargin.left - margin.left (5)
//size = passpartoutMargin.bottom - margin.left //for horizontal
})
}
| 1 | Content Area ist immer gleich groß wie das Window. |
| 2 | FittingWithMargin als "Rand" der Content-Area verwenden — beim Reset/Resize |
| 3 | Der Content-Layer wird mit einem Clip versehen. Dadurch wird verhindert, dass der Content über den Rand hinaus zeichnet. Hinter der Value-Axis ist somit kein Content sichtbar. |
| 4 | Wichtig: Nur bei Outwards landet die Achse auf der durch den Extend vorgegebenen Position. |
| 5 | Hier wird die Margin mit bei der Positionierung einberechnet. Extend + Margin ergeben die exakte Position der Achse. |
24.2. Paintables: Wie kann ein anderes Theme (z.B. FontAwesome anstatt MaterialIcons) aktiviert werden.
Generell müssen die korrekten Stellen identifiziert werden, an denen Icons (Paintables) verwendet werden.
Dies ist typischerweise beim ToolbarLayer der Fall.
An der entsprechenden Stelle dann die korrekten Icons instantiieren.
Es gibt keinen klassischen Support for Themes bei Icons (mehr).
24.3. Zeichenprimitive
24.3.1. Warum sehe ich eine Linie nach stroke() über/hinter/vor einem Rectangle (nicht)?
stroke() zeichnet die halbe Linienbreite inner- und die andere Hälfte außerhalb der angegeben Koordinaten/Form.
Deshalb gibt es manchmal komische Effekte.
Lösungsvorschläge:
+ zuerst fill(), dann stroke() aufrufen.
* Falls das nicht möglich ist, die entsprechenden Linien einpassen (d.h. jeweils die halbe Linienstärke links und rechts bzw. oben und unten abziehen).
25. Architecture Hints - für Layer und Gestalt
Hier sind einige Vorschläge zu finden, die bei der Entwicklung neuer Gestalts helfen.
25.1. Content Area
Zunächst sollte festgelegt werden, welche Teile des Layers in der Content Area liegen.
Die sinnvolle Verwendung der Content Area ist auf jeden Fall notwendig, da viele Konzepte (z.B. das Limitieren des Zoomings/Pannings) auf der Content Area aufbauen.
25.1.1. Inhalt vs. "Verzierung"
Es bietet sich an wirklich nur den eigentlich Inhalt selbst ohne "Verzierungen" drum herum in die Content Area zu platzieren.
Insbesondere Dinge, die nicht mit gezoomt werden, sollten nicht Teil der Content Area sein.
Statt dessen ist die Verwendung z.B. der FittingWithMargin sinnvoll, um Platz für die Dekorationen zu schaffen.
26. Usage and Charting FX
26.1. Usage ChartingFX
26.1.1. Gradle: Minimized JAR
Gradle kann nicht benötigte Files aus dem JAR entfernen.
Dabei werden allerdings Dependencies, welche mit
api eingebunden sind, nicht entfernt.
Daher sollte die Dependency auf ChartingFX auf jeden Fall als implementation eingebunden werden.
Wenn z.B. in SDDs Common-Code ausgelagert und dann genutzt wird, sollte diese Dependency mit dem Keyword
api eingefügt werden.
Damit ist dann sicher gestellt, dass der komplette Inhalt des Common-Teils im JAR-File landet.
|
Appendix A: Sichtbarkeit und Größen von Block-Elementen
Gegeben ein DIV mit style="width:200px;height:200px"
| Wird beim Layout berücksichtigt | Größe (px) | |
|---|---|---|
style="visibility:hidden" |
ja |
200 x 200 |
style="display:none" |
nein |
0 x 0 |
hidden="hidden" |
nein |
0 x 0 |
Gegeben ein DIV mit style="width:200px;height:200px" und ein Kind-DIV mit style="width:200px;height:200px"
| Parent-DIV | Child-DIV Größe (px) |
|---|---|
style="visibility:hidden" |
200 x 200 |
style="display:none" |
0 x 0 |
hidden="hidden" |
0 x 0 |