Pirámides de población con R y amCharts

Una de las maneras de profesionalizar nuestras visualizaciones en R-Shiny es utilizando “wrappers”, es decir, librerías que “envuelven” código de otros lenguajes con estructuras en R para permitirnos ampliar funcionalidades de otros entornos sin conocer los lenguajes originales.

A continuación les presentamos una manera de construir una función “wrapper” propia para generar pirámides de población usando la librería AmCharts de JavaScript.

Consigna:


Recordatorio

La resolución del ejercicio debe subirse al campus virtual en un archivo .R o .RMD. En cualquiera de los dos formatos, el código debe poder ejecutarse de manera independiente en cualquier computadora (incluyendo el acceso a los datos y la carga de las librerías necesarias)


library(shiny)
library(htmltools)
library(jsonlite)
library(dplyr)


mostrar_piramide_poblacion_amcharts <- function(data_piramide) {

  data_json <- as.character(toJSON(data_piramide))

  html_js_content <- paste0('
    <html>
    <head>
      <style>
        body { margin: 0; padding: 0; }
        #chartdiv {
          width: 100%;
          height: 500px;
        }
      </style>
      <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
      <script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
      <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
    </head>
    <body>
      <div id="chartdiv"></div>

      <script>
        am5.ready(function() {
          var root = am5.Root.new("chartdiv");

          root.setThemes([
            am5themes_Animated.new(root)
          ]);

          var chart = root.container.children.push(
            am5xy.XYChart.new(root, {
              panX: false,
              panY: false,
              wheelX: "panX",
              wheelY: "zoomX",
              layout: root.verticalLayout,
              arrangeTooltips: false,
              paddingLeft: 0,
              paddingRight: 10
            })
          );

          chart.getNumberFormatter().set("numberFormat", "#.#s");

          var legend = chart.children.push(
            am5.Legend.new(root, {
              centerX: am5.p50,
              x: am5.p50
            })
          );

          var data = ', data_json, ';

          var yAxis = chart.yAxes.push(
            am5xy.CategoryAxis.new(root, {
              categoryField: "age",
              renderer: am5xy.AxisRendererY.new(root, {
                inversed: true,
                cellStartLocation: 0.1,
                cellEndLocation: 0.9,
                minorGridEnabled: true,
                minGridDistance: 20
              })
            })
          );
          yAxis.data.setAll(data);

          var xAxis = chart.xAxes.push(
            am5xy.ValueAxis.new(root, {
              renderer: am5xy.AxisRendererX.new(root, {
                minGridDistance: 60,
                strokeOpacity: 0.1
              })
            })
          );

          function createSeries(field, labelCenterX, pointerOrientation, rangeValue) {
            var series = chart.series.push(
              am5xy.ColumnSeries.new(root, {
                xAxis: xAxis,
                yAxis: yAxis,
                valueXField: field,
                categoryYField: "age",
                sequencedInterpolation: true,
                clustered: false,
                tooltip: am5.Tooltip.new(root, {
                  pointerOrientation: pointerOrientation,
                  labelText: "{categoryY}: {valueX}"
                })
              })
            );

            series.columns.template.setAll({
              height: am5.p100,
              strokeOpacity: 0,
              fillOpacity: 0.8
            });

            series.bullets.push(function() {
              return am5.Bullet.new(root, {
                locationX: 1,
                locationY: 0.5,
                sprite: am5.Label.new(root, {
                  centerY: am5.p50,
                  text: "{valueX}",
                  populateText: true,
                  centerX: labelCenterX
                })
              });
            });

            series.data.setAll(data);
            series.appear();

            var rangeDataItem = xAxis.makeDataItem({
              value: rangeValue
            });
            xAxis.createAxisRange(rangeDataItem);
            rangeDataItem.get("grid").setAll({
              strokeOpacity: 0,
              stroke: series.get("stroke")
            });

            var label = rangeDataItem.get("label");
            label.setAll({
              text: field.toUpperCase(),
              fontSize: "1.1em",
              fill: series.get("stroke"),
              paddingTop: 10,
              isMeasured: false,
              centerX: labelCenterX
            });
            label.adapters.add("dy", function() {
              return -chart.plotContainer.height();
            });

            return series;
          }

          createSeries("male", am5.p100, "right", -3);
          createSeries("female", 0, "left", 4);

          var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
            behavior: "zoomY"
          }));
          cursor.lineY.set("forceHidden", true);
          cursor.lineX.set("forceHidden", true);

          chart.appear(1000, 100);

        });
      </script>
    </body>
    </html>
  ')

  # Usamos tags$iframe para renderizar un documento HTML completo de forma aislada
  return(tags$iframe(srcdoc = html_js_content,
                     width = "100%",
                     height = "550px",
                     style = "border: none; overflow: hidden;"))
}

# Datos de ejemplo
data_piramide <- data.frame(
  age = c("70-74", "65-69", "60-64", "55-59", "50-54", "45-49", "40-44", "35-39", "30-34","25-29", "20-24", "15-19", "10-14", "5-9", "0-4"),
  male = c(-0.5, -0.8, -1.1, -1.7, -2.2, -2.8, -3.4, -4.2, -5.2, -5.6, -5.1, -3.8, -3.2, -4.4, -5.0),
  female = c(0.8, 1.0, 1.3, 1.9, 2.5, 3.0, 3.6, 4.1, 4.8, 5.1, 5.1, 3.8, 3.4, 4.1, 4.8)
)

browsable(mostrar_piramide_poblacion_amcharts(data_piramide))