- Säulendiagramm, mit dynamischen Säulen
- Säulendiagramm, mit SVG
- Skalierung in D3
- Ordinal Scale und RangeBande für die x-Achse
- Color Scale
- Quantitative Scale
- Abstände
- Scatter-Plot
- Achsen
- Interaktive Charts
- Transitions
var dataset = [5, 10, 15, 17, 25];
d3.select('body').selectAll('div')
.data(dataset)
.enter()
.append('div')
.attr('class','bar')
.style('height', function(d){
return d*5+'px';
});Der obige Code erzeugt ein simples Säulendiagramm basierend auf dem dataset-Array.
selectAll('div'): liefert ein Array von allen DIVs die gefunden werden können. Sind keine DIVs innerhalb des body-elements ist das Array leer.
data(dataSet, keyFunction): definiert zum einen das dataSet-Array, zum anderen die keyFunction (optional).
keyFunction Die keyFunction bestimmt die Keys für die einzelnen Elemente des dataSet-Arrays. WIrd keine keyFunction angegeben, so sind die Keys automatisch die Indices des dataSet-Arrays. Für [5, 10, 15, 17, 25] wären es [0, 1, 2, 3, 4]. Danach werden die dataSet-Keys mit den Keys der Elemente der selectAll-Funktion verglichen. Alle data-keys die unterschiedlich sind, werden dann Teil der enter-Selection und in der enter()-Funktion aufgerufen. Wären etwa 2 DIVs unterhalb vom body-Element vorhanden, dann bekämen diese die Indices [0,1]. Dann würde [0,1] mit [0,1,2,3,4] verglichen werden und nur die Datensets mit Index [2,3,4] in die Enter-Selection aufgenommen werden.
enter().append('div'): Die enter().append() Funktion erzeugt dann so viele neue Nodes, wie es Elemente in der Enter-Selection gibt. In diesem Fall werden neue DIVs erzeugt.
Siehe: http://knowledgestockpile.blogspot.co.at/ https://www.youtube.com/watch?v=OZXYk_bgQGQ (Enter, Update and Exit)
.attr('class', 'bar'): damit erzeugt man ein Attribut für jedes Element. Z.B.: eine Klasse,...
.style('height', val): jedem Element kann man nun styles geben.
z.B.: würde .style('height', '30px') jedem DIV eine Höhe von 30px geben. Das funktioniert zwar, ist in den meisten Fällen nicht sehr nützlich, weil man ja die Daten des Datensets darstellen möchte.
Das macht man indem man den String in eine Funktion ändert, der die einzelnen Daten-Werte d übergeben werden:
.style('height', function(d){
return d * 5 + 'px';
});Dadurch erhält jede Säule ihre entsprechende Höhe aus dem Datensatz.
###2. Säulendiagramm, mit SVG###
Folgender Sourcecode zeigt das Säulendiagramm in SVG implementiert:
var dataset = [5, 10, 15, 17, 25];
var svg = d3.select('#chartArea').append('svg')
.attr('width', 400)
.attr('height', 300); // bei SVG braucht man keine px angeben.
var multiplier = 9; // multiplier
svg.selectAll('rect') // rect anstelle div
.data(dataset)
.enter()
.append('rect') // rect anstelle div
.attr('class','bar')
.attr('x', function(d, i){ // der 2. Parameter in den attr-Funktionen liefert den INDEX des Elements im Dataset.
return i * 22; // Breite der Säule + bißchen mehr
})
.attr('y', function(d){
return 300 - d * multiplier; // richtet die Säulen von unten des SVG-Elements aus.
})
.attr('width', 20)
.attr('height', function(d){ // 'height' ist in SVG ein attr und kein style
return d * multiplier; // keine 'px'-Angabe notwendig in SVG
});Notiz: multiplier dient dazu die Säulen skalieren zu können, um z.B. das ganze SVG-Element nutzen zu können. Das ist allerdings nicht die beste Art und Weise. Siehe Kapitel 3.
return 300 - d * multiplier; 300 ist höhe des SVG-ELEMENTS, d * multiplier ist Höhe der Säule
Styles für SVG-Elemente definiert man so wie bei HTML per CSS. Allerdings gibt es kleine Unterschiede. So definiert man etwa keine background-color sondern fill .
.bar{
display:inline-block;
width: 20px;
height:75px; // wird von D3 überschrieben
fill: teal;
}d3.scale WIKI: Skalierung in D3
Scales sind Funktionen die ein Mapping machen von einer input-domain auf eine output-range.
Es gibt unterschiedliche Typen von Scales. Quantitative scales haben eine kontinuierliche domain, z.B.: ein Set von Zahlen, Daten. Ordinal scales haben eine diskrete domain, z.B.: ein Set von Namen oder Kategorien.
Beispiele:
d3.scale.linear()
d3.scale.ordinal()
d3.time.scale()
Scales sind optional, man muß sie nicht verwenden. Nur muß man dann die Mathematik dahinter selber machen.
Beispiel:
var dataset = [5, 10, 15, 17, 25];
var w = 400, h = 300;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w)
.attr('height', h);
var yScale = d3.scale.linear() // SCALE: nimmt einen Wert von einer Input-DOMAIN und mappt ihn auf einen neuen Wert einer Output-RANGE
.domain([0,25]) // unser Dataset wird auf eine Domain von min 0- 25 max gemappt und je nachdem wo der neue Wert liegt,...
.range([0,h]); // ...wird er auf unsere RANGE gemappt. 0 bis zur Höhe
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('class','bar')
.attr('x', function(d, i){
return i * 22;
})
.attr('y', function(d){
return h - yScale(d);
})
.attr('width', 20)
.attr('height', function(d){
return yScale(d);
});Erklärung: Die Höhe und die y-Koordinate der Säulen wird nun durch die yScale-function bestimmt. Die yScale Function mappt die Werte des datasets LINEAR auf eine DOMAIN und die erhaltenen Werte wiederum auf die angegebene Range.
Problem: Falls das Datenset größere Werte enthält als man in der DOMAIN angegeben hat, werden die Säulen abgeschnitten. Um das zu verhindern, kann man die obere Grenze der DOMAIN von D3 berechnen lassen:
d3.max(dataset) // bestimmt den größten Wert im Dataset-Array
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)]) // die Domain wird nun dynamisch angepasst.
.range([0,h]); Dynamische Farben
Den Säulen kann man dynamisch, abhängig von ihren Werten im Dataset, eine Farbe zuweisen. Das kann man auch mit scale.linear() machen:
var color = d3.scale.linear()
.domain([0,d3.max(dataset)])
.range(["red","green"]);
So setzt man die "fill"-Farbe abhängig von der linearen Scale:
.attr('fill',function(d){
return color(d);
})
Für ein ordinal skalierbares Merkmal bestehen Rangordnungen der Art „größer“, „kleiner“, „mehr“, „weniger“, „stärker“, „schwächer“ zwischen je zwei unterschiedlichen Merkmalswerten (z. B. x > y > z). Über die Abstände zwischen diesen benachbarten Urteilsklassen ist jedoch nichts ausgesagt. Meist handelt es sich um qualitative Merkmale, wie z. B. der in der Frage gesuchte „höchste erreichbare Bildungsabschluss“. Ein weiteres Beispiel sind die Schulnoten: Note 1 ist besser als Note 2, man hat aber keine Auskunft darüber, ob der Unterschied zwischen Note 1 und 2 gleich groß ist wie der zwischen Note 3 und Note 4. Eine Sonderform der Ordinalskala ist die Rangskala. Hierbei kann jeder Wert nur einmal vergeben werden. Beispiele hierfür sind die Erreichung von Rängen im Sport.
Beispiel:
var dataset = [5, 10, 15, 17, 22];
var w = 400, h = 300;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w)
.attr('height', h);
var xScale = d3.scale.ordinal() // die x-Achse
.domain(dataset) // In unserem Fall ist die DOMAIN das Dataset an sich. Dadurch erzeugt es für jedes Element im Dataset ein Element in der Domain.
.rangeBands([0,w],0.25,0); // [0,w] = Renderbereich; 0.25 = verhältnis balkenbreite/abstand; 0= Außenabstand
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)])
.range([0,h]);
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('class','bar')
.attr('x', function(d){
return xScale(d); // x-Koordinate wird von xScale() berechnet.
})
.attr('y', function(d){
return h - yScale(d);
})
.attr('width', xScale.rangeBand()) // Balkenbreite wird von xScale.rangeBand() berechnet.
.attr('height', function(d){
return yScale(d);
});rangeBands: Wenn man ein Diagramm baut, möchte man die Balken auf der x-Achse gleich verteilt haben. Man könnte das manuell berechnen (verfügbarer Platz, Balkenbreite, Paddings,..), oder man verwendet rangeBands
Es unterteilt den verfügbaren Platz in gleiche Bereiche.
Siehe: RangeBands WIKI
var dataset = _.map(_.range(50), function(i){ // erzeugt ein array aus 50 Zufalls-Zahlen zwischen 0-500
return Math.random() *500;
});
var w = 400, h = 300;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w)
.attr('height', h);
var xScale = d3.scale.ordinal()
.domain(dataset)
.rangeBands([0,w],0.25,0);
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)])
.range([0,h]);
var colorScale = d3.scale.linear() // Die Balken werden linear gemappt, und...
.domain([0, d3.max(dataset)]) // ... bekommen abhängig vom Datensatzwert...
.range(['orange', 'green']); // eine Farbe zwischen orange und grün.
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('class','bar')
.attr('x', function(d){
return xScale(d);
})
.attr('y', function(d){
return h - yScale(d);
})
.attr('fill', colorScale) //Kurzschreibweise anstatt: function(d){return colorScale(d);}
.attr('width', xScale.rangeBand())
.attr('height', function(d){
return yScale(d);
});
var dataset = _.map(_.range(50), function(i){
return Math.random() *500;
});
var w = 400, h = 300;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w)
.attr('height', h);
var xScale = d3.scale.ordinal()
.domain(dataset)
.rangeBands([0,w],0.25,0);
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)])
.range([0,h]);
var colorScale = d3.scale.linear()
.domain([0, dataset.length]) // Die Domain muß von 0 bis Anzahl der Elemente gehen
.range(['orange', 'green']); // Dadurch kann man die Balken anhand des INDEX einfärben
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('class','bar')
.attr('x', function(d){
return xScale(d);
})
.attr('y', function(d){
return h - yScale(d);
})
.attr('fill', function(d,i){ // zweites attribut liefert den INDEX des Wertes
return colorScale(i); // die colorScale function ruft man mit dem INDEX auf
})
.attr('width', xScale.rangeBand())
.attr('height', function(d){
return yScale(d);
});
d3.scale.quantize() Interpoliert die Farben nicht, sondern teilt die Balken in 2 Farbbereiche auf, oder mehr, je nachdem wieviele Farben im range([])-Array definiert sind
var colorScale = d3.scale.quantize()
.domain([0, dataset.length])
.range(['orange', 'blue']);
d3.scale.quantile() Dadurch bekommt man mehr Einstellmöglichkeiten. Diese Einstellmöglichkeiten erhält man indem man die domain() modifiziert:
var colorScale = d3.scale.quantile()
.domain([0, dataset.length / 4, dataset.length])
.range(['orange', 'blue', 'red', 'yellow']);
Obige domain ist in 2 Teile unterteilt. Der erste Teil sind die ersten 25% des Datensets. Der zweite Teil sind die restlichen 75%. Die Farben sind entsprechend dieser Aufteilung verteilt. Die ersten 2 Farben sind der ersten Domain zugeordnet (dem ersten Viertel). Die letzten 2 Farben sind dem zweiten Teil der Domain zugeordnet. (den 75%)
Ein realistisches Beispiel wäre etwa Ausreißer hervorzuheben/verbergen:
var colorScale = d3.scale.quantile()
.domain([0, 10, dataset.length-10, dataset.length]) // Die ersten und letzten 10 Elemente sind unsere "Ausreißer"
.range(['red','green', 'blue', 'red']); // die Ausreißer werden rot dargestellt. Grün, Blau,.. visualisieren den bereich dazwischen
Oft sollen die Diagramme nicht das gesamte SVG füllen, weil man noch Platz für Text oder Achsen braucht. Darum braucht man Abstände (margins)
Folgende Ansatz funktioniert gut, um in D3 Abstände zu definieren:
- man definiert ein margin-object mit left, right, top, bottom
- man subtrahiert von w und h die margins
- man versetzt (translate) das Diagramm um den Betrag des margin-top und margin-left
var margin = {top:20, right:20, bottom:20, left:20 };
var w = 400 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w + margin.left + margin.right )
.attr('height', h + margin.top + margin.bottom )
.append('g') // man arbeitet dadurch nicht mehr direkt am SVG-Element. Aber das ist egal.
.attr('transform', 'translate(' + margin.left + ', ' + margin.top +')' );
Um mit Scatter Plot arbeiten zu können, ändern wir das Dataset so um, dass es Objekte enthält. Die Objekte bestehn aus den Koordinaten und Radius: (cx,cy,r)
var dataset = _.map(_.range(100), function(i){ // erzeugt 100 objekte mit Zufallswerten für x,y,r
return {
x : Math.random() * 100,
y : Math.random() * 100,
r : Math.random() * 30
};
});
var margin = {top:50, right:50, bottom:50, left:50 };
var w = 400 - margin.left - margin.right,
h = 300 - margin.top - margin.bottom;
var svg = d3.select('#chartArea').append('svg')
.attr('width', w + margin.left + margin.right )
.attr('height', h + margin.top + margin.bottom )
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top +')' );
var xScale = d3.scale.linear() // für scatter-plot sind xScale und yScale wieder linear
.domain([0,100]) // die domain kann man manuell setzen...
.range([0,w]);
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset, function(d){ // aber auch bei Objekten kann man auf die d3.max function zurückgreifen
return d.y; // nur muß man dann angeben, von welchem wert man das maximum berechnen möchte.
})])
.range([0,h]);
svg.selectAll('circle') // circle statt rechtecke
.data(dataset)
.enter()
.append('circle') // circle statt rechtecke
.attr('class','bubble') // für die kreise gibts eine eigene css klasse
.attr('cx', function(d){
return xScale(d.x); // kreise brauchen eine x-koordinate
})
.attr('cy', function(d){
return yScale(d.y); // kreise brauchen eine y-koordinate
})
.attr('r', function(d){
return d.r; // kreise brauchen einen radius
});
Das SVG Koordinatensystem hat links oben seinen Ursprung.
Um links unten zu beginnen muß man die Range-Werte bei der yScale tauschen: .range([h,0]);
Folgender Code erzeugt eine x-Achse und eine y-Achse
var xAxis = d3.svg.axis() // erzeugt die Achse
.scale(xScale) // eine Scale muß übergeben werden
.orient('bottom') // Unten ausgerichtet
.ticks(4) // ungefähre Anzahl der Stichmarker,
.innerTickSize(3) // gr
.outerTickSize(1) // Achsen-Dicke
.tickPadding(10); // Abstand der Zahlen zur Achse
svg.append('g')
.attr('class','axis') // Klasse für Strich-farbe,....
.attr('transform','translate(0,' + (h + 10) +')') // um noch etwas extra -abstand zu bekommen
.call(xAxis); // mit Hilfe der call-Methode muß man nicht alles aneinanderketten
var yAxis = d3.svg.axis() // erzeugt die y-Achse
.scale(yScale) // yScale
.orient('left'); // links ausgerichtet
svg.append('g')
.attr('class','axis') // Klasse für Strich-farbe,....
.attr('transform','translate(-20,0)') // etwas nach links verschieben
.call(yAxis); Die Achsen können auch gestylt werden. Schriftgröße, Tick-Linien,...
Tick-Linien, Font-size ändert man etwa so:
.axis text {
font: 12px sans-serif;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}Im Wesentlichen gehts um (mouseover,mouseout,...)
TIPP: Bei interaktiven Style-Änderungen, sollte man nicht die styles direkt ändern, sondern nur eine css-klasse ändern. Das Styling an sich sollte im CSS-File gemacht werden. z.B.: "active" Klasse bei hover.
svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('class','bubble')
.attr('cx', function(d){
return xScale(d.x);
})
.attr('cy', function(d){
return yScale(d.y);
})
.attr('r', function(d){
return d.r;
})
.on('mouseover', function(d){
d3.select(this).classed('active',true); // fügt eine klasse "active" hinzu
})
.on('mouseout', function(d){
d3.select(this).classed('active',false); // entfernt sie wieder
})
.on('mousedown', function(d){
d3.select(this).attr('r', d.r*2); // ändert den radius
})
.on('mouseup', function(d){
d3.select(this).attr('r', d.r); // ändert den radius wieder zurück auf den Standard-wert
})
;Im CSS
.bubble.active{
fill:rgba(220,30,30,0.4);
stroke:red;
}Ein Klick auf einen Button: <button onclick="update()">Update</button>
ruft update() auf.
update() ändert die Daten in unserem Dataset. (mit der _.each()-Funktion )
Um die Änderungen auch wirklich sehen zu können, muß man den Chart re-rendern.
Man muß eine Referenz zu all den Kreisen bekommen und alle Properties ändern.
Das ist sehr einfach und geht mit derselectAll()-Methode, die wir bei der Erstellung der Kreise bereits benutzt haben.
Transition: Um einen animierten Übergang zu erhalten, bedient man sich der sehr einfachen
D3-Methode transition().
function update(){ // wird aufgerufen wenn der Button geklickt wird
_.each(dataset,function(datum){ // ändert die Werte der x,y,r Eigenschaft aller Kreise im Datenset,
datum.x = Math.round(Math.random()*100);
datum.y = Math.round(Math.random()*100);
datum.r = Math.round(5 + Math.random() * 10);
});
svg.selectAll('circle') // muß ausgeführt werden, um den Chart mit den neuen Werten zu rendern
.transition() // erzeugt einen animierten Übergang. Alle Properties werden animiert.
.duration(500) // 500ms Dauer.
.delay(300) // delay(330) Verzögerung für die Gesamte Animation.
.ease('elastic') // elastic=bounce-Effect,
.attr('cx', function(d){
return xScale(d.x);
})
.attr('cy', function(d){
return yScale(d.y);
})
.attr('r', function(d){
return d.r;
});
}TIPP1: Übergibt man nicht den Wert in ms, sondern eine function, dann kann man den delay pro index machen:
.delay(function(d,i){ // jedes Element im Datensatz wird nacheinander animiert.
return i * 100;
})TIPP2: Man kann auch die Properties nacheinander animieren. Man braucht nur vor jeder attr()-Methode die man animieren möchte,
eine transition() hinzufügen:
svg.selectAll('circle')
.transition() // Animation für 'cx'
.duration(500)
.delay(300)
.ease('elastic')
.attr('cx', function(d){
return xScale(d.x);
})
.transition() // Animation für 'cy'
.duration(500)
.attr('cy', function(d){
return yScale(d.y);
})
.transition() // Animation für 'r'
.duration(500)
.attr('r', function(d){
return d.r;
})
.style('fill', 'purple'); // man kann auch Farben animieren. Die Farbe ändert sich hier gleichzeitig mit dem Radius.
}Das Ergebnis von z.B.: svg.selectAll('circle') nennt man das Selektions-Objekt (selection-object).
Das Selektion-Objekt hat die Methode call(), mit der man den Code flexibler machen kann und besser organisieren kann.
Zur Erklärung von call() trennen wir den code für die transition heraus, und fügen sie in eine Funktion ein.
Diese Funktion können wir mit call() aufrufen.
Beispiel:
function steppedTransition(selection){ // unsere Transition Funktion. Als Argument erhält es das Selektion-Objekt
selection.transition()
.duration(500)
.delay(300)
.ease('elastic')
.attr('cx', function(d){
return xScale(d.x);
})
.transition()
.duration(500)
.attr('cy', function(d){
return yScale(d.y);
})
.transition()
.duration(500)
.attr('r', function(d){
return d.r;
})
.style('fill', 'purple');
}
function update(){
_.each(dataset,function(datum){
datum.x = Math.round(Math.random()*100);
datum.y = Math.round(Math.random()*100);
datum.r = Math.round(5 + Math.random() * 10);
});
svg.selectAll('circle').call(steppedTransition); // Die obige Transition-Funktion kann mit call() aufgerufen werden.
}Dadurch ist der Code besser aufgeteilt. Man kann nun mehrere Transition-Funktionen erstellen und diese wann auch immer aufrufen.
Folgendes Beispiel nutzt die filter()-Funktion um den Einsatz der call() Funktion zu verdeutlichen:
function update(){
_.each(dataset,function(datum){ // ändert die Werte x,y,r aller Elemente im Datensatz
datum.x = Math.round(Math.random()*100);
datum.y = Math.round(Math.random()*100);
datum.r = Math.round(5 + Math.random() * 10);
});
svg.selectAll('circle') // aktualisiert den Chart (Kreise)
.filter(function(d){ // mit filter() bestimmt man die Elemente, auf denen die Transition durchgeführt werden soll.
return d.x < 50; // in diesem Fall werden nur Elemente, deren x-koordinate klener als 50 ist, an die steppedTransition übergeben.
})
.call(steppedTransition);
}Im obigen Beispiel ändert die _.each Methode alle Elemente im Chart. Im folgenden Beispiel wird nicht mehr die underscore-each() Methode verwendet, sondern die D3.js-each() Methode.
function update(){
svg.selectAll('circle') // 1. Alle Kreise werden selektiert
.filter(function(d){
return d.x < 50; // 2. Nur Kreise deren x kleiner als 50 ist, gelangen zur nächsten Methode.
})
.each(function(datum){ // 3. alle die übrig sind, gelangen in diese each()-Methode
datum.x = Math.round(Math.random() * 100); // deren werte werden hier geändert
datum.y = Math.round(Math.random() * 100);
datum.r = Math.round(5 + Math.random() * 10);
})
.call(steppedTransition); // der Chart wird wieder aktualisiert und animiert.
}Tipp 1 : Man kann auch das Datenändern in eine neue (nicht anonyme) function auslagern:
function newData(datum){
datum.x = Math.round(Math.random()*100);
datum.y = Math.round(Math.random()*100);
datum.r = Math.round(5 + Math.random() * 10);
}
function update(){
svg.selectAll('circle')
.filter(function(d){
return d.x < 50;
})
.each(newData) // übersichtlicher wenn es nicht "inline" ist
.call(steppedTransition);
}Tipp 2: Mann kann unterschiedliche Animationen für verschiedene Daten haben:
function update(){
svg.selectAll('circle')
.filter(function(d){
return d.x < 50;
})
.each(newData) // übersichtlicher wenn es nicht "inline" ist
.call(steppedTransition);
svg.selectAll('circle') // ein zweiter Datensatz wird selektiert
.filter(function(d){ // mit Hilfe der Filter Funktion.
return d.x >= 50; // dieses Mal alle Elemente mit x >= 50
})
.each(newData) // übersichtlicher und auch wiederverwendbar!
.call(plainTransition, 1300); // für diese wird eine andere Animation angezeigt. Notiz: zusätzlich kann man Argumente übergeben, zb: Duration 1300ms
}Beispiel der plainTransition:
function plainTransition(selection, dur){ // man kann den Methoden, die mit call aufgerufen werden auch Argumente übergeben:
selection.transition()
.duration(dur) // duration
.delay(1300)
.attr('cx', function(d){
return xScale(d.x);
})
.attr('cy', function(d){
return yScale(d.y);
})
.attr('r', function(d){
return d.r;
})
.style('fill', 'red');
}<div id="chart"></div>
<button onclick="updateChart('Flugzeuge')">Flugzeuge</button>
<button onclick="updateChart('Autos')">Flugzeuge</button>
<button onclick="updateChart('Geld')">Flugzeuge</button>
<script src="bower_components/d3/d3.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/underscore/underscore-min.js"></script>
<script>
var w = 600;
var h = 400;
var path;
var subjects;
$.getJSON('./line-chart-data.json', function(json){
subjects = json;
// konvertiert den Datum-String in ein Datum-Objekt
_.keys(subjects).forEach(function(subject){ // _.keys liefert ["Autos", "Flugzeuge", "Geld"] zurück. foreach(d) lloopt also über die 3 Keys
subjects[subject].forEach(function(d){ // loopt über die Objekte des Datensatzes subject["Autos"], subject["Flugzeuge], subject["Geld"]
d.date = d3.time.format("%Y%m%d").parse(d.date); // der string "d" wird dann mit d3-Timeformater in ein Date-Objekt geändert.
});
});
path = d3 // danach wird der PATH erzeugt
.select('#chart')
.append('svg')
.attr('width', w)
.attr('height', h)
.append('g')
.append('path');
updateChart('Flugzeuge');
});
function updateChart(subject){
debugger;
var data = subjects[subject];
var dates = _.pluck(data, 'date'); // bringt die Date-properties in ihre eigene Collection
var counts = _.pluck(data, 'count'); // bringt die count-properties in ihre eigene Collection
var x = d3.time.scale() // wenn man mit Dates arbeitet, hilft time.scale(). Es werden alle Dates im Array 'dates' durchlaufen, und liefert anschließen ein Array zurück,
.domain(d3.extent(dates)) // Extent liefert ein Array zurück mit [min,max]-Wert
.range([0,w]);
var y = d3.scale.linear() // linear scale anstelle time-scale für y-achse
.domain(d3.extent(counts)) // wiederum "d3.extent" um den Wertbereich der counts zu erhalten.
.range([h,0]); // range ist größe des Charts
var line = d3.svg.line() // das line object wird erstellt
.interpolate('bundle') // interpolate('bundle') erzeugt weiche Kurven, weil des die Punkte interpoliert
.x(function(d){
return x(d.date); // x-property in dem man das datum zurück liefert
})
.y(function(d){
return y(d.count); // y-property in dem man das count zurück liefert
});
path // path objekt von oben
.datum(data) // im Gegensatz zu "data" erzeugt es keine "Selection"
.transition() // animiert die Chart-Änderungen
.duration(450)
.attr('d', line); // weist dem d Attribut die Linie zu. z.B.: d="M50,50 A30,30 0 0,1 35,20 M110,110 L100,0"
}
datum(data): Wenn man mehrere Shapes anhand von data erzeugt, wie beim Balkendiagramm, erzeugt man eine "Selection" In diesem Fall wollen wir aber die gesamte Collection auf das Object anwenden. Das macht man mit datum() Im Prinzip macht datum(data): "Benutz 'data' für das path-objekt"
###Flächendiagramm
Man kann ein Liniendiagramm sehr leicht in ein Flächendiagramm ändern. Man muß kleine CSS- Änderungen machen.
Im CSS muß das fill eine Farbe erhalten. Also von:
path
{
fill:none;
stroke:steelblue;
stroke-width:3px;
}
auf:
path
{
fill:lightblue;
}
Weiters muß man noch die line auf area ändern.
Flächendiagramme haben 2 y-Achses. (Oben,Unten) y0, y1
Also von:
var line = d3.svg.line()
.interpolate('bundle')
.x(function(d){
return x(d.date);
})
.y(function(d){
return y(d.count);
});Auf:
var area = d3.svg.area()
.interpolate('bundle')
.x(function(d){
return x(d.date);
})
.y0(function(d){
return y(0);
})
.y1(function(d){
return y(d.count);
});https://sarasoueidan.com/blog/svg-coordinate-systems/
invert() .invert()
Die invert() Funktion dreht die Scale einfach um: Für einen Wert in der range liefert die Funktion den entsprechenden Wert in der Domain z.B.:
var y=d3.scale.linear().domain([20,80]).range([0,120]);
y(50); // 60
y.invert(60); // 50Das ist sehr nützlich wenn z.B.: der Mauszeiger über den Chart bewegt wird und man wissen möchte welchen Wert der aktuellen Koordinate entspricht.
Einfache Beispiel: http://bl.ocks.org/mbostock/6123708
