Como Joseph R. mencionou, há o antigo clássico gnuplot
, além de uma série de ofertas mais modernas que podem criar todos os tipos de gráficos em vários formatos, tanto baseados em vetores quanto bitmap. Mas esses programas tendem a ser tão versáteis que leva um bom tempo para aprender a usá-los adequadamente, o que pode ser um pouco assustador quando você quer traçar rapidamente um simples gráfico de linhas.
Eu venho me ensinando sobre o formato SVG recentemente, criando pequenos arquivos SVG manualmente e também escrevendo programas em Python para desenhar várias coisas usando SVG, então quando eu vi a sua pergunta eu pensei que era uma oportunidade perfeita para fazer um pouco mais Prática de programação SVG. :)
Aqui está um programa em bruto do Python que faz um gráfico de linha simples a partir de dados no formato dado na questão. A saída está no formato SVG, impressa em stdout, portanto, você precisará usar o redirecionamento para salvá-la em um arquivo. Os dados de entrada são lidos a partir do nome do arquivo especificado na linha de comando, mas se nenhum nome de arquivo for fornecido, o programa lê seus dados de stdin, para que o programa possa ser usado em um pipeline.
Os dados de entrada podem conter linhas em branco ou linhas de comentário com #
como o primeiro caractere não em branco. Os valores X e Y em cada linha devem ser separados por pelo menos um caractere de espaço em branco (as guias são ok), outro espaço em branco em uma linha é ignorado, portanto, espaços antes do valor X ou após o valor Y são ignorados. >
O programa varre todos os dados XY para encontrar os valores máximo e mínimo, que são usados para calcular a viewBox do SVG para que a plotagem seja dimensionada e centralizada corretamente.
SVGgraph.py
#! /usr/bin/env python
''' Create a simple line graph as an SVG file
Written by PM 2Ring 2014.11.09
'''
import sys
def bounding_box(points):
xtup, ytup = zip(*points)
xlo = min(xtup)
xhi = max(xtup)
ylo = min(ytup)
yhi = max(ytup)
return xlo, ylo, xhi - xlo, yhi - ylo
def points_to_SVG(points, width, height):
#Get graph bounds & adjust to allow for a margin
xlo, ylo, xsize, ysize = bounding_box(points)
margin = 0.02
xmargin = xsize * margin
ymargin = ysize * margin
xlo -= xmargin
xsize += 2 * xmargin
ylo -= ymargin
ysize += 2 * ymargin
strokewidth = 2.0 * min(xsize, ysize) / float(max(width, height))
head = '''<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"
width="%d" height="%d" viewBox="%f %f %f %f"
preserveAspectRatio="xMidYMid meet">\n\n''' % (width, height, xlo, ylo, xsize, ysize)
body = ' <polyline points="\n' + '\n'.join(["%f, %f" % t for t in points]) + '\n"\n'
tail = 'style="fill:none; stroke-width:%f; stroke:#006600;"/>\n</svg>' % strokewidth
return head + body + tail
def main():
iname = sys.argv[1] if len(sys.argv) > 1 else None
f = open(iname, 'rt') if iname else sys.stdin
data = f.read().splitlines()
if iname is not None:
f.close()
points = []
for line in data:
#Skip blank lines
if not line: continue
x, y = line.split()
#Skip comments: lines which have '#' as the first non-blank char
if x.startswith('#'): continue
points.append((float(x), float(y)))
width, height = 800, 600
print points_to_SVG(points, width, height)
if __name__ == '__main__':
main()
Veja um exemplo de saída:
graphtest.svg
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="800" height="600" viewBox="-0.240855 -3.881333 12.524483 7.762666"
preserveAspectRatio="xMidYMid meet">
<polyline points="
0.000000, 0.000000
0.523599, 3.732051
1.047198, 2.598076
1.570796, -0.500000
2.094395, -0.866025
2.617994, 0.267949
3.141593, 0.000000
3.665191, -0.267949
4.188790, 0.866025
4.712389, 0.500000
5.235988, -2.598076
5.759587, -3.732051
6.283185, -0.000000
6.806784, 3.732051
7.330383, 2.598076
7.853982, -0.500000
8.377580, -0.866025
8.901179, 0.267949
9.424778, 0.000000
9.948377, -0.267949
10.471976, 0.866025
10.995574, 0.500000
11.519173, -2.598076
12.042772, -3.732051
"
style="fill:none; stroke-width:0.019407; stroke:#006600;"/>
</svg>
FWIW, aqui está o programa que usei para gerar os dados de teste para esse SVG.
SVGgraph-points.py
#! /usr/bin/env python
''' Create a list of points to test SVGgraph.py with
Written by PM 2Ring 2014.11.09
'''
import sys
from math import pi, sin
def f(x):
return sin(x) + 2.0 * sin(x * 2.0) + 1.5 * sin(x * 3.0)
def make_points(n):
points = n * [None]
for i in xrange(n):
x = 4.0 * pi * i / n
y = f(x)
points[i] = (x, y)
return points
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 24
points = make_points(n)
print '\n'.join(["%f %f" % t for t in points])
if __name__ == '__main__':
main()
Uso
python SVGgraph-points.py 24 > testdata
python SVGgraph.py testdata > graphtest.svg
ou
python SVGgraph-points.py | python SVGgraph.py > graphtest.svg
Dê ao SVGgraph-points.py um argumento de 200 ou mais para criar um gráfico suave.
Como eu disse acima, este é apenas um roteiro grosseiro; Eu não senti vontade de adicionar manuseio sofisticado de linha de comando. :)
Você pode desejar modificar os parâmetros width
e height
, no script Python ou no SVG, mas eles não são essenciais, pois os programas de exibição SVG geralmente permitem controlar a escala enquanto visualiza imagens. E mesmo que você edite esses valores no arquivo SVG, a imagem será sempre centralizada e dimensionada adequadamente para que nenhuma peça seja cortada.
Você também pode experimentar o fator de escala margin
, atualmente definido como 0,02, que determina a margem mínima em torno do gráfico. Você pode controlar a espessura (nominal) da linha plotada ajustando o strokewidth
multiplicador, que atualmente é definido como 2.0.
Divirta-se!
Editar
Aqui está uma nova versão do script gráfico que usa um sistema de coordenadas convencional, em vez do sistema invertido que o SVG (e muitos outros sistemas de desenho de computador) usa. Então agora seus gráficos não estarão de cabeça para baixo. :)
#! /usr/bin/env python
''' Create a simple line graph as an SVG file
Uses a conventional coordinate system,
not the usual inverted SVG system.
Written by PM 2Ring 2014.11.11
'''
import sys
def bounding_box(points):
xtup, ytup = zip(*points)
xlo = min(xtup)
xhi = max(xtup)
ylo = min(ytup)
yhi = max(ytup)
return xlo, ylo, xhi, yhi
def points_to_SVG(points, width, height):
#Get graph bounds & adjust to allow for a margin
xlo, ylo, xhi, yhi = bounding_box(points)
xsize = xhi - xlo
ysize = yhi - ylo
margin = 0.02
xmargin = xsize * margin
ymargin = ysize * margin
xlo -= xmargin
xsize += 2 * xmargin
yhi += ymargin
ysize += 2 * ymargin
strokewidth = 2.0 * min(xsize, ysize) / float(max(width, height))
head = '''<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="%d" height="%d" viewBox="%f %f %f %f"
preserveAspectRatio="xMidYMid meet">
<polyline style="fill:none; stroke-width:%f; stroke:#006600;"
transform="scale(1, -1)"
points="\n''' % (width, height, xlo, -yhi, xsize, ysize, strokewidth)
body = '\n'.join(["%f, %f" % t for t in points])
tail = '\n"/>\n</svg>'
return head + body + tail
def main():
iname = sys.argv[1] if len(sys.argv) > 1 else None
f = open(iname, 'rt') if iname else sys.stdin
data = f.read().splitlines()
if iname is not None:
f.close()
points = []
for line in data:
#Skip blank lines
if not line: continue
x, y = line.split()
#Skip comments: lines which have '#' as the first non-blank char
if x.startswith('#'): continue
points.append((float(x), float(y)))
width, height = 800, 600
print points_to_SVG(points, width, height)
if __name__ == '__main__':
main()