español - Gráficos de estilo xkcd en MATLAB
plot matlab español (4)
Tan talentosas personas han descubierto cómo hacer gráficos de estilo xkcd en Mathematica , en LaTeX , en Python y en R ya.
¿Cómo se puede usar MATLAB para producir un gráfico que se parece al de arriba?
Lo que he intentado
Creé líneas onduladas, pero no pude obtener hachas onduladas. La única solución en la que pensé fue sobreescribirlos con líneas onduladas, pero quiero poder cambiar los ejes reales. Tampoco pude hacer funcionar la fuente Humor, el código utilizado fue:
annotation(''textbox'',[left+left/8 top+0.65*top 0.05525 0.065],...
''String'',{''EMBARRASSMENT''},...
''FontSize'',24,...
''FontName'',''Humor'',...
''FitBoxToText'',''off'',...
''LineStyle'',''none'');
Para la línea ondulada, experimenté agregando un pequeño ruido aleatorio y suavizando:
smooth(0.05*randn(size(x)),10)
Pero no pude hacer el fondo blanco que aparece a su alrededor cuando se cruzan ...
De acuerdo, aquí está mi intento menos crudo, pero todavía no del todo:
%# init
%# ------------------------
noise = @(x,A) A*randn(size(x));
ns = @(x,A) A*ones(size(x));
h = figure(2); clf, hold on
pos = get(h, ''position'');
set(h, ''position'', [pos(1:2) 800 450]);
blackline = {
''k'', ...
''linewidth'', 2};
axisline = {
''k'', ...
''linewidth'', 3};
textprops = {
''fontName'',''Comic Sans MS'',...
''fontSize'', 14,...
''lineWidth'',3};
%# Plot data
%# ------------------------
x = 1:0.1:10;
y0 = sin(x).*exp(-x/30) + 3;
y1 = sin(x).*exp(-x/3) + 3;
y2 = 3*exp(-(x-7).^6/.05) + 1;
y0 = y0 + noise(x, 0.01);
y1 = y1 + noise(x, 0.01);
y2 = y2 + noise(x, 0.01);
%# plot
plot(x,y0, ''color'', [0.7 0.7 0.7], ''lineWidth'',3);
plot(x,y1, ''w'',''lineWidth'',7);
plot(x,y1, ''b'',''lineWidth'',3);
plot(x,y2, ''w'',''lineWidth'',7);
plot(x,y2, ''r'',''lineWidth'',3);
%# text
%# ------------------------
ll(1) = text(1.3, 4.2,...
{''Walking back to my''
''front door at night:''});
ll(2) = text(5, 0.7, ''yard'');
ll(3) = text(6.2, 0.7, ''steps'');
ll(4) = text(7, 0.7, ''door'');
ll(5) = text(8, 0.7, ''inside'');
set(ll, textprops{:});
%# arrows & lines
%# ------------------------
%# box around "walking back..."
xx = 1.2:0.1:3.74;
yy = ns(xx, 4.6) + noise(xx, 0.007);
plot(xx, yy, blackline{:})
xx = 1.2:0.1:3.74;
yy = ns(xx, 3.8) + noise(xx, 0.007);
plot(xx, yy, blackline{:})
yy = 3.8:0.1:4.6;
xx = ns(yy, 1.2) + noise(yy, 0.007);
plot(xx, yy, blackline{:})
xx = ns(yy, 3.74) + noise(yy, 0.007);
plot(xx, yy, blackline{:})
%# left arrow
x_arr = 1.2:0.1:4.8;
y_arr = 0.65 * ones(size(x_arr)) + noise(x_arr, 0.005);
plot(x_arr, y_arr, blackline{:})
x_head = [1.1 1.6 1.62];
y_head = [0.65 0.72 0.57];
patch(x_head, y_head, ''k'')
%# right arrow
x_arr = 8.7:0.1:9.8;
y_arr = 0.65 * ones(size(x_arr)) + noise(x_arr, 0.005);
plot(x_arr, y_arr, blackline{:})
x_head = [9.8 9.3 9.3];
y_head = [0.65 0.72 0.57];
patch(x_head, y_head, ''k'')
%# left line on axis
y_line = 0.8:0.1:1.1;
x_line = ns(y_line, 6.5) + noise(y_line, 0.005);
plot(x_line, y_line, blackline{:})
%# right line on axis
y_line = 0.8:0.1:1.1;
x_line = ns(y_line, 7.2) + noise(y_line, 0.005);
plot(x_line, y_line, blackline{:})
%# axes
x_xax = x;
y_xax = 0.95 + noise(x_xax, 0.01);
y_yax = 0.95:0.1:5;
x_yax = x(1) + noise(y_yax, 0.01);
plot(x_xax, y_xax, axisline{:})
plot(x_yax, y_yax, axisline{:})
% finalize
%# ------------------------
xlim([0.95 10])
ylim([0 5])
axis off
Resultado:
Cosas para hacer:
- Encuentra mejores funciones (mejor defínelas por partes)
- Agregue "anotaciones" y líneas onduladas a las curvas que describen
- Encuentra una fuente mejor que Comic Sans!
- Generalice todo en una función
plot2xkcd
para que podamos convertir cualquier gráfico / figura al estilo xkcd.
El primer paso ... encuentre la fuente del sistema que le guste (use la función listfonts
para ver qué hay disponible) o instale una que coincida con el estilo de escritura a mano de xkcd . Encontré una fuente TrueType del tipo "Humor Sans" del usuario ch00f mencionada en esta publicación del blog , y la usaré para mis ejemplos posteriores.
Como lo veo, generalmente necesitará tres objetos gráficos modificados diferentes para hacer este tipo de gráficos: un objeto de ejes , un objeto de línea y un objeto de texto . También es posible que desee que un objeto de anotación facilite las cosas, pero por el momento lo he dejado en claro, ya que podría ser más difícil de implementar que los tres objetos anteriores.
Creé funciones de envoltorio que crearon los tres objetos, anulando ciertas configuraciones de propiedad para hacerlos más parecidos a xkcd. Una limitación es que los nuevos gráficos que producen no se actualizarán en ciertos casos (como los cuadros de delimitación en los objetos de texto al cambiar el tamaño de los ejes), pero esto podría explicarse con una implementación más completa orientada a objetos que implique la herencia del controlador. clase , uso de eventos y escuchas , etc. Por ahora, aquí están mis implementaciones más simples:
xkcd_axes.m:
function hAxes = xkcd_axes(xkcdOptions, varargin)
hAxes = axes(varargin{:}, ''NextPlot'', ''add'', ''Visible'', ''off'', ...
''XLimMode'', ''manual'', ''YLimMode'', ''manual'');
axesUnits = get(hAxes, ''Units'');
set(hAxes, ''Units'', ''pixels'');
axesPos = get(hAxes, ''Position'');
set(hAxes, ''Units'', axesUnits);
xPoints = round(axesPos(3)/10);
yPoints = round(axesPos(4)/10);
limits = [xlim(hAxes) ylim(hAxes)];
ranges = [abs(limits(2) - limits(1)) abs(limits(4) - limits(3))];
backColor = get(get(hAxes, ''Parent''), ''Color'');
xColor = get(hAxes, ''XColor'');
yColor = get(hAxes, ''YColor'');
line(''Parent'', hAxes, ''Color'', xColor, ''LineWidth'', 3, ...
''Clipping'', ''off'', ...
''XData'', linspace(limits(1), limits(2), xPoints), ...
''YData'', limits(3) + rand(1, xPoints).*0.005.*ranges(2));
line(''Parent'', hAxes, ''Color'', yColor, ''LineWidth'', 3, ...
''Clipping'', ''off'', ...
''YData'', linspace(limits(3), limits(4), yPoints), ...
''XData'', limits(1) + rand(1, yPoints).*0.005.*ranges(1));
xTicks = get(hAxes, ''XTick'');
if ~isempty(xTicks)
yOffset = limits(3) - 0.05.*ranges(2);
tickIndex = true(size(xTicks));
if ismember(''left'', xkcdOptions)
tickIndex(1) = false;
xkcd_arrow(''left'', [limits(1) + 0.02.*ranges(1) xTicks(1)], ...
yOffset, xColor);
end
if ismember(''right'', xkcdOptions)
tickIndex(end) = false;
xkcd_arrow(''right'', [xTicks(end) limits(2) - 0.02.*ranges(1)], ...
yOffset, xColor);
end
plot([1; 1]*xTicks(tickIndex), ...
0.5.*[-yOffset; yOffset]*ones(1, sum(tickIndex)), ...
''Parent'', hAxes, ''Color'', xColor, ''LineWidth'', 3, ...
''Clipping'', ''off'');
xLabels = cellstr(get(hAxes, ''XTickLabel''));
for iLabel = 1:numel(xLabels)
xkcd_text(xTicks(iLabel), yOffset, xLabels{iLabel}, ...
''HorizontalAlignment'', ''center'', ...
''VerticalAlignment'', ''middle'', ...
''BackgroundColor'', backColor);
end
end
yTicks = get(hAxes, ''YTick'');
if ~isempty(yTicks)
xOffset = limits(1) - 0.05.*ranges(1);
tickIndex = true(size(yTicks));
if ismember(''down'', xkcdOptions)
tickIndex(1) = false;
xkcd_arrow(''down'', xOffset, ...
[limits(3) + 0.02.*ranges(2) yTicks(1)], yColor);
end
if ismember(''up'', xkcdOptions)
tickIndex(end) = false;
xkcd_arrow(''up'', xOffset, ...
[yTicks(end) limits(4) - 0.02.*ranges(2)], yColor);
end
plot(0.5.*[-xOffset; xOffset]*ones(1, sum(tickIndex)), ...
[1; 1]*yTicks(tickIndex), ...
''Parent'', hAxes, ''Color'', yColor, ''LineWidth'', 3, ...
''Clipping'', ''off'');
yLabels = cellstr(get(hAxes, ''YTickLabel''));
for iLabel = 1:numel(yLabels)
xkcd_text(xOffset, yTicks(iLabel), yLabels{iLabel}, ...
''HorizontalAlignment'', ''right'', ...
''VerticalAlignment'', ''middle'', ...
''BackgroundColor'', backColor);
end
end
function xkcd_arrow(arrowType, xArrow, yArrow, arrowColor)
if ismember(arrowType, {''left'', ''right''})
xLine = linspace(xArrow(1), xArrow(2), 10);
yLine = yArrow + rand(1, 10).*0.003.*ranges(2);
arrowScale = 0.05.*ranges(1);
if strcmp(arrowType, ''left'')
xArrow = xLine(1) + arrowScale.*[0 0.5 1 1 1 0.5];
yArrow = yLine(1) + arrowScale.*[0 0.125 0.25 0 -0.25 -0.125];
else
xArrow = xLine(end) - arrowScale.*[0 0.5 1 1 1 0.5];
yArrow = yLine(end) + arrowScale.*[0 -0.125 -0.25 0 0.25 0.125];
end
else
xLine = xArrow + rand(1, 10).*0.003.*ranges(1);
yLine = linspace(yArrow(1), yArrow(2), 10);
arrowScale = 0.05.*ranges(2);
if strcmp(arrowType, ''down'')
xArrow = xLine(1) + arrowScale.*[0 0.125 0.25 0 -0.25 -0.125];
yArrow = yLine(1) + arrowScale.*[0 0.5 1 1 1 0.5];
else
xArrow = xLine(end) + arrowScale.*[0 -0.125 -0.25 0 0.25 0.125];
yArrow = yLine(end) - arrowScale.*[0 0.5 1 1 1 0.5];
end
end
line(''Parent'', hAxes, ''Color'', arrowColor, ''LineWidth'', 3, ...
''Clipping'', ''off'', ''XData'', xLine, ''YData'', yLine);
patch(''Parent'', hAxes, ''FaceColor'', arrowColor, ...
''EdgeColor'', arrowColor, ''LineWidth'', 2, ''Clipping'', ''off'', ...
''XData'', xArrow + [0 rand(1, 5).*0.002.*ranges(1)], ...
''YData'', yArrow + [0 rand(1, 5).*0.002.*ranges(2)]);
end
end
xkcd_text.m:
function hText = xkcd_text(varargin)
hText = text(varargin{:});
set(hText, ''FontName'', ''Humor Sans'', ''FontSize'', 13, ...
''FontWeight'', ''normal'');
backColor = get(hText, ''BackgroundColor'');
edgeColor = get(hText, ''EdgeColor'');
if ~strcmp(backColor, ''none'') || ~strcmp(edgeColor, ''none'')
hParent = get(hText, ''Parent'');
extent = get(hText, ''Extent'');
nLines = size(get(hText, ''String''), 1);
extent = extent + [-0.5 -0.5 1 1].*0.25.*extent(4)./nLines;
yPoints = 5*nLines;
xPoints = round(yPoints*extent(3)/extent(4));
noiseScale = 0.05*extent(4)/nLines;
set(hText, ''BackgroundColor'', ''none'', ''EdgeColor'', ''none'');
xBox = [linspace(extent(1), extent(1) + extent(3), xPoints) ...
extent(1) + extent(3) + noiseScale.*rand(1, yPoints) ...
linspace(extent(1) + extent(3), extent(1), xPoints) ...
extent(1) + noiseScale.*rand(1, yPoints)];
yBox = [extent(2) + noiseScale.*rand(1, xPoints) ...
linspace(extent(2), extent(2) + extent(4), yPoints) ...
extent(2) + extent(4) + noiseScale.*rand(1, xPoints) ...
linspace(extent(2) + extent(4), extent(2), yPoints)];
patch(''Parent'', hParent, ''FaceColor'', backColor, ...
''EdgeColor'', edgeColor, ''LineWidth'', 2, ''Clipping'', ''off'', ...
''XData'', xBox, ''YData'', yBox);
hKids = get(hParent, ''Children'');
set(hParent, ''Children'', [hText; hKids(hKids ~= hText)]);
end
end
xkcd_line.m:
function hLine = xkcd_line(xData, yData, varargin)
yData = yData + 0.01.*max(range(xData), range(yData)).*rand(size(yData));
line(xData, yData, varargin{:}, ''Color'', ''w'', ''LineWidth'', 8);
hLine = line(xData, yData, varargin{:}, ''LineWidth'', 3);
end
Y aquí hay una secuencia de comandos de muestra que utiliza estos para recrear el cómic anterior. ginput
las líneas usando ginput
para marcar puntos en la trama con el mouse, capturándolos y luego representándolos como quería:
xS = [0.0359 0.0709 0.1004 0.1225 0.1501 0.1759 0.2219 0.2477 0.2974 0.3269 0.3582 0.3895 0.4061 0.4337 0.4558 0.4797 0.5074 0.5276 0.5589 0.5810 0.6013 0.6179 0.6271 0.6344 0.6381 0.6418 0.6529 0.6713 0.6842 0.6934 0.7026 0.7118 0.7265 0.7376 0.7560 0.7726 0.7836 0.7965 0.8149 0.8370 0.8573 0.8867 0.9033 0.9346 0.9659 0.9843 0.9936];
yS = [0.2493 0.2520 0.2548 0.2548 0.2602 0.2629 0.2629 0.2657 0.2793 0.2657 0.2575 0.2575 0.2602 0.2629 0.2657 0.2766 0.2793 0.2875 0.3202 0.3856 0.4619 0.5490 0.6771 0.7670 0.7970 0.8270 0.8433 0.8433 0.8243 0.7180 0.6199 0.5272 0.4510 0.4128 0.3392 0.2711 0.2275 0.1757 0.1485 0.1131 0.1022 0.0858 0.0858 0.1022 0.1267 0.1567 0.1594];
xF = [0.0304 0.0488 0.0727 0.0967 0.1335 0.1630 0.2090 0.2348 0.2698 0.3011 0.3269 0.3545 0.3803 0.4153 0.4466 0.4724 0.4945 0.5110 0.5350 0.5516 0.5608 0.5700 0.5755 0.5810 0.5884 0.6013 0.6179 0.6363 0.6492 0.6584 0.6676 0.6731 0.6842 0.6860 0.6934 0.7007 0.7136 0.7265 0.7394 0.7560 0.7726 0.7818 0.8057 0.8444 0.8794 0.9107 0.9475 0.9751 0.9917];
yF = [0.0804 0.0940 0.0967 0.1049 0.1185 0.1458 0.1512 0.1540 0.1649 0.1812 0.1812 0.1703 0.1621 0.1594 0.1703 0.1975 0.2411 0.3065 0.3801 0.4782 0.5708 0.6526 0.7452 0.8106 0.8324 0.8488 0.8433 0.8270 0.7888 0.7343 0.6826 0.5981 0.5300 0.4782 0.3910 0.3420 0.2847 0.2248 0.1621 0.0995 0.0668 0.0395 0.0232 0.0177 0.0204 0.0232 0.0259 0.0204 0.0232];
xE = [0.0267 0.0488 0.0856 0.1409 0.1759 0.2164 0.2514 0.3011 0.3269 0.3637 0.3969 0.4245 0.4503 0.4890 0.5313 0.5608 0.5939 0.6344 0.6694 0.6934 0.7192 0.7394 0.7523 0.7689 0.7891 0.8131 0.8481 0.8757 0.9070 0.9346 0.9604 0.9807 0.9936];
yE = [0.0232 0.0232 0.0232 0.0259 0.0259 0.0259 0.0313 0.0259 0.0259 0.0259 0.0368 0.0395 0.0477 0.0586 0.0777 0.0886 0.1213 0.1730 0.2466 0.2902 0.3638 0.5082 0.6499 0.7916 0.8924 0.9414 0.9550 0.9387 0.9060 0.8760 0.8542 0.8379 0.8188];
hFigure = figure(''Position'', [300 300 700 450], ''Color'', ''w'');
hAxes = xkcd_axes({''left'', ''right''}, ''XTick'', [0.45 0.60 0.7 0.8], ...
''XTickLabel'', {''YARD'', ''STEPS'', ''DOOR'', ''INSIDE''}, ...
''YTick'', []);
hSpeed = xkcd_line(xS, yS, ''Parent'', hAxes, ''Color'', [0.5 0.5 0.5]);
hFear = xkcd_line(xF, yF, ''Parent'', hAxes, ''Color'', [0 0.5 1]);
hEmb = xkcd_line(xE, yE, ''Parent'', hAxes, ''Color'', ''r'');
hText = xkcd_text(0.27, 0.9, ...
{''WALKING BACK TO MY''; ''FRONT DOOR AT NIGHT:''}, ...
''Parent'', hAxes, ''EdgeColor'', ''k'', ...
''HorizontalAlignment'', ''center'');
hSpeedNote = xkcd_text(0.36, 0.35, {''FORWARD''; ''SPEED''}, ...
''Parent'', hAxes, ''Color'', ''k'', ...
''HorizontalAlignment'', ''center'');
hSpeedLine = xkcd_line([0.4116 0.4282 0.4355 0.4411], ...
[0.3392 0.3256 0.3038 0.2820], ...
''Parent'', hAxes, ''Color'', ''k'');
hFearNote = xkcd_text(0.15, 0.45, {''FEAR''; ''THAT THERE''''S''; ...
''SOMETHING''; ''BEIND ME''}, ...
''Parent'', hAxes, ''Color'', ''k'', ...
''HorizontalAlignment'', ''center'');
hFearLine = xkcd_line([0.1906 0.1998 0.2127 0.2127 0.2201 0.2256], ...
[0.3501 0.3093 0.2629 0.2221 0.1975 0.1676], ...
''Parent'', hAxes, ''Color'', ''k'');
hEmbNote = xkcd_text(0.88, 0.45, {''EMBARRASSMENT''}, ...
''Parent'', hAxes, ''Color'', ''k'', ...
''HorizontalAlignment'', ''center'');
hEmbLine = xkcd_line([0.8168 0.8094 0.7983 0.7781 0.7578], ...
[0.4864 0.5436 0.5872 0.6063 0.6226], ...
''Parent'', hAxes, ''Color'', ''k'');
Y (trompetas) aquí está la trama resultante:
En lugar de volver a implementar todas las diversas funciones de trazado, quería crear una herramienta genérica que pudiera convertir cualquier trazado existente en un trazado de estilo xkcd.
Este enfoque significa que puede crear gráficos y estilizarlos utilizando las funciones estándar de MATLAB y luego, cuando haya terminado, puede volver a renderizar el gráfico en un estilo xkcd mientras conserva el estilo general del gráfico.
Ejemplos
Trama
Bar & Parcela
Caja y parcela
Cómo funciona
La función funciona iterando sobre los hijos de un eje. Si los hijos son de tipo line
o patch
, los distorsiona ligeramente. Si el hijo es de tipo hggroup
, itera en los hggroup
del grupo hggroup
. Tengo planes de admitir otros tipos de gráficos, como la image
, pero no está claro cuál es la mejor manera de distorsionar la imagen para tener un estilo xkcd.
Finalmente, para garantizar que las distorsiones se vean uniformes (es decir, las líneas cortas no se distorsionan más que las largas), mido la longitud de la línea en píxeles y luego la muestra es proporcional a su longitud. Luego agrego ruido a cada muestra Nth que produce líneas que tienen más o menos la misma cantidad de distorsión.
El código
En lugar de pegar varios cientos de líneas de código, simplemente lo vincularé a una parte de la fuente . Además, el código fuente y el código para generar los ejemplos anteriores están disponibles gratuitamente en GitHub .
Como puede ver en los ejemplos, todavía no distorsiona los ejes, aunque planeo implementarlo tan pronto como descubra la mejor manera de hacerlo.
Veo dos formas de resolver esto: la primera es agregar un poco de jitter a las coordenadas x / y de las características de la trama. Esto tiene la ventaja de que puede modificar fácilmente una trama, pero tiene que dibujar los ejes por sí mismo si desea tenerlos xkcdyfied (consulte la solución de @Rody Oldenhuis ). La segunda forma es crear un gráfico que no esté nervioso y usar imtransform
para aplicar una distorsión aleatoria a la imagen. Esto tiene la ventaja de que puede usarlo con cualquier gráfico, pero terminará con una imagen, no con un gráfico editable.
Primero mostraré el # 2, y mi intento en el # 1 a continuación (si te gusta el # 1 mejor, ¡mira la solución de Rody !).
Esta solución se basa en dos funciones clave: EXPORT_FIG del intercambio de archivos para obtener una captura de pantalla con IMTRANSFORM e IMTRANSFORM para obtener una transformación.
%# define plot data
x = 1:0.1:10;
y1 = sin(x).*exp(-x/3) + 3;
y2 = 3*exp(-(x-7).^2/2) + 1;
%# plot
fh = figure(''color'',''w'');
hold on
plot(x,y1,''b'',''lineWidth'',3);
plot(x,y2,''w'',''lineWidth'',7);
plot(x,y2,''r'',''lineWidth'',3);
xlim([0.95 10])
ylim([0 5])
set(gca,''fontName'',''Comic Sans MS'',''fontSize'',18,''lineWidth'',3,''box'',''off'')
%# add an annotation
annotation(fh,''textarrow'',[0.4 0.55],[0.8 0.65],...
''string'',sprintf(''text%shere'',char(10)),''headStyle'',''none'',''lineWidth'',1.5,...
''fontName'',''Comic Sans MS'',''fontSize'',14,''verticalAlignment'',''middle'',''horizontalAlignment'',''left'')
%# capture with export_fig
im = export_fig(''-nocrop'',fh);
%# add a bit of border to avoid black edges
im = padarray(im,[15 15 0],255);
%# make distortion grid
sfc = size(im);
[yy,xx]=ndgrid(1:7:sfc(1),1:7:sfc(2));
pts = [xx(:),yy(:)];
tf = cp2tform(pts+randn(size(pts)),pts,''lwm'',12);
w = warning;
warning off images:inv_lwm:cannotEvaluateTransfAtSomeOutputLocations
imt = imtransform(im,tf);
warning(w)
%# remove padding
imt = imt(16:end-15,16:end-15,:);
figure(''color'',''w'')
imshow(imt)
Aquí está mi intento inicial de jittering
%# define plot data
x = 1:0.1:10;
y1 = sin(x).*exp(-x/3) + 3;
y2 = 3*exp(-(x-7).^2/2) + 1;
%# jitter
x = x+randn(size(x))*0.01;
y1 = y1+randn(size(x))*0.01;
y2 = y2+randn(size(x))*0.01;
%# plot
figure(''color'',''w'')
hold on
plot(x,y1,''b'',''lineWidth'',3);
plot(x,y2,''w'',''lineWidth'',7);
plot(x,y2,''r'',''lineWidth'',3);
xlim([0.95 10])
ylim([0 5])
set(gca,''fontName'',''Comic Sans MS'',''fontSize'',18,''lineWidth'',3,''box'',''off'')