|  | 
| 22 | 22 | 
 | 
| 23 | 23 | // eslint-disable-next-line no-restricted-syntax | 
| 24 | 24 | (function iife() { | 
| 25 |  | -	var throttledRerender; // Function for re-rendering a visualization | 
| 26 |  | -	var editorConfig;      // Object used for storing an editor configuration | 
| 27 |  | -	var TOGGLE_STATE;      // Current editor menu toggle state | 
| 28 |  | -	var ACK_INTERVAL;      // Number of milliseconds between requests to acknowledge received events | 
| 29 |  | -	var eventSource;       // Instance used to listen for server events | 
| 30 |  | -	var VERBOSE;           // Flag to indicate whether the application should be "chatty" in logging events | 
| 31 |  | -	var editor;            // Editor instance | 
| 32 |  | -	var schema;            // Visualization schema | 
| 33 |  | -	var lastID;            // Identifier of the last event received by the client | 
| 34 |  | -	var view;              // Vega visualization view instance | 
| 35 |  | -	var ack;               // Reference to an interval timer for performing periodic acknowledgment of received events | 
|  | 25 | +	var throttledRerender;       // Function for re-rendering a visualization | 
|  | 26 | +	var editorConfig;            // Object used for storing an editor configuration | 
|  | 27 | +	var TOGGLE_STATE;            // Current editor menu toggle state | 
|  | 28 | +	var ACK_INTERVAL;            // Number of milliseconds between requests to acknowledge received events | 
|  | 29 | +	var eventSource;             // Instance used to listen for server events | 
|  | 30 | +	var rawSchema;               // Raw visualization schema | 
|  | 31 | +	var VERBOSE;                 // Flag to indicate whether the application should be "chatty" in logging events | 
|  | 32 | +	var editor;                  // Editor instance | 
|  | 33 | +	var schema;                  // Schema used for rendering a visualization | 
|  | 34 | +	var lastID;                  // Identifier of the last event received by the client | 
|  | 35 | +	var view;                    // Vega visualization view instance | 
|  | 36 | +	var ack;                     // Reference to an interval timer for performing periodic acknowledgment of received events | 
| 36 | 37 | 
 | 
| 37 | 38 | 	VERBOSE = 1; | 
| 38 | 39 | 	lastID = -1; | 
|  | 
| 175 | 176 | 		} | 
| 176 | 177 | 	} | 
| 177 | 178 | 
 | 
|  | 179 | +	/** | 
|  | 180 | +	* Returns an editor-specific signal name. | 
|  | 181 | +	* | 
|  | 182 | +	* ## Notes | 
|  | 183 | +	* | 
|  | 184 | +	* -   The intent of this function is to namespace editor signals to avoid potential naming collisions. | 
|  | 185 | +	* | 
|  | 186 | +	* @private | 
|  | 187 | +	* @param {string} name - raw signal name | 
|  | 188 | +	* @returns {string} signal name belonging to the editor signal namespace | 
|  | 189 | +	*/ | 
|  | 190 | +	function signalName( name ) { | 
|  | 191 | +		if ( | 
|  | 192 | +			name === 'width' || | 
|  | 193 | +			name === 'height' || | 
|  | 194 | +			name === 'padding' || | 
|  | 195 | +			name === 'autosize' | 
|  | 196 | +		) { | 
|  | 197 | +			return name; | 
|  | 198 | +		} | 
|  | 199 | +		return '_config_' + name.replace( '/', '_' ); | 
|  | 200 | +	} | 
|  | 201 | + | 
|  | 202 | +	/** | 
|  | 203 | +	* Returns a deep copy of a JSON object. | 
|  | 204 | +	* | 
|  | 205 | +	* @private | 
|  | 206 | +	* @param {Object} obj - JSON object | 
|  | 207 | +	* @returns {Object} deep copy | 
|  | 208 | +	*/ | 
|  | 209 | +	function copyJSON( obj ) { | 
|  | 210 | +		return JSON.parse( JSON.stringify( obj ) ); | 
|  | 211 | +	} | 
|  | 212 | + | 
|  | 213 | +	/** | 
|  | 214 | +	* Adds Editor signals to the visualization specification. | 
|  | 215 | +	* | 
|  | 216 | +	* @private | 
|  | 217 | +	* @param {Object} spec - visualization specification | 
|  | 218 | +	* @returns {Object} updated specification | 
|  | 219 | +	*/ | 
|  | 220 | +	function addEditorSignals( spec ) { | 
|  | 221 | +		var signals; | 
|  | 222 | + | 
|  | 223 | +		spec = copyJSON( spec ); | 
|  | 224 | + | 
|  | 225 | +		signals = spec.signals || []; | 
|  | 226 | + | 
|  | 227 | +		// FIXME: if a value is a signal, we want the initial signal value. We can use the view.signal API (https://vega.github.io/vega/docs/api/view/#view_signal), but we first need to generate the data flow graph so that the value can be dynamically determined. So maybe what we do is we first render the "raw schema", then, upon setting up the editor, we create our new wired up schema, which we then use to re-render the visualization. | 
|  | 228 | + | 
|  | 229 | +		// FIXME: we need to determine how we want to handle values already having signals, as Vega does not explicitly allow for values to have multiple signal inputs. In principle, we could just blow them away, as we'll be replacing with our own, but a user may expect the default Vega behavior. | 
|  | 230 | + | 
|  | 231 | +		signals.push({ | 
|  | 232 | +			'name': signalName( 'background' ), | 
|  | 233 | +			'value': spec.background || '#fff' | 
|  | 234 | +		}); | 
|  | 235 | +		spec.background = { | 
|  | 236 | +			'signal': signalName( 'background' ) | 
|  | 237 | +		}; | 
|  | 238 | +		signals.push({ | 
|  | 239 | +			'name': signalName( 'description' ), | 
|  | 240 | +			'value': spec.description || '' | 
|  | 241 | +		}); | 
|  | 242 | +		spec.description = { | 
|  | 243 | +			'signal': signalName( 'description' ) | 
|  | 244 | +		}; | 
|  | 245 | + | 
|  | 246 | +		signals.push({ | 
|  | 247 | +			'name': signalName( 'title/text' ), | 
|  | 248 | +			'value': spec.title.text || [] | 
|  | 249 | +		}); | 
|  | 250 | +		spec.title.text = { | 
|  | 251 | +			'signal': signalName( 'title/text' ) | 
|  | 252 | +		}; | 
|  | 253 | + | 
|  | 254 | +		spec.signals = signals; | 
|  | 255 | + | 
|  | 256 | +		return spec; | 
|  | 257 | +	} | 
|  | 258 | + | 
| 178 | 259 | 	/** | 
| 179 | 260 | 	* Renders a visualization. | 
| 180 | 261 | 	* | 
|  | 
| 184 | 265 | 	*/ | 
| 185 | 266 | 	function render( spec ) { | 
| 186 | 267 | 		view = new vega.View( vega.parse( spec ), { | 
|  | 268 | +			'logLevel': vega.Warn, | 
| 187 | 269 | 			'renderer': 'svg', | 
| 188 | 270 | 			'container': '#main', | 
| 189 | 271 | 			'hover': true | 
|  | 
| 195 | 277 | 	* Re-renders a visualization. | 
| 196 | 278 | 	* | 
| 197 | 279 | 	* @private | 
|  | 280 | +	* @param {View} view - Vega view instance | 
| 198 | 281 | 	* @param {Object} spec - visualization specification | 
| 199 |  | -	* @returns {Promise} promise which resolves upon rendering a visualization | 
| 200 | 282 | 	*/ | 
| 201 |  | -	function rerender( spec ) { | 
|  | 283 | +	function rerender( view, spec ) { | 
|  | 284 | +		log( 'Re-rendering visualization...' ); | 
| 202 | 285 | 		view.finalize(); | 
| 203 |  | -		return render( spec ); | 
|  | 286 | +		render( spec ) | 
|  | 287 | +			.then( onRender ) | 
|  | 288 | +			.catch( onError ); | 
|  | 289 | + | 
|  | 290 | +		/** | 
|  | 291 | +		* Callback invoked upon rendering a visualization. | 
|  | 292 | +		* | 
|  | 293 | +		* @private | 
|  | 294 | +		*/ | 
|  | 295 | +		function onRender() { | 
|  | 296 | +			log( 'Successfully re-rendered visualization.' ); | 
|  | 297 | +		} | 
|  | 298 | + | 
|  | 299 | +		/** | 
|  | 300 | +		* Callback invoked upon encountering an error. | 
|  | 301 | +		* | 
|  | 302 | +		* @private | 
|  | 303 | +		* @param {Error} error - error object | 
|  | 304 | +		*/ | 
|  | 305 | +		function onError( error ) { | 
|  | 306 | +			console.log( 'Encountered an error when attempting to render visualization. Error: %s', error.message ); | 
|  | 307 | +		} | 
| 204 | 308 | 	} | 
| 205 | 309 | 
 | 
| 206 | 310 | 	/** | 
|  | 
| 235 | 339 | 		* @returns {Promise} promise which resolves upon rendering a visualization | 
| 236 | 340 | 		*/ | 
| 237 | 341 | 		function onJSON( spec ) { | 
| 238 |  | -			schema = spec; | 
| 239 |  | -			return render( spec ); | 
|  | 342 | +			rawSchema = spec; | 
|  | 343 | +			schema = addEditorSignals( rawSchema ); | 
|  | 344 | +			return render( schema ); | 
| 240 | 345 | 		} | 
| 241 | 346 | 
 | 
| 242 | 347 | 		/** | 
|  | 
| 308 | 413 | 			// FIXME: depending on changes, we may need to re-initialize the editor | 
| 309 | 414 | 
 | 
| 310 | 415 | 			// FIXME: perform JSON patch (currently, we're receiving the entire schema) | 
| 311 |  | -			onRefresh( event ); // FIXME | 
|  | 416 | + | 
|  | 417 | +			// onRefresh( event ); // FIXME | 
|  | 418 | + | 
|  | 419 | +			// rawSchema = JSON.parse( event.data ); // FIXME | 
| 312 | 420 | 		} | 
| 313 | 421 | 
 | 
| 314 | 422 | 		/** | 
|  | 
| 321 | 429 | 		function onRefresh( event ) { | 
| 322 | 430 | 			log( 'Received a "refresh" event.' ); | 
| 323 | 431 | 			lastID = event.id; | 
| 324 |  | -			schema = JSON.parse( event.data ); | 
| 325 |  | -			throttledRerender( schema ); | 
|  | 432 | + | 
|  | 433 | +			rawSchema = JSON.parse( event.data ); | 
|  | 434 | +			schema = addEditorSignals( rawSchema ); | 
|  | 435 | + | 
|  | 436 | +			throttledRerender( view, schema ); | 
| 326 | 437 | 
 | 
| 327 | 438 | 			// FIXME: this may need to be `reInitEditor` (e.g., if new axes/scales/datasets are added or axes/scales/datasets are removed, etc) | 
| 328 | 439 | 			refreshEditor(); | 
|  | 
| 394 | 505 | 	* @private | 
| 395 | 506 | 	*/ | 
| 396 | 507 | 	function syncEditorConfigGeneral() { | 
| 397 |  | -		editorConfig.general.background = schema.background || '#fff'; | 
| 398 |  | -		editorConfig.general.description = schema.description || ''; | 
| 399 |  | -		editorConfig.general.height = schema.height; | 
| 400 |  | -		editorConfig.general.width = schema.width; | 
|  | 508 | +		editorConfig.general.background = rawSchema.background || '#fff'; | 
|  | 509 | +		editorConfig.general.description = rawSchema.description || ''; | 
|  | 510 | +		editorConfig.general.height = rawSchema.height; | 
|  | 511 | +		editorConfig.general.width = rawSchema.width; | 
| 401 | 512 | 	} | 
| 402 | 513 | 
 | 
| 403 | 514 | 	/** | 
|  | 
| 406 | 517 | 	* @private | 
| 407 | 518 | 	*/ | 
| 408 | 519 | 	function syncEditorConfigPadding() { | 
| 409 |  | -		editorConfig.padding.bottom = schema.padding.bottom || 0; | 
| 410 |  | -		editorConfig.padding.left = schema.padding.left || 0; | 
| 411 |  | -		editorConfig.padding.right = schema.padding.right || 0; | 
| 412 |  | -		editorConfig.padding.top = schema.padding.top || 0; | 
|  | 520 | +		editorConfig.padding.bottom = rawSchema.padding.bottom || 0; | 
|  | 521 | +		editorConfig.padding.left = rawSchema.padding.left || 0; | 
|  | 522 | +		editorConfig.padding.right = rawSchema.padding.right || 0; | 
|  | 523 | +		editorConfig.padding.top = rawSchema.padding.top || 0; | 
| 413 | 524 | 	} | 
| 414 | 525 | 
 | 
| 415 | 526 | 	/** | 
|  | 
| 418 | 529 | 	* @private | 
| 419 | 530 | 	*/ | 
| 420 | 531 | 	function syncEditorConfigTitle() { | 
| 421 |  | -		editorConfig.title.text = schema.title.text.join( '\n' ); | 
| 422 |  | -		editorConfig.title.align = schema.title.align || ''; | 
| 423 |  | -		editorConfig.title.fontSize = schema.title.fontSize || 0; | 
| 424 |  | -		editorConfig.title.fontStyle = schema.title.fontStyle || ''; | 
| 425 |  | -		editorConfig.title.fontWeight = schema.title.fontWeight || ''; | 
|  | 532 | +		editorConfig.title.text = rawSchema.title.text.join( '\n' ); | 
|  | 533 | +		editorConfig.title.align = rawSchema.title.align || ''; | 
|  | 534 | +		editorConfig.title.fontSize = rawSchema.title.fontSize || 0; | 
|  | 535 | +		editorConfig.title.fontStyle = rawSchema.title.fontStyle || ''; | 
|  | 536 | +		editorConfig.title.fontWeight = rawSchema.title.fontWeight || ''; | 
| 426 | 537 | 	} | 
| 427 | 538 | 
 | 
| 428 | 539 | 	/** | 
|  | 
| 507 | 618 | 		var folder = editor.addFolder( 'Padding' ); | 
| 508 | 619 | 		folder.close(); | 
| 509 | 620 | 
 | 
| 510 |  | -		folder.add( editorConfig.padding, 'bottom', 0 ); | 
| 511 |  | -		folder.add( editorConfig.padding, 'left', 0 ); | 
| 512 |  | -		folder.add( editorConfig.padding, 'right', 0 ); | 
| 513 |  | -		folder.add( editorConfig.padding, 'top', 0 ); | 
|  | 621 | +		folder.add( editorConfig.padding, 'bottom' ); | 
|  | 622 | +		folder.add( editorConfig.padding, 'left' ); | 
|  | 623 | +		folder.add( editorConfig.padding, 'right' ); | 
|  | 624 | +		folder.add( editorConfig.padding, 'top' ); | 
| 514 | 625 | 	} | 
| 515 | 626 | 
 | 
| 516 | 627 | 	/** | 
|  | 
| 560 | 671 | 		var opts; | 
| 561 | 672 | 		var url; | 
| 562 | 673 | 		var obj; | 
|  | 674 | +		var flg; | 
| 563 | 675 | 		var ns; | 
| 564 | 676 | 
 | 
| 565 | 677 | 		// FIXME: other properties/objects | 
| 566 | 678 | 
 | 
|  | 679 | +		// For height/width/padding updates, we avoid propagating changes to the in-memory specification, as these updates also require additional layout modifications (e.g., scale ranges) and the logic for that is on the server. Instead, after pushing these changes to the server, the server will respond with a "refresh" event which will force the visualization to be re-rendered... | 
| 567 | 680 | 		obj = event.object; | 
| 568 | 681 | 		switch ( obj ) { | 
| 569 | 682 | 		case editorConfig.general: | 
| 570 | 683 | 			ns = ''; | 
|  | 684 | +			switch ( event.property ) { | 
|  | 685 | +			case 'width': | 
|  | 686 | +			case 'height': | 
|  | 687 | +				flg = true; | 
|  | 688 | +				break; | 
|  | 689 | +			default: | 
|  | 690 | +				break; | 
|  | 691 | +			} | 
| 571 | 692 | 			break; | 
| 572 | 693 | 		case editorConfig.padding: | 
| 573 | 694 | 			ns = 'padding/'; | 
|  | 695 | +			flg = true; | 
| 574 | 696 | 			break; | 
| 575 | 697 | 		case editorConfig.title: | 
| 576 | 698 | 			ns = 'title/'; | 
|  | 
| 580 | 702 | 		} | 
| 581 | 703 | 		log( 'Editor changed: %s%s', ns, event.property ); | 
| 582 | 704 | 
 | 
|  | 705 | +		if ( !flg ) { | 
|  | 706 | +			log( 'Updating rendered visualization...' ); | 
|  | 707 | +			view.signal( signalName( ns+event.property ), event.value ); | 
|  | 708 | +			view.runAsync() | 
|  | 709 | +				.then( onRender ); | 
|  | 710 | +		} | 
| 583 | 711 | 		url = '/config/'+ns+event.property; | 
| 584 | 712 | 		opts = { | 
| 585 | 713 | 			'method': 'POST', | 
| 586 | 714 | 			'headers': { | 
| 587 | 715 | 				'Content-Type': 'text/plain' | 
| 588 | 716 | 			}, | 
| 589 |  | -			'body': event.value, | 
|  | 717 | +			'body': event.value.toString(), | 
| 590 | 718 | 			'credentials': 'same-origin' | 
| 591 | 719 | 		}; | 
| 592 | 720 | 
 | 
|  | 
| 595 | 723 | 			.then( onResponse ) | 
| 596 | 724 | 			.catch( onError ); | 
| 597 | 725 | 
 | 
|  | 726 | +		/** | 
|  | 727 | +		* Callback invoked upon rendering a visualization. | 
|  | 728 | +		* | 
|  | 729 | +		* @private | 
|  | 730 | +		*/ | 
|  | 731 | +		function onRender() { | 
|  | 732 | +			log( 'Successfully rendered visualization.' ); | 
|  | 733 | +		} | 
|  | 734 | + | 
| 598 | 735 | 		/** | 
| 599 | 736 | 		* Callback invoked upon receiving an HTTP response. | 
| 600 | 737 | 		* | 
|  | 
| 680 | 817 | 	function sendAcknowledgment() { | 
| 681 | 818 | 		var opts = { | 
| 682 | 819 | 			'method': 'POST', | 
| 683 |  | -			'body': lastID.toString(), | 
|  | 820 | +			'body': lastID.toString(), // FIXME: this isn't working | 
| 684 | 821 | 			'credentials': 'same-origin' | 
| 685 | 822 | 		}; | 
| 686 | 823 | 		log( 'Sending acknowledgement to server...' ); | 
|  | 
0 commit comments