2015-06-02 10 views
8

ggplot2 scheint keine eingebaute Methode zu haben, um mit Überlappungen für Text auf scatter plots umzugehen. Ich habe jedoch eine andere Situation, in der die Bezeichnungen auf einer diskreten Achse stehen, und ich frage mich, ob jemand hier eine bessere Lösung hat als das, was ich gemacht habe.Umgang mit ggplot2 und überlappenden Beschriftungen auf einer diskreten Achse

Einige Codebeispiel:

library(ggplot2) 

#some example data 
test.data = data.frame(text = c("A full commitment's what I'm thinking of", 
           "History quickly crashing through your veins", 
           "And I take A deep breath and I get real high", 
           "And again, the Internet is not something that you just dump something on. It's not a big truck."), 
         mean = c(3.5, 3, 5, 4), 
         CI.lower = c(4, 3.5, 5.5, 4.5), 
         CI.upper = c(3, 2.5, 4.5, 3.5)) 

#plot 
ggplot(test.data, aes_string(x = "text", y = "mean")) + 
    geom_point(stat="identity") + 
    geom_errorbar(aes(ymax = CI.upper, ymin = CI.lower), width = .1) + 
    scale_x_discrete(labels = test.data$text, name = "") 

enter image description here

So sehen wir, dass die x-Achsenbeschriftungen auf der jeweils anderen sind. Zwei Lösungen kommen mir in den Sinn: 1) Abkürzungen der Etiketten und 2) Hinzufügen von Zeilenumbrüchen zu den Etiketten. In vielen Fällen (1) wird es reichen, aber in einigen Fällen ist dies nicht möglich. Also schrieb ich eine Funktion zum Hinzufügen von Zeilenumbrüchen (\n) alle n-ten Zeichen an die Saiten, um überlappende Namen zu vermeiden:

library(ggplot2) 

#Inserts newlines into strings every N interval 
new_lines_adder = function(test.string, interval){ 
    #length of str 
    string.length = nchar(test.string) 
    #split by N char intervals 
    split.starts = seq(1,string.length,interval) 
    split.ends = c(split.starts[-1]-1,nchar(test.string)) 
    #split it 
    test.string = substring(test.string, split.starts, split.ends) 
    #put it back together with newlines 
    test.string = paste0(test.string,collapse = "\n") 
    return(test.string) 
} 

#a user-level wrapper that also works on character vectors, data.frames, matrices and factors 
add_newlines = function(x, interval) { 
    if (class(x) == "data.frame" | class(x) == "matrix" | class(x) == "factor") { 
    x = as.vector(x) 
    } 

    if (length(x) == 1) { 
    return(new_lines_adder(x, interval)) 
    } else { 
    t = sapply(x, FUN = new_lines_adder, interval = interval) #apply splitter to each 
    names(t) = NULL #remove names 
    return(t) 
    } 
} 

#plot again 
ggplot(test.data, aes_string(x = "text", y = "mean")) + 
    geom_point(stat="identity") + 
    geom_errorbar(aes(ymax = CI.upper, ymin = CI.lower), width = .1) + 
    scale_x_discrete(labels = add_newlines(test.data$text, 20), name = "") 

Und der Ausgang ist: enter image description here

Dann kann man einige Zeit verbringen spielen mit die Intervallgröße, um zu vermeiden, dass zwischen den Etiketten zu viel Leerraum ist.

Wenn die Anzahl der Etiketten variiert, ist diese Art der Lösung nicht so gut, da sich die optimale Intervallgröße ändert. Da die normale Schriftart nicht mono-beabstandet ist, wirkt sich der Text der Beschriftungen auch auf die Breite aus, und daher muss man bei der Auswahl eines guten Intervalls besondere Vorsicht walten lassen (dies kann man vermeiden, indem man eine Monospace-Schriftart verwendet) , aber sie sind extra breit). Schließlich ist die new_lines_adder() Funktion dumm, dass es Wörter in zwei auf dumme Weisen teilt, die Menschen nicht tun würden. Z.B. in dem oben genannten spaltete es "Atem" in "Brust". Man könnte es umschreiben, um dieses Problem zu vermeiden.

Man kann auch die Schriftgröße verringern, aber dies ist ein Kompromiss mit der Lesbarkeit und oft ist die Verringerung der Schriftgröße unnötig.

Was ist der beste Weg, um diese Art von Etikettenüberladung zu behandeln?

+0

Ich beschäftige mich normalerweise mit überlappenden Labels, indem ich sie rotiere: '+ theme (axis.text.x = element_text (angle = 60, hjust = 1))' (aber es ist nicht ideal, wenn sie sehr lang sind, da es a erzeugt großer Rand) – scoa

Antwort

0

Aufbauend auf @Stibu Antwort und Kommentar berücksichtigt diese Lösung die Anzahl der Gruppen und nutzt die von Stibu entwickelte intelligente Aufteilung, während ein Fix für durch einen Schrägstrich getrennte Wörter hinzugefügt wird.

Funktionen:

#Inserts newlines into strings every N interval 
new_lines_adder = function(x, interval) { 
    #add spaces after/
    x = str_replace_all(x, "/", "/ ") 
    #split at spaces 
    x.split = strsplit(x, " ")[[1]] 
    # get length of snippets, add one for space 
    lens <- nchar(x.split) + 1 
    # now the trick: split the text into lines with 
    # length of at most interval + 1 (including the spaces) 
    lines <- cumsum(lens) %/% (interval + 1) 
    # construct the lines 
    x.lines <- tapply(x.split, lines, function(line) 
    paste0(paste(line, collapse=" "), "\n"), simplify = TRUE) 
    # put everything into a single string 
    result <- paste(x.lines, collapse="") 
    #remove spaces we added after/
    result = str_replace_all(result, "/ ", "/") 
    return(result) 
} 

#wrapper for the above, meant for users 
add_newlines = function(x, total.length = 85) { 
    # make sure, x is a character array 
    x = as.character(x) 
    #determine number of groups 
    groups = length(x) 
    # apply splitter to each 
    t = sapply(x, FUN = new_lines_adder, interval = round(total.length/groups), USE.NAMES=FALSE) 
    return(t) 
} 

habe ich versucht, einige Werte für die Standardeingabe und 85 ist der Wert, für den der Text Ergebnis für die Beispieldaten anständig. Alle höheren und "Venen" in Label 2 werden nach oben bewegt und kommen dem dritten Label zu nahe.

Hier ist, wie es aussieht:

enter image description here

Dennoch wäre es besser, ein echtes Maß für die gesamte Textbreite zu verwenden, nicht viele Zeichen wie auf dieser Proxy verlassen zu müssen bedeutet in der Regel, dass die Etiketten viel Platz verschwenden. Vielleicht könnte man new_lines_adder() mit etwas Code basierend auf strwidth umschreiben, um mit dem Problem der ungleiche Breite der Zeichen umzugehen.

Ich lasse diese Frage unbeantwortet, falls jemand einen Weg finden kann, dies zu tun.

Ich habe die beiden Funktionen zu my personal package on github hinzugefügt, damit jeder, der sie benutzen will, sie von dort holen kann.

4

Ich habe versucht, zusammen, um eine andere Version von new_lines_adder zu setzen:

new_lines_adder = function(test.string, interval) { 
    #split at spaces 
    string.split = strsplit(test.string," ")[[1]] 
    # get length of snippets, add one for space 
    lens <- nchar(string.split) + 1 
    # now the trick: split the text into lines with 
    # length of at most interval + 1 (including the spaces) 
    lines <- cumsum(lens) %/% (interval + 1) 
    # construct the lines 
    test.lines <- tapply(string.split,lines,function(line) 
     paste0(paste(line,collapse=" "),"\n"),simplify = TRUE) 
    # put everything into a single string 
    result <- paste(test.lines,collapse="") 
    return(result) 
} 

Es Linien nur in Räumen teilt und stellt sicher, dass die Linien höchstens die Anzahl der Zeichen von interval gegeben enthalten. Damit sieht Ihr Grundstück wie folgt:

enter image description here

würde ich nicht behaupten, dass dies der beste Weg zu sein. Es ignoriert weiterhin, dass nicht alle Zeichen dieselbe Breite haben. Vielleicht kann mit strwidth etwas Besseres erreicht werden.

Durch die Art und Weise: Sie können add_newlines an folgende Vereinfachung:

add_newlines = function(x, interval) { 

    # make sure, x is a character array 
    x = as.character(x) 
    # apply splitter to each 
    t = sapply(x, FUN = new_lines_adder, interval = interval,USE.NAMES=FALSE) 
    return(t) 
} 

Am Anfang as.character sicher macht Sie eine Zeichenfolge haben. Es tut auch nicht weh, das zu tun, wenn Sie bereits eine Zeichenkette haben, so dass die if Klausel nicht benötigt wird.

Auch die nächste if-Klausel ist unnötig: Sapply funktioniert perfekt, wenn x nur ein Element enthält.Und Sie können die Namen unterdrücken, indem Sie USE.NAMES=FALSE so einstellen, dass Sie die Namen in einer zusätzlichen Zeile nicht entfernen müssen.

+0

Die passende Zahl scheint um 72 zu sein. – Deleet

+1

Ich bin nicht sicher, dass ich verstehe, was Sie meinen. Ist 72 (Zeichen) die Gesamtbreite, die alle Etiketten zusammen haben sollten? Bis jetzt haben Sie 4 * 20 = 80 verwendet, was vernünftig erscheint. Was Sie tun könnten, ist, dass 'add_newlines' so umgeschrieben wird, dass es die Gesamtlänge aller Labels annimmt und diese Zahl dann durch die Anzahl der Labels dividiert. Also würden Sie 'add_newlines (test.data $ text, 80)' aufrufen, die dann viermal 'new_lines_adder (x, 80/4)' aufrufen würde. – Stibu

+0

Es ist, weil ich Ziffern (123456789) verwendet habe, um die Zahl zu schätzen, und die Zahlen sind breiter als die Buchstaben (z.B. etaoinshr [9 am häufigsten in Englisch]), so dass das Ergebnis etwas kleiner war. Gute Idee mit der automatischen Behandlung der Anzahl der Gruppen in der 'new_lines_adder()'. Ich werde diesen Ansatz versuchen. Man kann auch den Standardwert für "add_newlines()" auf 80 setzen, da dies zwischen den Plots nicht variieren sollte (hoffe ich!). – Deleet

Verwandte Themen