Creating vector graphics with Python

When we deal with data and we want to plot them in graphs, sometimes we need to generate those graphs using a format that allows scaling to any resolution and without losing quality. If we also need some interactivity, being able to create graphical representations in SVG format is a great option.

Vector graphics are composed of text in XML, which allows any browser that respects W3C standards to render them without any problem.

Pandas and Pygal for SVG graphics in Python

For this demo, we are going to use a dataset that allows us to show different types of graphical representations of it. In this case, I will use a Pokémon dataset, where we will be able to see the characteristics of 800 Pokémon from 6 different generations.

We will use Pandas to create a dataframe with this structure, with which we will work in the following steps.

Pandas dataframe representing the data structure we are going to use.

To generate SVG graphics we will use the Python library Pygal.

Let’s start with something easy, we are going to make a simple graph to see the number of pokémon of each generation.

To do this, we will group by generation and we will get the number of pokémon of each one of them with this simple code.

pie_chart = pygal.Pie()
pie_chart.title = 'Pokemon by Generation'
for id, row in data.groupby(["Generation"]).size().reset_index().iterrows():
  pie_chart.add("Generation %s" % row["Generation"], row[0])
pie_chart.render_to_file('pie_chart_by_generation.svg')
Pokemon by Generation166391.4928844728903171.48743594049068106437.4147128241523284.8629929327153160362.8301650050641378.7126447794734121238.79006958495955356.81312547254106165203.08996784921055233.9325693087051882280.5752923931324153.02568661574566Pokemon by GenerationGeneration 1Generation 2Generation 3Generation 4Generation 5Generation 6

Now we can go a little deeper and make a plot that represents a larger group of data. We will show the distribution of the main attributes grouped by Pokémon type. To do this, we will generate a Box Plot type graph that will allow us to see in a simple way all these grouped data with the following code: 

for this_attribute in ["HP", "Attack", "Defense"]:
  box_plot = pygal.Box(box_mode="tukey")
  box_plot.title = 'Pokemon Attribute: %s' % this_attribute

  for this_type in data["Type 1"].unique():
    box_plot.add(this_type, data[data["Type 1"]==this_type]["HP"].values)
  
  box_plot.render_to_file('box_plot_attribute_%s.svg' % this_attribute)

This allows us to see the graphs of the three attributes separately.

Pokemon Attribute: HP002020404060608080100100120120140140160160180180200200220220240240Min: 30 Lower Whisker: 30 Q1: 50 Q2: 65.5 Q3: 75 Upper Whisker: 100 Max: 12320.342307692307692320.7541478129714Min: 38 Lower Whisker: 38 Q1: 58 Q2: 70 Q3: 80 Upper Whisker: 110 Max: 11543.996153846153845309.1349924585219Min: 20 Lower Whisker: 20 Q1: 51.5 Q2: 70 Q3: 90.5 Upper Whisker: 130 Max: 17067.65307.17119155354453Min: 1 Lower Whisker: 20 Q1: 45 Q2: 60 Q3: 70 Upper Whisker: 86 Max: 8691.30384615384614333.68250377073906Min: 30 Lower Whisker: 30 Q1: 55 Q2: 70 Q3: 90 Upper Whisker: 140 Max: 255114.9576923076923299.6432880844646Min: 35 Lower Whisker: 35 Q1: 50 Q2: 67.5 Q3: 80.5 Upper Whisker: 105 Max: 105138.6115384615385315.02639517345403Min: 20 Lower Whisker: 20 Q1: 50 Q2: 60 Q3: 70 Upper Whisker: 90 Max: 90162.26538461538462330.73680241327304Min: 10 Lower Whisker: 10 Q1: 54.5 Q2: 75 Q3: 92.5 Upper Whisker: 115 Max: 115185.91923076923078312.08069381598796Min: 35 Lower Whisker: 35 Q1: 54.75 Q2: 78 Q3: 91.25 Upper Whisker: 126 Max: 126209.57307692307694299.6432880844646Min: 30 Lower Whisker: 30 Q1: 50 Q2: 70 Q3: 79.5 Upper Whisker: 120 Max: 144233.2269230769231311.26244343891403Min: 20 Lower Whisker: 20 Q1: 50 Q2: 68 Q3: 80 Upper Whisker: 120 Max: 190256.88076923076926315.02639517345403Min: 30 Lower Whisker: 30 Q1: 50 Q2: 68.5 Q3: 80 Upper Whisker: 123 Max: 123280.53461538461534310.6078431372549Min: 20 Lower Whisker: 20 Q1: 45 Q2: 59.5 Q3: 64.5 Upper Whisker: 90 Max: 150304.18846153846147334.33710407239823Min: 36 Lower Whisker: 36 Q1: 53 Q2: 70 Q3: 90 Upper Whisker: 110 Max: 110327.84230769230766308.1530920060332Min: 41 Lower Whisker: 41 Q1: 67 Q2: 80 Q3: 102.5 Upper Whisker: 125 Max: 125351.4961538461538289.66063348416293Min: 35 Lower Whisker: 35 Q1: 50.5 Q2: 65 Q3: 74.25 Upper Whisker: 100 Max: 126375.15319.36312217194575Min: 40 Lower Whisker: 40 Q1: 57 Q2: 60 Q3: 75 Upper Whisker: 100 Max: 100398.8038461538461316.9901960784314Min: 40 Lower Whisker: 40 Q1: 59.5 Q2: 79 Q3: 82 Upper Whisker: 85 Max: 85422.45769230769224312.5716440422323Pokemon Attribute: HPGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying Pokemon Attribute: Attack002020404060608080100100120120140140160160180180Min: 27 Lower Whisker: 27 Q1: 55 Q2: 70 Q3: 94 Upper Whisker: 132 Max: 13220.342307692307692259.6093117408907Min: 30 Lower Whisker: 30 Q1: 61.5 Q2: 84.5 Q3: 102 Upper Whisker: 160 Max: 16043.996153846153845233.253036437247Min: 10 Lower Whisker: 10 Q1: 53 Q2: 72 Q3: 92 Upper Whisker: 150 Max: 15567.65260.0485829959514Min: 10 Lower Whisker: 10 Q1: 45 Q2: 65 Q3: 90 Upper Whisker: 155 Max: 18591.30384615384614265.31983805668017Min: 5 Lower Whisker: 10 Q1: 55 Q2: 70.5 Q3: 85 Upper Whisker: 130 Max: 160114.9576923076923271.68927125506076Min: 43 Lower Whisker: 43 Q1: 60 Q2: 74 Q3: 91 Upper Whisker: 106 Max: 106138.6115384615385261.3663967611336Min: 30 Lower Whisker: 30 Q1: 52.5 Q2: 65 Q3: 85 Upper Whisker: 123 Max: 123162.26538461538462269.49291497975713Min: 40 Lower Whisker: 40 Q1: 72 Q2: 85 Q3: 122 Upper Whisker: 180 Max: 180185.91923076923078206.45748987854256Min: 20 Lower Whisker: 20 Q1: 43.75 Q2: 52 Q3: 74 Upper Whisker: 80 Max: 131209.57307692307694307.16042510121457Min: 35 Lower Whisker: 35 Q1: 80 Q2: 100 Q3: 120 Upper Whisker: 145 Max: 145233.2269230769231214.8036437246964Min: 20 Lower Whisker: 20 Q1: 45 Q2: 57 Q3: 96.25 Upper Whisker: 165 Max: 190256.88076923076926257.3031376518219Min: 40 Lower Whisker: 40 Q1: 59.5 Q2: 95 Q3: 120.5 Upper Whisker: 165 Max: 165280.53461538461534214.8036437246964Min: 30 Lower Whisker: 30 Q1: 52.5 Q2: 66 Q3: 93.5 Upper Whisker: 120 Max: 165304.18846153846147266.6376518218624Min: 30 Lower Whisker: 30 Q1: 50 Q2: 67 Q3: 90 Upper Whisker: 130 Max: 130327.84230769230766264.4412955465587Min: 50 Lower Whisker: 50 Q1: 85.5 Q2: 113.5 Q3: 134.5 Upper Whisker: 180 Max: 180351.4961538461538178.12449392712554Min: 50 Lower Whisker: 50 Q1: 65 Q2: 88 Q3: 102.5 Upper Whisker: 150 Max: 150375.15225.56578947368425Min: 24 Lower Whisker: 50 Q1: 76.25 Q2: 89 Q3: 110 Upper Whisker: 150 Max: 150398.8038461538461216.89018218623488Min: 30 Lower Whisker: 30 Q1: 50 Q2: 85 Q3: 107.5 Upper Whisker: 115 Max: 115422.45769230769224255.4362348178138Pokemon Attribute: AttackGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying Pokemon Attribute: Defense002020404060608080100100120120140140160160180180200200220220Min: 30 Lower Whisker: 30 Q1: 50 Q2: 66 Q3: 85 Upper Whisker: 131 Max: 13120.342307692307692294.2926421404682Min: 37 Lower Whisker: 37 Q1: 50 Q2: 64 Q3: 78 Upper Whisker: 120 Max: 14043.996153846153845299.01003344481603Min: 20 Lower Whisker: 20 Q1: 54 Q2: 70 Q3: 89 Upper Whisker: 133 Max: 18067.65292.84113712374585Min: 30 Lower Whisker: 30 Q1: 48.75 Q2: 60 Q3: 90 Upper Whisker: 140 Max: 23091.30384615384614291.8432274247492Min: 5 Lower Whisker: 5 Q1: 43 Q2: 60 Q3: 75 Upper Whisker: 120 Max: 126114.9576923076923315.7023411371237Min: 35 Lower Whisker: 35 Q1: 52.5 Q2: 67 Q3: 82.5 Upper Whisker: 120 Max: 120138.6115384615385296.10702341137124Min: 15 Lower Whisker: 15 Q1: 49.5 Q2: 65 Q3: 80 Upper Whisker: 115 Max: 115162.26538461538462307.9005016722408Min: 25 Lower Whisker: 25 Q1: 52.5 Q2: 84.5 Q3: 110 Upper Whisker: 160 Max: 160185.91923076923078268.8913043478261Min: 28 Lower Whisker: 28 Q1: 49.5 Q2: 66 Q3: 77.5 Upper Whisker: 95 Max: 95209.57307692307694310.9849498327759Min: 30 Lower Whisker: 30 Q1: 53.5 Q2: 70 Q3: 79.75 Upper Whisker: 95 Max: 95233.2269230769231306.53971571906357Min: 15 Lower Whisker: 15 Q1: 48 Q2: 65 Q3: 81.25 Upper Whisker: 130 Max: 160256.88076923076926302.5480769230769Min: 40 Lower Whisker: 40 Q1: 71 Q2: 100 Q3: 122 Upper Whisker: 168 Max: 200280.53461538461534243.85284280936455Min: 30 Lower Whisker: 30 Q1: 60 Q2: 72.5 Q3: 114 Upper Whisker: 145 Max: 145304.18846153846147272.7015050167224Min: 15 Lower Whisker: 15 Q1: 47.5 Q2: 75 Q3: 85 Upper Whisker: 110 Max: 184327.84230769230766304.99749163879596Min: 35 Lower Whisker: 35 Q1: 67.5 Q2: 90 Q3: 100 Upper Whisker: 130 Max: 130351.4961538461538272.3386287625418Min: 30 Lower Whisker: 30 Q1: 50.5 Q2: 70 Q3: 90 Upper Whisker: 125 Max: 125375.15293.0225752508361Min: 50 Lower Whisker: 50 Q1: 96.25 Q2: 120 Q3: 150 Upper Whisker: 230 Max: 230398.8038461538461191.14506688963212Min: 35 Lower Whisker: 35 Q1: 52.5 Q2: 75 Q3: 80 Upper Whisker: 80 Max: 80422.45769230769224308.626254180602Pokemon Attribute: DefenseGrassFireWaterBugNormalPoisonElectricGroundFairyFightingPsychicRockGhostIceDragonDarkSteelFlying

If what we need is to be able to compare the main attributes between certain Pokémon, we will create a radar type graph that allows us to see this data in a simple way, as follows:

x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']

radar_chart = pygal.Radar(width=600, height=500)
radar_chart.title = 'Pokemon Attributes'
radar_chart.x_labels = x_labels
radar_chart.add('Bulbasaur', data[data["Name"]=="Bulbasaur"][x_labels].values[0])
radar_chart.render_to_file('radar_plot_attribute_Bulbasaur.svg') 
display(SVG('radar_plot_attribute_Bulbasaur.svg'))
Pokemon Attributes60605050404030302020101000HP1.570796327Attack2.617993878Defense3.665191429Sp. Atk4.71238898Sp. Def5.759586532Speed6.806784083Pokemon Attributes45220.274.30769230769232HP4987.091155245679134.75641025641022Attack4987.09115524567893279.2435897435897Defense65220.19999999999996398.6666666666667Sp. Atk65396.77295732716044302.8333333333334Sp. Def45342.4428166111112140.65384615384625SpeedBulbasaur

With this kind of graph, we can compare data in a simple and understandable way, showing a large comparative data set at one time. For example, we can compare the attributes of these four legendary Pokémon to see their differences at a glance.

Pokemon Attributes140140120120100100808060604040202000HP1.570796327Attack2.617993878Defense3.665191429Sp. Atk4.71238898Sp. Def5.759586532Speed6.806784083Pokemon Attributes90221.494.98701298701297HP100106.11739754817539144.77056277056272Attack90117.64565779335781263.0064935064935Defense125221.39999999999995362.5735930735931Sp. Atk85319.3902120840509259.89502164502164Sp. Def90325.1543422066422150.99350649350657Speed90221.494.98701298701297HP85123.40978791594908154.1049783549783Attack100106.11739754817535269.22943722943717Defense95221.39999999999995325.23593073593076Sp. Atk125365.5032530647807284.7867965367966Sp. Def85319.390212084051154.10497835497847Speed106221.475.07359307359309HP11094.58913730299294138.54761904761898Attack90117.64565779335781263.0064935064935Defense154221.39999999999995398.6666666666667Sp. Atk90325.1543422066422263.0064935064936Sp. Def130371.2673831873721126.10173160173173Speed90221.494.98701298701297HP90117.64565779335786150.99350649350646Attack85123.40978791594904259.89502164502164Defense125221.39999999999995362.5735930735931Sp. Atk90325.1543422066422263.0064935064936Sp. Def100336.6826024518247144.77056277056283SpeedMoltresArticunoMewtwoZapdos

If what we want is to see the average of each of the Pokémon’s attributes, but this time grouped by type, so that we can find, for example, which type has, on average, the highest attack or defense, we can display it in the following way:

x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']
dot_chart = pygal.Dot(x_label_rotation=30, width=600, height=600)
dot_chart.title = 'Pokemon attributes by Type'
dot_chart.x_labels = x_labels
for id, row in data.groupby(["Type 1"])[x_labels].mean().reset_index().iterrows():
  dot_chart.add(row["Type 1"], row[x_labels])

dot_chart.render_to_file('dot_chart_attribute_by_type.svg')
Pokemon attributes by TypeBug17.5Dark16.5Dragon15.5Electric14.5Fairy13.5Fighting12.5Fire11.5Flying10.5Ghost9.5Grass8.5Ground7.5Ice6.5Normal5.5Poison4.5Psychic3.5Rock2.5Steel1.5Water0.5HPAttackDefenseSp. AtkSp. DefSpeed56.8840579739.3885734752328923.107905982905947HP70.97101449102.9185306933504523.107905982905947Attack70.72463768166.4484879114680223.107905982905947Defense53.86956522229.978445129585623.107905982905947Sp. Atk64.79710145293.508402347703123.107905982905947Sp. Def61.68115942357.038359565820723.107905982905947Speed66.8064516139.3885734752328949.97756410256409HP88.38709677102.9185306933504549.97756410256409Attack70.22580645166.4484879114680249.97756410256409Defense74.64516129229.978445129585649.97756410256409Sp. Atk69.51612903293.508402347703149.97756410256409Sp. Def76.16129032357.038359565820749.97756410256409Speed83.312539.3885734752328976.84722222222223HP112.125102.9185306933504576.84722222222223Attack86.375166.4484879114680276.84722222222223Defense96.84375229.978445129585676.84722222222223Sp. Atk88.84375293.508402347703176.84722222222223Sp. Def83.03125357.038359565820776.84722222222223Speed59.7954545539.38857347523289103.71688034188031HP69.09090909102.91853069335045103.71688034188031Attack66.29545455166.44848791146802103.71688034188031Defense90.02272727229.9784451295856103.71688034188031Sp. Atk73.70454545293.5084023477031103.71688034188031Sp. Def84.5357.0383595658207103.71688034188031Speed74.1176470639.38857347523289130.58653846153845HP61.52941176102.91853069335045130.58653846153845Attack65.70588235166.44848791146802130.58653846153845Defense78.52941176229.9784451295856130.58653846153845Sp. Atk84.70588235293.5084023477031130.58653846153845Sp. Def48.58823529357.0383595658207130.58653846153845Speed69.8518518539.38857347523289157.45619658119654HP96.77777778102.91853069335045157.45619658119654Attack65.92592593166.44848791146802157.45619658119654Defense53.11111111229.9784451295856157.45619658119654Sp. Atk64.7037037293.5084023477031157.45619658119654Sp. Def66.07407407357.0383595658207157.45619658119654Speed69.9038461539.38857347523289184.32585470085468HP84.76923077102.91853069335045184.32585470085468Attack67.76923077166.44848791146802184.32585470085468Defense88.98076923229.9784451295856184.32585470085468Sp. Atk72.21153846293.5084023477031184.32585470085468Sp. Def74.44230769357.0383595658207184.32585470085468Speed70.7539.38857347523289211.19551282051282HP78.75102.91853069335045211.19551282051282Attack66.25166.44848791146802211.19551282051282Defense94.25229.9784451295856211.19551282051282Sp. Atk72.5293.5084023477031211.19551282051282Sp. Def102.5357.0383595658207211.19551282051282Speed64.437539.38857347523289238.0651709401709HP73.78125102.91853069335045238.0651709401709Attack81.1875166.44848791146802238.0651709401709Defense79.34375229.9784451295856238.0651709401709Sp. Atk76.46875293.5084023477031238.0651709401709Sp. Def64.34375357.0383595658207238.0651709401709Speed67.2714285739.38857347523289264.93482905982904HP73.21428571102.91853069335045264.93482905982904Attack70.8166.44848791146802264.93482905982904Defense77.5229.9784451295856264.93482905982904Sp. Atk70.42857143293.5084023477031264.93482905982904Sp. Def61.92857143357.0383595658207264.93482905982904Speed73.7812539.38857347523289291.8044871794872HP95.75102.91853069335045291.8044871794872Attack84.84375166.44848791146802291.8044871794872Defense56.46875229.9784451295856291.8044871794872Sp. Atk62.75293.5084023477031291.8044871794872Sp. Def63.90625357.0383595658207291.8044871794872Speed7239.38857347523289318.67414529914527HP72.75102.91853069335045318.67414529914527Attack71.41666667166.44848791146802318.67414529914527Defense77.54166667229.9784451295856318.67414529914527Sp. Atk76.29166667293.5084023477031318.67414529914527Sp. Def63.45833333357.0383595658207318.67414529914527Speed77.275510239.38857347523289345.54380341880335HP73.46938776102.91853069335045345.54380341880335Attack59.84693878166.44848791146802345.54380341880335Defense55.81632653229.9784451295856345.54380341880335Sp. Atk63.7244898293.5084023477031345.54380341880335Sp. Def71.55102041357.0383595658207345.54380341880335Speed67.2539.38857347523289372.41346153846155HP74.67857143102.91853069335045372.41346153846155Attack68.82142857166.44848791146802372.41346153846155Defense60.42857143229.9784451295856372.41346153846155Sp. Atk64.39285714293.5084023477031372.41346153846155Sp. Def63.57142857357.0383595658207372.41346153846155Speed70.6315789539.38857347523289399.28311965811963HP71.45614035102.91853069335045399.28311965811963Attack67.68421053166.44848791146802399.28311965811963Defense98.40350877229.9784451295856399.28311965811963Sp. Atk86.28070175293.5084023477031399.28311965811963Sp. Def81.49122807357.0383595658207399.28311965811963Speed65.3636363639.38857347523289426.15277777777777HP92.86363636102.91853069335045426.15277777777777Attack100.7954545166.44848791146802426.15277777777777Defense63.34090909229.9784451295856426.15277777777777Sp. Atk75.47727273293.5084023477031426.15277777777777Sp. Def55.90909091357.0383595658207426.15277777777777Speed65.2222222239.38857347523289453.0224358974359HP92.7037037102.91853069335045453.0224358974359Attack126.3703704166.44848791146802453.0224358974359Defense67.51851852229.9784451295856453.0224358974359Sp. Atk80.62962963293.5084023477031453.0224358974359Sp. Def55.25925926357.0383595658207453.0224358974359Speed72.062539.38857347523289479.892094017094HP74.15178571102.91853069335045479.892094017094Attack72.94642857166.44848791146802479.892094017094Defense74.8125229.9784451295856479.892094017094Sp. Atk70.51785714293.5084023477031479.892094017094Sp. Def65.96428571357.0383595658207479.892094017094SpeedPokemon attributes by TypeBugDarkDragonElectricFairyFightingFireFlyingGhostGrassGroundIceNormalPoisonPsychicRockSteelWater

But if what we want to see is how the attributes of a particular Pokémon are compared to the others, we can represent it in the following way, which would even allow us to buy attributes between different Pokémon in a simple way.

pokemon_names = ["Pikachu", "Dragonite"]
x_labels = ['HP', 'Attack', 'Defense', 'Sp. Atk', 'Sp. Def', 'Speed']

for pokemon_name in pokemon_names:
  this_pokemon = data[data["Name"]==pokemon_name]

  gauge = pygal.SolidGauge(inner_radius=0.70)
  gauge.title = '%s stats vs others Pokemon' % pokemon_name
  for this_label in x_labels:
    gauge.add(this_label, [{'value': this_pokemon[this_label].values[0], 'max_value': data[this_label].max()}])

  gauge.render_to_file('gauge_%s_vs_all.svg' % pokemon_name) 
Pikachu stats vs others Pokemon35198.2568300816166727.14714196816772355563.128631582253547.1813274565239640205.17587323816252208.826854841452350558.6984820770726220.0401566240202650212.76762268943932392.185696772389690577.485445.0Pikachu stats vs others Pokemon255351905523040194502305018090HPAttackDefenseSp. AtkSp. DefSpeed Dragonite stats vs others Pokemon91231.1164456261389359.40505582406426134563.812714768591129.9246099127647795235.36022350907496248.630886835774100577.404670544551270.3063588926622100236.46094010985098431.147697351311480576.4506358638362433.17716382354706Dragonite stats vs others Pokemon255911901342309519410023010018080HPAttackDefenseSp. AtkSp. DefSpeed

Conclusion

Using SVG graphics representations of our data, we can represent these data at any size and scale, without worrying about having a specific resolution and obtaining an optimal result.

In addition, tools such as Pygal, allow us to integrate this type of graphics not only in Dashboards or specific presentations, but also in all types of intranets, webapps or web pages where we can adapt them without problems to the visual environment and respecting the original design.

And that is all! If you found this post interesting, we encourage you to visit the Data Analytics category to see all the related posts and to share it on your networks. See you soon!
David Martín
David Martín
Articles: 16