Karten in QlikView & A brief history of time

Landkarten in QlikView - eine historische Aufarbeitung

Benötigte man in QlikView 8 eine Kartendarstellung, dann war die Antwort recht einfach: Man nehme ein fixes Hintergrundbild und benutze ein Punktdiagramm (Scatter Chart) um die Punkte entsprechend darauf zu positionieren.  Die Punktgröße kann zusätzlich genutzt werden um die Kennzahl in Relation zu stellen.

Die Einwohnerzahlen auf der Weltkarte mittels Punktdiagramm

Ausgehend von diesem Grundgedanken gab es tolle Verfeinerungen dieses Verfahrens (Drilldown von Land auf Bundesländer indem man pro Bundesland ein verstecktes Chart vorbereitete; Kabinenbelegungen auf dem Grundrissplan eines Kreuzfahrtschiffes, etc.). Auf meiner Festplatte schlummern dazu noch einige Schätze :).

Kreuzfahrtschiff Kabinenbelegung mit einem Punktdiagramm

QlikView 9 beinhaltete dann die Google Maps Integration. Das ermöglichte den dynamischen Kartendrilldown bis auf Straßenebene, brachte aber auch die Problematik des Lizenzthemas mit sich: Google erlaubt zwar das Kartenmaterial gratis im Internet wiederzuverwenden; da viele QlikView Applikationen aber reine Intranet-Applikationen sind, sollte man hier die Google Lizenzvereinbarungen genauer studieren. Und natürlich benötigt der Anwender eine aktive Internetverbindung um das Kartenmaterial On The Fly von Google nachzuladen.

Rechts die "gezoomte" GoogleMaps Integration im Vergleich zur "starren" Punktdiagramm Lösung


Beschäftigt man sich heute mit Kartendarstellungen in QlikView, findet man mittlerweile eine Vielzahl von interessanten Beispielen in der QlikCommunity bzw. Lösungen von professionellen Anbietern im QlikView Market (inklusive unser eigenen HD Heatmap Extension :-)). Auch QlikTech selbst fördert dieses Thema im aktuellen Release QV11.20 SR5 mit einer weiteren Karten-Extension.

Wer noch keine Möglichkeit hatte einen Blick auf die neue QV11.20SR5 Extension zu werfen, findet das Beispiel auf unserem Demo-Accesspoint. Die neue Extension bietet 3 Möglichkeiten Informationen auf der Karte darzustellen:
  • Punktdarstellung
  • Routendarstellung
  • Polygondarstellung
Die Extension erlaubt das Einbinden von unterschiedlichen (freien) Kartenanbietern, Zoom bis auf Straßenebene sowie eine bidirektionale Kommunikation (Auswahl in der Karte, selektiert den Wert in der QlikView Applikation und vice versa).  Momentan noch "unfertig" sieht die Tooltip-Implementierung aus - da muss man manuell nachbessern.

Die verschiedenen Darstellungen der QV11.20SR5 Extension

Routen und Flächen im IEPlugin

Extensions sind nur im QlikView FullBrowser (AJAX) Client verfügbar. Benutzten die Endanwender das ActiveX/IE-Plugin, kann man also die neue Extension leider nicht einsetzen. Für die Darstellung von Punkten auf Landkarten funktioniert die genannte Punktediagramm-Methode mittels fixen Hintergrundbild bzw. Google Maps Integration. Die Frage ist nun: klappt es auch Routen und Flächen im IEPlugin darzustellen?

Für Routen kann man sich eine Eigenschaft des ScatterCharts zu Nutzen machen: Will man in QlikView etwa sehen, wie sich ein Produkt über die Jahre entwickelt, kann man das Jahr als erste Dimension im Chart hinzufügen. Stellt man dann unter "Präsentation" noch die Pfeildarstellung ein, erreicht man folgende Darstellung:

Punkdiagramm mit 2 Dimensionen: das Produkt Minnki Pälsii fällt beim Bruttogewinn seit 3 Jahren ab
Wenn man also Pfeile im Punktdiagramm darstellen kann, warum diese nicht auch für Routen benutzen? Bereitet man sich im Datenmodell ein entsprechendes Routen-Feld vor, kann man folgende Darstellung erreichen.

Routen im Punktdiagramm
Mit sehr vielen Routen wird die fixe Kartendarstellung mangels dynamischen Zooms etc. nicht alle Anforderungen erfüllen können. Für kleinere Anwendungsgebiete ist diese Möglichkeit jedoch schnell & einfach zu implementieren. Nutzt man das Diagramm in Kombination mit der Google Maps Integration, hat man sogar eine "zoombare" Lösung. Das gesamte Beispiel können Sie sich hier ansehen, bzw. über http://demo.heldendaten.net herunterladen

Für die Darstellung von Flächen kann man das QlikTech Punktdiagramm leider nicht nutzen. Punkte und Linien sind die einzigen beiden Darstellungsformen. Hier kenne ich lediglich die Möglichkeit, die Landkarte über die Google Chart API einfärben zu lassen. Exemplarisch sei hier ein Beitrag aus der QlikCommunity gezeigt:

Beispiel aus Rob Wunderlich's QlikView Cookbook

Spanische Provinzen Heatmap


Die Lösung setzt eine aktive Internetverbindung voraus, da bei jeder QlikView-Selektion eine URL bei Google aufgerufen wird, und das gerenderte Bild zurückkommt. Folgende zwei Dinge sollten bei dieser Lösung beachtet werden:
  • Bitte Vorsicht bei sensiblen Unternehmensdaten!  Diese Daten gehen an Google.
  • Meiner Erfahrung nach kann ein GET-Parameter nicht unendlich lang werden (zumindest war das in QV9 der Fall). Also für sehr viele Flächenfärbungen könnte man hier an Grenzen stoßen.



  





The Lost Datarows: DISTINCT - Verhalten bei Join, Concatenate

Normalerweise erzähle ich meinen QlikView-Novizen immer, dass man ein QlikView Skript von oben nach unten (und - falls Reiter vorhanden - von links nach rechts) liest. Innerhalb eines Load&Select-Statements gilt die Ausnahme, dass man diesen Statement-Block von unten nach oben lesen muss. Zuerst kommen die Daten aus einer Datenquelle (unten), dann werden sie mit einem (oder mehreren) Load-Statement weiterverarbeitet.

Hier ein einfaches Beispiel - aus der Ursprungstabelle lade ich mit einem oberhalb liegenden LOAD DISTINCT die eindeutigen Preise für die 3 Produkte. Das Statement muss von unten (Inputtabelle hat 4 Zeilen*3 Spalten) nach oben  (Ergebnistabelle hat 3 Zeilen*2 Spalten) gelesen werden.

Es gibt jedoch einen Fall den man genauer betrachten muss: LOAD DISTINCT in Kombination mit JOIN bzw. CONCATENATE:

Ergänzt man das Skript um einen Left Join auf die Faktentabelle, sieht das Skript folgendermaßen aus:

Facts:
LOAD * INLINE [
    Year, Product, Quantity
    2013, ProductA, 500
    2013, ProductA, 500 
    2013, ProductB, 300
    2012, ProductA, 700
    2012, ProductB, 400
];

left join (Facts)
Load
Distinct
 Product,
 Price;
LOAD * INLINE [
    Product, Price, Variante
    ProductA, 100, X
    ProductA, 100, Y
    ProductB, 10, A
    ProductC, 700, A
];


Liest man das Skript nach den genannten Regeln, würde man folgendes Join-Verhalten erwarten:
  • (Schritt -1: Die Faktentabelle wird  mit 5 Zeilen geladen)
  • Schritt 0: Die Produkttabelle wird mit 4 Zeilen INLINE geladen
  • Schritt 1: Load Distinct reduziert die Produkttabelle auf 3 Zeilen
  • Schritt 2: Der Left Join beläst per Definition die Faktentabelle (linke Tabelle) gleich und ergänzt die passenden Preise aus der Produkttabelle (rechten Tabelle).
Symbolische Darstellung des "zu erwartenden" Verhalten

Als Ergebnis würde man sich also eine Quantity von (500+500+300+700+400) = 2400 erwarten.

Lässt man das Script aber in QlikView laufen, erhält man überraschenderweise ein Chart mit der Quantity 1900:


Quanity für ProductA ist 2013 nur 500
Was ist passiert? Die Quantity für ProductA ist im Jahr 2013 nur 500, statt der ursprünglichen 1000 aus der Faktentabelle. Die Faktentabelle enthält auch nur 4 statt der ursprünglichen 5 Zeilen. Es scheint also, als ob der LEFT JOIN Einfluss auf unsere Faktentabelle genommen hat.

Betrachtet man das Ergebnis genauer, so ist es nicht der LEFT JOIN, sondern vielmehr das Keyword DISTINCT auf der Produkttabelle das Schuld an unserem fehlenden Datensatz ist. Das DISTINCT hat nicht nur Einfluss auf die geladene Produkttabelle, sondern reduziert auch alle Einträge in der Faktenabelle auf eindeutige Werte.  


Beim Lesen eines QlikView Skrips muss also folgende zusätzliche Regel beachtet werden:


DISTINCT wird in QlikView nicht auf die Inputtabelle angewandt, sondern am auf die gesamte, geladene Tabelle! Wird eine Tabelle also einmalig als DISTINCT markiert (direkt oder indirekt [durch Join/Concatenate]) sind die Zeilen diese Tabelle in allen weiteren Operationen IMMER DISTINCT!

Um aus der ursprünglichen Faktentabelle nicht "versehentlich" Zeilen zu entfernen, benötigen wir somit eine Zwischentabelle. Das Skript sieht dann folgendermaßen aus

FixFacts:
LOAD * INLINE [
    FixYear, FixProduct, FixQuantity
    2013, ProductA, 500
    2013, ProductA, 500 
    2013, ProductB, 300
    2012, ProductA, 700
    2012, ProductB, 400
];

Temp:
Load
Distinct
 FixProduct,
 FixPrice;
LOAD * INLINE [
    FixProduct, FixPrice, FixVariante
    ProductA, 100, X
    ProductA, 100, Y
    ProductB, 10, A
    ProductC, 700, A
];

left join (FixFacts)
load
*
resident Temp;

drop table Temp;
 
 
Quantity 2400 als korrektes Ergebnis

Hier klappt es nun wie erwartet, weil nur die "Temp"-Tabelle mit dem Keyword DISTINCT markiert wurde. Die "FixFacts" Tabelle ist an keiner Stelle mit dem Keyword DISTINCT versehen. Somit bleibt das ProduktA im Jahr 2013 mit einer Quantity von 500 zweimal in der Tabelle erhalten.

Ein zweites Szenario wo man dieses Verhalten manchmal unabsichtlich auslöst, ist der Befehl CONCATENATE. Das Skript unterhalb löscht Zeilen aus meiner Istdaten-Tabelle "ConcatFacts", obwohl ich eigentlich nur die Plandaten DISTINCT daran "anhängen" wollte:

ConcatFacts:
LOAD * INLINE [
    CYear, CProduct, CQuantity
    2013, ProductA, 500
    2013, ProductA, 500 
    2013, ProductB, 300
];

//Append Plan Data
concatenate(ConcatFacts)
LOAD distinct * INLINE [
    CYear, CProduct, Plan
    2013,ProductA, 900
    2013,ProductA, 900
    2013,ProductB, 200
]; 
 
Ist-Quantity von ProduktA ist nach CONCATENATE & DISTINCT nur 500

Auch hier hilft wieder der Umweg über eine temporäre "DISTINCT"-Tabelle
FixConcatFacts:
LOAD * INLINE [
    CCYear, CCProduct, CCQuantity
    2013, ProductA, 500
    2013, ProductA, 500 
    2013, ProductB, 300
];

//Append Plan Data
Tmp:
LOAD distinct * INLINE [
    CCYear, CCProduct, CCPlan
    2013,ProductA, 900
    2013,ProductA, 900
    2013,ProductB, 200
];

Concatenate(FixConcatFacts)
load
*
Resident Tmp;

drop table Tmp;


Quantity 1000 für ProductA als korrektes Ergebnis


Zuletzt hatten wir dieses "Phänomen" am MasterSummit for QlikView diskutiert. Das Verhalten ist zumindest seit QlikView 8.* (meiner ersten QlikView Version) konsistent. Falls Sie diese Eigenschaft von DISTINCT noch nicht kannten, und Ihnen in Ihren Skripten Datenzeilen abgehen, durchsuchen Sie Ihre QlikView Applikationen nach den beschriebenen Mustern! Das komplette Beispiel zu diesem Blogeintrag findet sich unter http://content.heldendaten.eu/DistinctVerhalten.zip




Auf in eine transparente Zukunft

Vor einigen Wochen hatte ich die Anfrage alle "zukünftigen" Balken in einem gestapelten Balkendiagramm transparent darzustellen. Konkret ging es um Lagerbewegungen die bis in die Zukunft projiziert werden. Die Transparenz soll dem Anwender helfen zu sehen, ab welchem Monat es sich um "Forecast"-Zahlen handelt.

Aufgrund der Vielzahl an Produkten war es nicht gewünscht bereits im Skript Farben für jedes Produkt zu hinterlegen. Deswegen wollten wir die Standardfarben von QlikView benutzen.

Die Aufgabenstellung: Ab November (aktuelles Monat) die Balken transparent darstellen)

Mein erster Ansatz funktionierte, verlangt aber viele unschöne Stringoperationen:

if(Month >= month(today()),
 argb(120,
   mid(color(rowno()),5,3),
   mid(color(rowno()),9,3),
   mid(color(rowno()),13,3)
    )
 ,
 color(rowno())
)

Aus der aktuellen Farbe des Balkens color(rowno()) schneidet die Formel die Stellen für RGB aus, und füge Sie mit Transparenz 120 wieder in die ARGB()-Formel ein.

Farben in QlikView

Für eine bessere Lösung muss man sich zuerst klar werden, wie Farben in QlikView funktionieren. Jede Farbfunktion in QlikView liefert eine Zahl zurück. Ruft man etwa =num(black()) auf, bekommt man folgenden dezimalen Output:

Was auf den ersten Blick nach einer zufälligen Dezimalzahl aussieht, ist bei näherer Betrachtung eine Bit-Darstellung einer ARBG-Farbe. QlikView bietet fertige Funktionen um Zahlen in beliebigen Zahlenbasen darzustellen. Mit der Funktion =num(black(),'(hex)') bekommt man den aussagekräftigeren Hexadezimalen-Wert für die Farbe Schwarz: ff000000!

QlikView liefert also für jede Farbe einen ARGB-Wert zurück. Insgesamt haben wir als für jede Farbinformation 1 Byte = 8 Bit zur Verfügung. black() ist also nichts anderes als Rot=0, Grün=0, Blau=0 mit einer vollen Deckkraft von A=FF(hex)


Die Farbe Schwarz binär dargestellt liefert QlikView mit der Funktion =num(black(),'(bin)').
Man erinnere sich an seine Informatik-Ausbildung: 1Hex-Stelle sind 4 bit!

 

Transparent rechnen

Mit diesem Wissen lässt sich nun die Transparenz viel einfacher manipulieren.  Um den Transparentwert beispielsweise zu "dreivierteln" könnte man das 30. bit auf 0 setzen. Um das zu erreichen, kann man von der Farbe  2 hoch 30 abziehen: =num(black() - pow(2,30),'(bin)')

Zurückkommend auf die ursprüngliche Fragestellung, kann man jetzt komplett ohne String-Operationen auskommen. Die Formel um die Balken halb-transparent darzustellen lautet nun

if(Month >= month(today()),
 color(rowno()) - pow(2,31)
 ,
 color(rowno())
)

Das fertige Chart

Download Beispiel

Qv.exe /nodata als Shortcut am Desktop

Arbeitet man mit großen QlikView Applikationen, kann das Öffnen einer .qvw von der Festplatte schon mal etwas dauern. Will man aber nur schnell das Skript nachlesen, oder eine komplizierte Formel (bzw. ein ganzes Objekt) kopieren, ist es oft gar nicht notwendig die .qvw mit gesamten Daten zu öffnen.

Hat man die QlikView Applikation unter den "Zuletzt geöffneten Dateien" auf der Startseite gelistet, kann man mittels Rechtsklick|'Applikation ohne Daten öffnen' wählen.
Hat man die .qvw jedoch nicht in der Startliste, so müßte man kompliziert die Qv.exe mit dem Commandlineparameter /nodata benutzen. Wem das zu kompliziert ist, kann sich einen entsprechenden Shortcut am Desktop anlegen. Einfach den Pfad

                                        "C:\Program Files\QlikView\Qv.exe" /nodata


als Shortcut anlegen. Wenn man nun mittels Drag&Drop die .qvw auf den Shortcut fallen läßt, dann öffnet der QlikView Developer die Applikation ohne Daten. Die Applikation zeigt nun alle Listboxen mit "nicht verfügbar" an, aber das Skript und die Objektdefinitionen sind schnell griffbereit.

Eine kleine Warnung: Speichern Sie bitte die .qvw nur mit großer Vorsicht, nachdem Sie diese mit /nodata geöffnet haben. Leider werden bei diesem Feature nämlich keine Frontend-Variablen mitgeladen. Speichern Sie also die .qvw, dann gehen diese Variablen verloren. Gleiche Problematik hat übrigens auf das "-prj"-Ordner Feature für SVN/Team Foundation Server Integration! Als Workaround hilft nur, alle Variablen bereits im Skript zu definieren!

Update QV11.20SR4 12.11.2013
Öffnet man eine QlikView Applikation mit /nodata in  QV11.20 SR4 12129 werden Variablen nun korrekt mitgeladen. In den Release-Notes konnte ich zwar keine Anmerkung finden, aber wie der Screenshot unterhalb zeigt, sind bei meinem Test alle 3 Frontend-Variablen befüllt (nur das Tabellendiagramm kann natürlich nicht dargestellt werden, weil ja keine Daten geladen wurden). Zum Vergleich: QV10SR4 zeigt leere Variablendefinitionen.



Für den -prj-Folder konnte ich keine Änderung feststellen, also hier ist die Warnung weiterhin aufrecht!









QlikView Master Summit - Drop Fields Diskussion

Rob Wunderlich hat sich in seinem aktuellen Blogpost einer unserer Diskussionen vom MasterSummit for QlikView in Barcelona angenommen.

Falls Sie Rob's Document Analyzer benutzen, um Felder am Ende des Skripts zu droppen, hier nochmals meine Beobachtung.

Attached a small example that shows the issue:

- 01_testDatabase.qvw produces a .qvw with about 350MB on disk
- 02_testDatabaseBinaryDrop400fields.qvw does a BINARY Load and then drops 400 fields --> it's about 18 MB on disk (136 MB in Memory including QV.exe)

So one would expect that 18 MB is the final size of the .qvw!
However when I do an additional RESIDENT-Load after dropping fields, the .qvw gets much smaller:

- 03_testDatabaseBinaryDrop400fields_Resident does a BINARY Load, then drops 400 fields and THEN makes a RESIDENT LOAD on the table, dropping the original table

--> suddenly the .qvvw is only 1.2 MB on disk (26 MB in Memory including qv.exe)!
Download Example

Rob hat dies nun weiter analysiert und bemerkt, dass die  Record pointers nicht freigegeben werden.  Erst mit einem weiteren RESIDENT-Load wird tatsächlich der komplette DISK/MEMORY-Space freigegeben.

  • Im Moment ist es besser die nicht benutzen Felder im Skript auszukommentieren, anstatt Sie erst am Ende des Skripts zu droppen!
  • Falls dies nicht möglich ist, sollte man nach den Drop Fields Statement die (Fakten)-Tabelle nochmals resident laden.


heldendaten GmbH,2020