This article is available in English too.
Proviamo a creare una semplice inforgrafica a partire da un file XML
contenente i dati sulle emissioni di anidride carbonica in Europa.
Il file ha il seguente formato:
<dataset>
<nodes>
<node>
<rank>58</rank>
<country>Norway</country>
<tonsperson>11.40</tonsperson>
<tons>52.35</tons>
</node>
<node>
<rank>54</rank>
<country>Sweden</country>
<tonsperson>6.53</tonsperson>
<tons>58.77</tons>
</node>
...
</nodes>
</dataset>
Per poter posizionare le informazioni su una cartina geografica, dobbiamo
aggiungere i campi latitudine e longitudine per ogni stato europeo (non c'è
bisogno di una precisione elevata).
<dataset>
<nodes>
<node>
<rank>58</rank>
<country>Norway</country>
<tonsperson>11.40</tonsperson>
<tons>52.35</tons>
<latitude>62</latitude>
<longitude>15</longitude>
</node>
<node>
<rank>54</rank>
<country>Sweden</country>
<tonsperson>6.53</tonsperson>
<tons>58.77</tons>
<latitude>60</latitude>
<longitude>17</longitude>
</node>
...
</nodes>
</dataset>
Per utilizzare i dati dobbiamo caricarli in una sorgente dati XML,
utilizzando l'oggetto System.Xml.XmlDataSource
, ma il problema è
che l'oggetto vuole i campi dei record specificati come se fossero
attributi del nodo, cioè il file dovrebbe essere della forma:
<nodes>
<node rank="58" country="Norway" tonsperson="11.40" tons="52.35"
latitude="62" longitude="15">
</node>
...
</nodes>
Per trasformare il documento XML non è necessario farlo a mano, ma più
semplicemente utilizzeremo un file XSL (eXtensible Stylesheet
Language) che all'interno conterrà le specifiche XSLT (XSL Transformations) di trasformazione. Il file è il seguente:
<?xml version="1.0"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" >
<xsl:strip-space elements="*"/>
<xsl:output method="xml"
omit-xml-declaration="yes"
indent="yes"
standalone="yes" />
<xsl:template match="/">
<xsl:for-each select="dataset">
<xsl:for-each select="nodes">
<xsl:element name="nodes">
<xsl:for-each select="node">
<xsl:element
name="node">
<xsl:attribute
name="rank">
<xsl:value-of select="rank"/>
</xsl:attribute>
<xsl:attribute
name="country">
<xsl:value-of select="country"/>
</xsl:attribute>
<xsl:attribute
name="tonsperson">
<xsl:value-of select="tonsperson"/>
</xsl:attribute>
<xsl:attribute
name="tons">
<xsl:value-of select="tons"/>
</xsl:attribute>
<xsl:attribute
name="latitude">
<xsl:value-of select="latitude"/>
</xsl:attribute>
<xsl:attribute
name="longitude">
<xsl:value-of select="longitude"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Creiamo ora una nuova pagina ed inseriamo all'interno un oggetto
System.Web.UI.DataVisualization.Charting.Chart
, specificando nel nostro caso un
grafico di tipo bolla, con due valori in ordinata e che non abbia nessuna
griglia o bordo
se non una mappa dell'Europa come sfondo:
<asp:Chart ID="m_Chart" runat="server" Height="600px" Width="600px" >
<Series>
<asp:Series ChartArea="m_ChartArea" Name="m_Series"
MarkerBorderColor="Brown" ChartType="Bubble"
MarkerStyle="Circle" YValuesPerPoint="2">
</asp:Series>
</Series>
<ChartAreas>
<asp:ChartArea Name="m_ChartArea" BackImage="europe.png"
BackImageAlignment="Center" BackImageWrapMode="Scaled">
<AxisY LineColor="Transparent">
<MajorGrid Enabled="False" />
<MajorTickMark Enabled="False" />
<LabelStyle Enabled="False" />
</AxisY>
<AxisX LineColor="Transparent">
<MajorGrid Enabled="False" />
<MajorTickMark Enabled="False" />
<LabelStyle Enabled="False" />
</AxisX>
</asp:ChartArea>
</ChartAreas>
</asp:Chart>
Procediamo ora a caricare il documento XML all'interno dell'oggetto
XmlDataSource
.
protected void Page_Load(object sender, EventArgs e)
{
XmlDataSource dataSource = new XmlDataSource();
dataSource.DataFile="dataset.xml";
dataSource.TransformFile = "transform.xsl";
XmlDocument doc = dataSource.GetXmlDocument();
...
Il seguente codice carica i punti all'interno del grafico, dove come ascissa
si usa la longitudine, come primo valore di ordinata la latitudine e come
secondo valore, ovvero il raggio della bolla, si usano i milioni di tonnellate di CO2 emesse
da ogni paese. Come etichetta di ogni bolla si usa la nazione. Il valore
tonnellate/persona viene invece utilizzato per definire il colore della bolla,
più il valore è alto, più la bolla tende al rosso acceso.
Per fare in modo che
l'algoritmo funzioni con qualsiasi insieme di dati e per fare in modo
che i colori si distribuiscano sempre uniformemente, il calcolo della
colorazione viene fatta in due fasi, nella prima scansione di determinano i
valori minimi e massimo, nella seconda scansione si dilatano i colori
all'interno dell'intervallo appena determinato.
Si noti che la virgola decimale presente nel file XML
potrebbe non coincidere
con quella della lingua installata nel sistema operativo in uso, dunque la conversione va fatta usando la cultura corrente.
Infine, una sintesi testuale delle informazioni di ogni bolla viene visualizzata
come tooltip.
Series series = m_Chart.Series[0];
XmlNodeList nodes = doc.SelectNodes("//node");
Double? minTonsPerson = null;
Double? maxTonsPerson = null;
foreach (XmlNode node in nodes)
{
String country = node.Attributes["country"].Value;
Double latitude = Convert.ToDouble(node.Attributes["latitude"].Value,
CultureInfo.InvariantCulture);
Double longitude = Convert.ToDouble(node.Attributes["longitude"].Value,
CultureInfo.InvariantCulture);
Double tons = Convert.ToDouble(node.Attributes["tons"].Value,
CultureInfo.InvariantCulture);
String tonsPersonStr = node.Attributes["tonsperson"].Value;
Double tonsPerson = Convert.ToDouble(tonsPersonStr,
CultureInfo.InvariantCulture);
if (minTonsPerson.HasValue)
{
if (tonsPerson < minTonsPerson.Value) minTonsPerson =
tonsPerson;
}
else
minTonsPerson = tonsPerson;
if (maxTonsPerson.HasValue)
{
if (tonsPerson > maxTonsPerson.Value) maxTonsPerson =
tonsPerson;
}
else
maxTonsPerson = tonsPerson;
DataPoint point = new DataPoint(longitude, new Double[] { latitude, tons
});
point.Label = country;
point.SetCustomProperty("tonsPerson", tonsPersonStr);
point.ToolTip = String.Format("{0}: {1} milliontons", country, tons);
series.Points.Add(point);
}
foreach (DataPoint point in series.Points)
{
Double tonsPerson = Convert.ToDouble(point.GetCustomProperty("tonsPerson"),
CultureInfo.InvariantCulture);
point.Color = Color.FromArgb(255, Convert.ToInt32(255 - 155 *
(tonsPerson - minTonsPerson) /
(maxTonsPerson - minTonsPerson)), 0);
}