Como había indicado en la Guía anterior he realizado este ejemplo utilizando la funcionalidad de este otro artículo de mostrar la integración de AnyChart con los Snippet de PHPRunner.
El ejemplo es muy sencillo, porque así entiendo que es más sencillo para la comprensión de los desarrolladores que empiezan a utilizar React, pero creo que explica con detalle la integración y potencia de la utilización de esta biblioteca gráfica de JavaScript (AnyChart).
Objetivo
Integrar en una misma página varios gráficos producidos por AnyChart ( Semi-donut y Mapas Temáticos), produciendo los cambios de datos por la selección de un campo Lookup.
DEMO: (versión React) https://fhumanes.com/map_anychart-react
DEMO: (versión PHPRunner) https://fhumanes.com/map_anychart
Solución Técnica.
Se ha añadido a la aplicación de PHPRunner, la parte correspondiente de los servicios Rest Full API para ofrecer los datos al desarrollo React.
Se ha utilizado una página contenedora de todos los gráficos, que además contiene:
- El campo Lookup en donde están todas las convocatorias de elecciones hasta el 2019. Inmediatamente, cuando se cambia el valor en este campo se actualizan los datos de todos los gráficos y se mantienen estos hasta que se vuelve a cambiar de convocatoria.
- Se define un Área de TABS (pestaña) para establecer el acceso a los gráficos. Cada uno de los gráficos se define en individuales ficheros JavaScript. La información de cabecera (Convocatoria) y todos los datos, se pasan del contenedor a estos ficheros, por lo que o se produce nueva carga de datos del server, cuando se cambia de pestaña.
Las convocatorias de 1983 y de 1987, no disponen de datos suficientes para hacer el mapa temático y por eso no salen. Esto se controla en el fichero contenedor o maestro.
Los ficheros más importantes del ejemplo son:
import * as React from 'react'; import {useEffect, useState} from 'react'; import { useRef } from "react"; import anychart from "anychart"; import './style.css'; import Grid from '@mui/material/Grid2'; import Box from '@mui/material/Box'; import { Select, MenuItem, InputLabel, FormHelperText } from '@mui/material'; import Tab from '@mui/material/Tab'; import TabContext from '@mui/lab/TabContext'; import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; import Swal from 'sweetalert2' import withReactContent from 'sweetalert2-react-content' import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import Nav from '../nav'; import Config from '../config'; import Semidonut from './semidonut'; import VotosMunicipios from './votosmunicipios'; const withRouter = (Component) => { const Wrapper = (props) => { const navigate = useNavigate(); return <Component navigate={navigate} {...props} />; }; return Wrapper; }; function DashBoard(props) { const [rows, setRows] = useState([ { "id": 29, "Orden": 33, "EsAsamblea": 1, "Titulo": "2019-AM", "Descripcion": "2019 - Asamblea de Madrid", "TipoConvocatoria_idTipoConvocatoria": 1, "EsFicticia": 0, "Orden_Ana": 32 } ]); const [convocatoria, setConvocatoria] = useState([ { "id": 29, "Orden": 33, "EsAsamblea": 1, "Titulo": "2019-AM", "Descripcion": "2019 - Asamblea de Madrid", "TipoConvocatoria_idTipoConvocatoria": 1, "EsFicticia": 0, "Orden_Ana": 32 } ]); const chartRef = useRef(null); const [escanos, setEscanos] = useState([{}]); const [votosMunicipios, setVotosMunicipios] = useState([{}]); const [colors, setColors] = useState([{}]); const [refresh, setRefresh] = useState(true); const [valueTabs, setValueTabs] = React.useState('1'); const handleChangeTabs = (event, newValue) => { setValueTabs(newValue); }; // var formWitdh = window.innerWidth; // console.log("Ancho de pantalla: ",formWitdh); // Notificación error grave function errorNotification(code, message) { const MySwal = withReactContent(Swal); MySwal.fire({ icon: 'error', title: message, text: '', timer: 5000, timerProgressBar: true, toast: true, position: "center", footer: '' }) // props.navigate(Config.URL_APP+'/logout'); } // Cargar los datos del Server cuando se carga y recarga la página const fetchCovocatorias = async () => { if (refresh) { // Para intentar que sólo sea una vez setRefresh(false); const url = Config.RUTA_API + "/convocatoriasList"; const formData = new FormData(); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": Config.AUTHORIZATION }, }; try { const response = await axios.post(url,formData,config) var resp = response.data; var data = resp.data; console.log("Respuesta del fetch 'Convocatorias': ",resp); // console.log("Data: ",data); // console.log("Primer Registro: ",data[0]); setRows(data); setConvocatoria(data[0]); fetchEscanos(data[0].id); // Accede a información de Escaños fetchVotosMunicipios(data[0].id); // Accede a información de Votos por Municipios if (resp.error === true ) { console.log("Hay un error: ",resp.message); errorNotification(resp.error,resp.message); } else { // console.log("Registros de respuesta: ", resp.data.length); } } catch(error) { console.log("Error Post - Información del Server- DashBorad", error); errorNotification('999',error); } } }; // Cargar los datos del Server cuando se carga y recarga la página const fetchEscanos = async ( id) => { if (id !== undefined) { // Para intentar que sólo sea una vez const url = Config.RUTA_API + "/escanosList"; const formData = new FormData(); formData.append('id',id); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": Config.AUTHORIZATION }, }; try { const response = await axios.post(url,formData,config) var resp = response.data; var data = resp.data; console.log("Respuesta del fetch 'Escaños': ",resp); // console.log("Data: ",data); // console.log("Primer Registro: ",data[0]); setEscanos(data); if (resp.error === true ) { console.log("Hay un error: ",resp.message); errorNotification(resp.error,resp.message); } else { // console.log("Registros de respuesta: ", resp.data.length); } } catch(error) { console.log("Error Post - Información del Server -Escanos", error); errorNotification('999',error); } } }; // Cargar los datos del Server cuando se carga y recarga la página const fetchVotosMunicipios = async ( id) => { if (id !== undefined) { // Para intentar que sólo sea una vez const url = Config.RUTA_API + "/municipiosList"; const formData = new FormData(); formData.append('id',id); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": Config.AUTHORIZATION }, }; try { const response = await axios.post(url,formData,config) var resp = response.data; var data = resp.data; var colors = resp.colors; console.log("Respuesta del fetch 'Municipios': ",resp); // console.log("Data: ",data); // console.log("Primer Registro: ",data[0]); setVotosMunicipios(data); setColors(colors); if (resp.error === true ) { console.log("Hay un error: ",resp.message); errorNotification(resp.error,resp.message); } else { // console.log("Registros de respuesta: ", resp.data.length); } } catch(error) { console.log("Error Post - Información del Server -VotosMunicipios", error); errorNotification('999',error); } } }; // Cargar los datos del Server cuando se carga la página useEffect(() => { console.log("Ejecutándose 'useEffect"); fetchCovocatorias(); }, [refresh]); const handleConvocatoria = (e) => { const { name, value } = e.target; const row = rows.find((rowid) => rowid.id === value); // console.log("Datos del Producto: ",row); setConvocatoria(row); // console.log("Datos de Input: ",input); fetchEscanos(row.id); // Reload data Escaños fetchVotosMunicipios(row.id); // Reload data Votos por Municipios }; return ( <> <Nav /> <div className='titlePage'> <h3 className="head_title">Datos Electorales de Comunidad de Madrid</h3> </div> <div className='panel'> <div className='box'> <Box component="form" noValidate autoComplete="off" > <Grid container spacing={2}> <Grid xs={12}> <InputLabel id="select-label">Selecciona la Convocatoria *</InputLabel> <Select // error={error} id="convacotaria" name="convacotaria" label="Convacotaria" required={false} variant="standard" className= 'CustomField' value={convocatoria.id ?? ''} onChange={handleConvocatoria} > {rows.map((row, index) => ( <MenuItem key={row.id} value={row.id}> {row.Descripcion} </MenuItem> ))} </Select> </Grid> </Grid> </Box> </div> </div> <div className='panel-tabs'> <TabContext value={valueTabs} > <div> <Box sx={{ mx: 'auto', displayPrint: 'block', alignItem: 'center', alignContent: 'center' , width: 300 }}> <TabList onChange={handleChangeTabs} textColor="secondary" indicatorColor="secondary" aria-label="lab API tabs example" > <Tab label="Escaños" value="1" /> <Tab label="Votos por Municipio" value="2" /> </TabList> </Box> </div> <TabPanel value="1"> { escanos.length !== 1? <Semidonut id={convocatoria.id} title={convocatoria.Descripcion} escanos={escanos} /> : ''} </TabPanel> <TabPanel value="2"> { votosMunicipios.length > 1? <VotosMunicipios id={convocatoria.id} title={convocatoria.Descripcion} votosmunicipios={votosMunicipios} colors={colors}/> : ''} </TabPanel> </TabContext> </div> </> ); } // IMPORTANT ---------------------------------------- export default withRouter(DashBoard);
import { useEffect, useRef } from "react"; import anychart from "anychart"; // import Swal from 'sweetalert2' // import withReactContent from 'sweetalert2-react-content' // import axios from 'axios'; import Config from "../config"; const Semidonut = ({ id, title, escanos }) => { // Scope: References // We use a `ref` to keep track of the AnyChart chart instance for proper cleanup console.log("Inciando Semidonut. id: "+id+" Title: "+title+" Escaños:",escanos); const chartRef = useRef(null); useEffect(() => { // console.log("Ejecutándose 'useEffect"); drawChart(); // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // Dispose of the chart instance when the component is unmounted return () => { if (chartRef.current) { console.log("Se está limpiando el recurso 'cartRef'"); chartRef.current.dispose(); chartRef.current = null; } }; }, [escanos]); // Dependency array ensures this effect runs only once // Función construct Grphics const drawChart = () => { var data = [{}]; data = [...escanos]; console.log("Empezamos a crear el Chart. Datos: ",data); // Cálculo de total de Escaños. let totalEscanos = 0; let i = 0; while (i < data.length) { totalEscanos = totalEscanos + data[i].value; i++; } // Ensure the chart is only created once to avoid duplicates. if (!chartRef.current) { console.log("Se va a iniciar el recurso 'cartRef'"); // var data = rows; // calculate total value var halfOfTotal = data.reduce((total, currentRow) => { return total + currentRow.value }, 0); // add the second half that equals initial total data.push({x: "hidden", value: halfOfTotal, fill: '#000 0'}); var chart = anychart.pie(data); // anychart.theme('lightEarth'); anychart.format.outputLocale('es-es'); // Set your licence key before you create chart. anychart.licenseKey(Config.ANYCHART_LICENSE); // Set logo source. // You can't customize credits without a license key. See https://www.anychart.com/buy/ to learn more. var credits = chart.credits(); credits.enabled(false); credits.logoSrc(''); // Add a title to the chart chart.title(title); // turn on chart animation // chart.animation(true); // apply custom start angle chart.startAngle(-90); //set the inner radius // (to turn the pie chart into a doughnut chart) chart.innerRadius("50%"); chart.padding(0).margin(0); // hide the half of the chart beyond the stage // chart.bounds(0, '50%', '100%', '100%'); chart.padding(0, 0, '-90%', 0); // set chart labels position to outside // chart.labels().enabled(false).position('outside'); chart.labels().enabled(true); chart.labels() .fontSize(11) .fontColor('white') .format("{%value}"); var tooltip = chart.tooltip(); tooltip.format("Escaños: {%value}\nVotos válido: {%porvotos}{type:number,decimalsCount:2}%"); // create standalone label and set settings var label = anychart.standalones.label(); label .enabled(true) .text(totalEscanos+'\n escaños \n \n \n \n') .width('100%') .height('100%') // .adjustFontSize(true, true) // .minFontSize(8) // .maxFontSize(20) // .fontColor('#178ACC') .position('center') .anchor('center') .hAlign('center') .vAlign('middle'); // set label to center content of chart chart.center().content(label); chart.legend(false); // Set event listener on the chart. chart.listen("pointClick", function(e){ var index = e.iterator.getIndex(); console.log('Click en Item (index): '+ index); // var row = data.row(index); console.log('Click en Item : '+ e.iterator.get("x")); console.debug(e.iterator); }); // Specify the container for the chart and draw it chart.container("semidonut"); chart.draw(); // Save the chart instance to the ref for cleanup on unmount chartRef.current = chart; } }; return ( <> {/* This is where the AnyChart Graphics Chart will be rendered */} <div id="semidonut" style={{ width: "100%", height: "300px" }}></div> </> ); }; export default Semidonut;
import { useEffect, useRef } from "react"; import anychart from "anychart"; // import Swal from 'sweetalert2' // import withReactContent from 'sweetalert2-react-content' // import axios from 'axios'; import Config from "../config"; import MunicipiosMadrid from './MunicipiosMadrid'; const VotosMunicipios = ({ id, title, votosmunicipios, colors }) => { // Scope: References // We use a `ref` to keep track of the AnyChart chart instance for proper cleanup console.log("Inciando Semidonut. id: "+id+" Title: "+title+" Votos:",votosmunicipios); const chartRef = useRef(null); useEffect(() => { // console.log("Ejecutándose 'useEffect"); drawChart(); // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // Dispose of the chart instance when the component is unmounted return () => { if (chartRef.current) { console.log("Se está limpiando el recurso 'cartRef'"); chartRef.current.dispose(); chartRef.current = null; } }; }, [votosmunicipios]); // Dependency array ensures this effect runs only once // Función construct Grphics const drawChart = () => { var dataSet = [{}]; dataSet = [...votosmunicipios]; console.log("Empezamos a crear el Chart. Datos: ",dataSet); // Ensure the chart is only created once to avoid duplicates. if (!chartRef.current) { console.log("Se va a iniciar el recurso 'cartRef'"); // set map Geo data var chart = anychart.map(); var geojson = JSON.parse(MunicipiosMadrid); chart.geoData(geojson); // anychart.theme('lightEarth'); anychart.format.outputLocale('es-es'); // Set your licence key before you create chart. anychart.licenseKey(Config.ANYCHART_LICENSE); // Set logo source. // You can't customize credits without a license key. See https://www.anychart.com/buy/ to learn more. var credits = chart.credits(); credits.enabled(false); credits.logoSrc(''); // Add a title to the chart chart.title(title); // set map title settings using html /* chart.title() .enabled(true) .useHtml(true) .padding(10, 0) .hAlign('center') .fontFamily("'Verdana', Helvetica, Arial, sans-serif") .text( '<span style="color:#7c868e; font-size: 18px">'+title+'</span>' ); */ chart.padding([0, 0, 0, 0]); // set the series var series = chart.choropleth(dataSet); // líneas de division series.stroke('0.5 #FFF'); series.hovered() .fill('#EAFE5F') .stroke(anychart.color.darken('#EAFE5F')); series.selected() .fill('#EAFE5F') .stroke(anychart.color.darken('#EAFE5F')); series.labels() .enabled(true) .fontSize(10) .fontColor('#212121') .format('{%value}'); // set tooltip settings series .tooltip() .position('left-center') .anchor('left-center') .offsetX(5) .offsetY(0) .titleFormat('{%name}') .format('Partido más votado: {%Titulo}\n'+ 'Votos recibidos: {%voto}{type:number}\n'+ '% Votos: {%PorcVotos}{type:number,decimalsCount:2}%\n'+ 'Censo: {%censo}{type:number}\n'+ 'Votos válidos: {%valido}{type:number}\n'+ 'Votos en Blancos: {%blanco}{type:number}\n'+ 'Votos Nulos: {%nulo}{type:number}' ); // disable labels series.labels(false); // Colors Scale series.colorScale(anychart.scales.ordinalColor(colors));chart.colorRange(false); chart.listen("pointClick", function(e){ var index = e.iterator.getIndex(); // console.log('Click en Item (index): ', index); // var row = data.row(index); // console.log('Click en Item : ', e.iterator.get("id")); // console.debug(e.iterator); }) // Specify the container for the chart and draw it chart.container("votosmunicipios"); chart.draw(); // Save the chart instance to the ref for cleanup on unmount chartRef.current = chart; } }; return ( <> {/* This is where the AnyChart Graphics Chart will be rendered */} <div id="votosmunicipios" style={{ width: "100%", height: "450px" }}></div> </> ); }; export default VotosMunicipios;
La parte del server, en lenguaje PHP, la he hecho como siempre, utilizando el Framework SLIM v4, integrado con PHPRunner. Ya lo he explicado muchas veces, como por ejemplo en este artículo.
Como siempre, os dejo los fuentes del proyecto React, que podéis instalar siguiendo este artículo y el proyecto PHPRunner con la integración de la parte de Rest Full API que sirve a la parte de React.
Adjuntos
Archivo | Tamaño de archivo | Descargas |
---|---|---|
map_anychart-react | 229 KB | 3 |
PHPRunner 1.8 + SLIM v4 | 4 MB | 4 |
Backup de la Base de Datos | 478 KB | 1372 |