La motivación de esta entrada de blog es mostrar cómo podemos crear gráficos interactivos que podremos introducirlos en nuestras páginas Webs desde una especificación creada mediante el lenguaje declarativo de Vega y el uso de la librería React.
Para ello crearemos un editor de gráficos Vega que nos permitirá ver cómo una especificación Vega se convierte en un gráfico que podremos introducirlo en nuestra Web de forma sencilla y ágil sin necesidad de tener que montar una estructura compleja.
Lo primero de todo va a ser montarnos el esqueleto de lo que va a ser nuestra pequeña aplicación. Para ello usaremos “Create React App” que nos posibilitará crearnos una aplicación “single page” con la librería React y sin ningún coste por parte de configuraciones.
Ejecutamos el comando:
npx create-react-app vegaeditor
El comando npx está disponible desde npm 5.2+, de lo contrario tendrás que ejecutar:
npm install -g create-react-app
create-react-app vegaeditor
Una vez ejecutado el comando anterior ya tenemos nuestra aplicación lista para ser lanzada, tan solo tendremos que acceder al directorio “vegaeditor” y lanzar el comando:
npm start
Esto arrancará un servidor en el puerto 3000 en nuestra propia máquina al cual podremos acceder navegando a “http://localhost:3000/”
El siguiente paso será instalarnos algunas librerías que usaremos en el desarrollo:
- npm install vega –save : nos permite usar Vega junto con React.
- npm install react-vega –save : crea el componente “Vega” que nos ayuda a simplicar la visualización de los gráficos desde la especificación del mismo.
Una vez instalado todo ya podemos empezar a crear nuestra aplicación. Para ello vamos a hacer un breve resumen de las carpetas que nos encontraremos dentro del proyecto “vegaeditor” que acabamos de crear:
Ahora mismo los ficheros que nos importan son App.js y App.css que son el fichero raíz de nuestra aplicación y sus estilos. Abrimos el fichero App.js y eliminamos su contenido dejando tan solo el esqueleto:
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return ( );
}
}
export default App;
Hacemos lo mismo con App.css y estaremos listos para empezar de cero con nuestro desarrollo.
Respecto al desarrollo, la idea es crear una única ventana con el siguiente aspecto:
Para ello vamos a crear el esqueleto de lo que sería la UI de nuestra app. Accedemos al fichero App.js y procedemos a crear la siguiente arquitectura con jsx donde dibujaremos la ventana, la sección de especificaciones y la parte de visualización del gráfico:
<div className='screen'>
<div className='specs'>
</div>
<div className='graph'>
</div>
</div>
Una vez hecho esto procedemos a crear los estilos:
.screen {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.specs {
width: 500px;
height: 100vh;
overflow: hidden;
border: 1px solid black;
background-color: yellow;
}
.graph {
width: calc(100% - 500px);
overflow: auto;
background-color: blue;
height: 500px;
}
Esto nos dibujará el layout que habíamos pensado inicialmente.
Siguiente paso, empezar a llenar nuestra ventana de elementos útiles.
Área de especificaciones
Podríamos usar el elemento “textarea” que nos dará un lienzo en blanco para poder añadir texto y también un método que nos controle lo que sucede dentro del mismo cada vez que hacemos un cambio,
<textarea className='codeView' onChange={(e) => this.onChange(e.target.value)}>{ spec }</textarea>
Esto nos obliga a dos cosas, a mantener un estado con el valor de “spec” y a crearnos nuestro método de onChange dentro del fichero App.js
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.state = {
spec: GRAPH_SPEC ß constante que deberemos de definir con la especificación Vega del gráfico
};
}
onChange(data) {
try {
var spec = data;
spec = JSON.parse(data, null, 2);
this.setState({
spec: spec,
error: null
});
}catch(e) {
console.error(e);
this.setState({
error: e.message
});
}
}
Como podemos ver, en cada cambio deberemos de comprobar si estamos creando un objeto JSON correcto y no está mal construido. En caso de que esté mal construido podemos añadir un nuevo valor al estado de nuestro componente con el nombre de error y añadirlo a la visualización.
{ this.state.error != null ?
<div className='error' >{this.state.error}</div>
:
null
}
Como se puede observar, ahora mismo obtendremos el elemento [object Object] en la visualización dentro del “textarea”, para ello simplemente necesitamos convertirlo a String quedando el código de la siguiente forma.
<textarea className='codeView' onChange={(e) => this.onChange(e.target.value)}>{JSON.stringify(this.state.spec, null, 2)}</textarea>
Visualización del gráfico
Lo primero de todo será importarnos el componente “Vega” desde la librería “react-vega”:
import Vega from 'react-vega';
Una vez tenemos el componente simplemente deberemos de facilitarle las especificaciones del gráfico para su visualización.
<Vega spec={this.state.spec} renderer="svg"/>
Y ya tenemos el resultado (si le quitamos el color del fondo a la parte de gráfico nos quedará de la siguiente forma)
Una vez llegados a este punto tan solo deberemos de editar las especificaciones del gráfico para ver cómo cambia la visualización del mismo.
Próximos pasos
Ya tenemos un editor de Vega con las funcionalidades básicas pero vamos a dotarlo de algunas funcionalidades extra que harán que todo sea más fácil.
Hacer que el gráfico responda al cambio de tamaño de la ventana
Lo primero de todo será instalarnos la librería “lodash” que nos facilitará una gran cantidad de utilidades, entre ellas la función “debounce”. Esta nos permitirá no atender todos los eventos de cambio de tamaño y atender solo el último y actuar N milisegundos después:
npm install lodash --save
Añadimos el estado “width” en el estado del componente y también nos importamos la librería
import _ from 'lodash';
…
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.state = {
width: null,
spec: GRAPH_SPEC,
error: null
};
}
y por último creamos las entradas necesarias en el ciclo de vida del componente para atender al evento de cambio de tamaño.
Añadimos el listener cuando el componente esté montado
componentDidMount(){
window.addEventListener('resize', this.resize_debounced);
this.resize();
}
Eliminamos el listener cuando el componente desaparece
componentWillUnmount() {
window.removeEventListener('resize', this.resize_debounced);
}
Añadimos la lógica
componentWillMount(){
this.resize_debounced = _.debounce(this.resize, 800);
}
resize() {
var width = window.innerWidth;
this.setState({
width: width
});
}
Dado que hemos creado la función “resize”, deberemos de asociarle “this” en el constructor
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.resize = this.resize.bind(this);
this.state = {
width: null,
spec: GRAPH_SPEC,
error: null
};
}
Ahora tan solo nos falta usar la propiedad “width” del estado en la visualización del gráfico. Para ello en la parte de render hacemos:
var spec = this.state.spec;
spec.width = this.state.width - SPEC_SPACE - RIGHT_MARGIN;
…
<Vega key={spec.width} spec={spec} renderer="svg"/>
Donde SPEC_SPACE (500px de las especificaciones) RIGHT_MARGIN (margen derecho que queramos en la parte derecha de la pantalla) son dos constantes.
Mejorar la edición del objeto JSON
Para este apartado vamos a instalar la librería “react-editable-json-tree” que nos permitirá facilitarle el objeto JSON y que este nos de una UI fácil de usar para editar objetos JSON de forma más simple.
Instalación
npm install react-editable-json-tree –save
Importamos
import { JsonTree } from 'react-editable-json-tree';
Lo usamos para reemplazar el “textarea”:
<div className='codeView'>
<JsonTree data={this.state.spec} isCollapsed={(keyPath, deep) => (deep === 3)} onFullyUpdate={this.onChange}/>
</div>
El resultado es un elemento JSON editable de forma sencilla:
Todavía hay un problema, y es que si realizamos algún cambio recibiremos el error “Unexpected token o in JSON at position 1”. Para ello deberemos de modificar la función “onChange” dado que ya no nos hará falta verificar que el objeto JSON está bien construido, pero si que deberemos de crear un nuevo objeto JSON desde el JSON original dado que de lo contrario, Vega no se da de cuenta que el objeto JSON ha sufrido cambios y no volvería a dibujárnoslo.
onChange(data) {
try {
var spec = data;
//spec = JSON.parse(data, null, 2); eliminada
spec = JSON.parse(JSON.stringify(data, null, 2));
this.setState({
spec: spec,
error: null
});
}catch(e) {
console.error(e);
this.setState({
error: e.message
});
}
}
Posibles mejoras
Añadir un botón en la cabecera para soportar ambos tipos de edición, tanto con el texto plano como con la librería “’react-editable-json-tree”.
Podrás encontrar todo el siguiente repositorio