Skip to content
Permalink
Browse files

Dynamic interactive graph loading

Render interactive graph with <graph mode='interactive'>.
Using mode='interactive' automatically uses vega 2.0, just like 'version':2 inside the graph spec.

Modes of operation:
1) show static image - simple <img href='...' /> tag
2) show live graph -- graph data is preloaded into wgGraphSpecs, and rendered
   once ext.graph.vega1 or ext.graph.vega2 is loaded.
   This mode is used when Graphoid (static) image is not available, e.g. 'preview'.
3) show static graph + render live on click (vega2 only) -- ext.graph.loader is preloaded,
   on click it loads ext.graph.vega2 and replaces the <img>.
4) show static graph + begin loading all the needed resources. Once loaded,
   replace automatically.  This should be done just like interactive, but without
   a user action. In theory, this method will show the img right away plus later allow interaction.
   We have to show the <img> in case of an older browser.

== Other changes ==
* renamed all classes from mw-wiki-graph-* to mw-graph-*

TODO:
* Make loading of vega and api call at the same time to optimize performance
* For #3, hide static image only after graph finishes rendering (render it in hidden mode and replace afterwards)
* Implement #4, but disable it by default
* decide on 3 mode names:
  * "static" (default) - graph is always an image (for now, in preview, it will be live)
  * "click-to-interact" - graph starts as a static image, but can be clicked. Current choice - "interactive"
  * "always-interactive" - graph is always interactive (this mode might be disabled for now)
  Good words - "live", "interact", "interactive", "auto", ....?

Change-Id: I42efddceb2aa9449075d6fbde1604a8ff222e254
  • Loading branch information...
nyurik committed Sep 30, 2015
1 parent 4cb4da7 commit b735f63ff4b673eef44f7851bafa7e377e79c1cc
@@ -42,8 +42,9 @@ public static function onGraphTag( $input, /** @noinspection PhpUnusedParameterI
array $args, Parser $parser, \PPFrame $frame ) {
// expand template arguments and other wiki markup
$input = $parser->recursivePreprocess( $input, $frame );
return self::buildHtml( $input, $parser->getTitle(), $parser->getRevisionId(),
$parser->getOutput(), $parser->getOptions()->getIsPreview() );
$parser->getOutput(), $parser->getOptions()->getIsPreview(), $args );
}
public static function finalizeParserOutput( ParserOutput $output, $title, $isPreview ) {
@@ -52,38 +53,34 @@ public static function finalizeParserOutput( ParserOutput $output, $title, $isPr
}
$specs = $output->getExtensionData( 'graph_specs' );
if ( $specs !== null ) {
global $wgGraphImgServiceAlways, $wgGraphImgServiceUrl;
if ( $isPreview || !$wgGraphImgServiceUrl || !$wgGraphImgServiceAlways ) {
// We can only load one version of vega lib - either 1 or 2
// If the default version is 1, and if any of the graphs need Vega2,
// we treat all graphs as Vega2 and load corresponding libraries.
// All this should go away once we drop Vega1 support.
global $wgGraphDefaultVegaVer;
$vegaVer = $wgGraphDefaultVegaVer;
if ( $vegaVer === 1 ) {
foreach ( $specs as $spec ) {
if ( property_exists( $spec, 'version' ) ) {
$ver = $spec->version;
if ( is_numeric( $ver ) && $ver > 1 ) {
$vegaVer = 2;
break;
}
}
}
}
$output->addModules( 'ext.graph.vega' . $vegaVer );
$output->addJsConfigVars( 'wgGraphSpecs', $specs );
$output->setProperty( 'graph_specs',
FormatJson::encode( $specs, false, FormatJson::ALL_OK ) );
$output->addTrackingCategory( 'graph-tracking-category', $title );
// We can only load one version of vega lib - either 1 or 2
// If the default version is 1, and if any of the graphs need Vega2,
// we treat all graphs as Vega2 and load corresponding libraries.
// All this should go away once we drop Vega1 support.
$liveSpecs = $output->getExtensionData( 'graph_live_specs' );
$interact = $output->getExtensionData( 'graph_interact' );
if ( $liveSpecs || $interact ) {
// TODO: these 3 js vars should be per domain if 'ext.graph' is added, not per page
global $wgGraphDataDomains, $wgGraphUrlBlacklist, $wgGraphIsTrusted;
$output->addJsConfigVars( 'wgGraphDataDomains', $wgGraphDataDomains );
$output->addJsConfigVars( 'wgGraphUrlBlacklist', $wgGraphUrlBlacklist );
$output->addJsConfigVars( 'wgGraphIsTrusted', $wgGraphIsTrusted );
$vegaVer = $output->getExtensionData( 'graph_vega2' ) ? 2 : 1;
if ( $liveSpecs ) {
$output->addModules( 'ext.graph.vega' . $vegaVer );
$output->addJsConfigVars( 'wgGraphSpecs', $liveSpecs );
} else {
$output->addModules( 'ext.graph.loader' );
}
}
$output->setProperty( 'graph_specs',
FormatJson::encode( $specs, false, FormatJson::ALL_OK ) );
$output->addTrackingCategory( 'graph-tracking-category', $title );
}
}
@@ -108,9 +105,11 @@ public static function editPageShowEditFormInitial( &$editpage, $output ) {
* @param int $revid
* @param ParserOutput $parserOutput
* @param bool $isPreview
* @param array $args
* @return string
*/
public static function buildHtml( $jsonText, $title, $revid, $parserOutput, $isPreview ) {
public static function buildHtml( $jsonText, $title, $revid, $parserOutput, $isPreview,
$args = null ) {
global $wgGraphImgServiceUrl, $wgServerName, $wgGraphImgServiceAlways;
$status = FormatJson::parse( $jsonText, FormatJson::TRY_FIXING | FormatJson::STRIP_COMMENTS );
@@ -119,41 +118,81 @@ public static function buildHtml( $jsonText, $title, $revid, $parserOutput, $isP
return $status->getWikiText();
}
$isInteractive = isset( $args['mode'] ) && $args['mode'] === 'interactive';
$data = $status->getValue();
// Figure out which vega version to use
global $wgGraphDefaultVegaVer;
$useVega2 = false;
if ( property_exists( $data, 'version' ) ) {
$ver = is_numeric( $data->version ) ? $data->version : 0;
} else {
$ver = false;
}
if ( $wgGraphDefaultVegaVer > 1 || $isInteractive ) {
if ( $ver === false ) {
// If version is not set, but we need to force vega2, insert it automatically
$data->version = 2;
}
$useVega2 = true;
} elseif ( $ver !== false ) {
$useVega2 = $ver > 1;
}
if ( $useVega2 ) {
$parserOutput->setExtensionData( 'graph_vega2', true );
}
// Calculate hash and store graph definition in graph_specs extension data
$specs = $parserOutput->getExtensionData( 'graph_specs' ) ?: array();
$data = $status->getValue();
// Make sure that multiple json blobs that only differ in spacing hash the same
$hash = sha1( FormatJson::encode( $data, false, FormatJson::ALL_OK ) );
$specs[$hash] = $data;
$parserOutput->setExtensionData( 'graph_specs', $specs );
$html = '';
$useGraphoid = !$isPreview && $wgGraphImgServiceUrl;
$loadLive = $isPreview || !$wgGraphImgServiceAlways;
$loadOnClick = !$loadLive && $useGraphoid && $isInteractive;
// Graphoid service image URL
if ( $wgGraphImgServiceUrl ) {
$imgTag = '';
if ( $useGraphoid ) {
$server = rawurlencode( $wgServerName );
$title = !$title ? '' : rawurlencode( $title->getPrefixedDBkey() );
$revid = rawurlencode( (string)$revid ) ?: '0';
$url = sprintf( $wgGraphImgServiceUrl, $server, $title, $revid, $hash );
// TODO: Use "width" and "height" from the definition if available
// In some cases image might still be larger - need to investigate
$html .= Html::rawElement( 'img', array(
'class' => 'mw-wiki-graph-img',
'src' => $url,
$imgTag = Html::rawElement( 'img', array(
'class' => 'mw-graph-img',
'src' => $url
) );
}
if ( $isPreview || !$wgGraphImgServiceUrl || !$wgGraphImgServiceAlways ) {
$html .= Html::element( 'div', array(
'class' => 'mw-wiki-graph',
'data-graph-id' => $hash,
$liveTag = '';
$containerClass = 'mw-graph-container';
if ( $loadOnClick ) {
$containerClass .= ' mw-graph-static';
$liveTag = Html::rawElement( 'div', array(
'class' => 'mw-graph-switch-button',
), wfMessage( 'graph-switch-button' )->text() );
$parserOutput->setExtensionData( 'graph_interact', true );
} else if ( $loadLive ) {
$liveTag = Html::element( 'div', array(
'class' => 'mw-graph'
) );
$liveSpecs = $parserOutput->getExtensionData( 'graph_live_specs' ) ?: array();
$liveSpecs[$hash] = $data;
$parserOutput->setExtensionData( 'graph_live_specs', $liveSpecs );
}
$attribs = array( 'class' => $containerClass );
if ( $loadOnClick || $loadLive ) {
// No point to set graph id unless we will use it on the client
$attribs['data-graph-id'] = $hash;
}
$container = Html::rawElement( 'div', $attribs, $imgTag . $liveTag );
return Html::rawElement( 'div', array(
'class' => 'mw-wiki-graph-container',
), $html );
return Html::rawElement( 'div', array(), $container );
}
}
@@ -24,6 +24,25 @@
"graph": "Graph\\ApiGraph"
},
"ResourceModules": {
"ext.graph.loader": {
"scripts": [
"js/graph-loader.js"
],
"styles": [
"styles/common.less"
],
"messages": [
"graph-loading",
"graph-loading-done"
],
"dependencies": [
"mediawiki.Uri"
],
"targets": [
"mobile",
"desktop"
]
},
"ext.graph": {
"styles": [
"styles/common.less"
@@ -42,5 +42,8 @@
"graph-ve-dialog-edit-unknown-graph-type-warning": "The type of this graph is unsupported, and any modifications made to it may break its display. Please edit the graph through the raw data specification on the '{{int:graph-ve-dialog-edit-page-raw}}' tab.",
"graph-ve-no-spec": "No graph specification found",
"graph-ve-vega-error": "Vega has encountered an error rendering this graph",
"graph-ve-vega-error-no-render": "Vega was unable to render this graph"
"graph-ve-vega-error-no-render": "Vega was unable to render this graph",
"graph-switch-button": "Make interactive",
"graph-loading": "Loading...",
"graph-loading-done": "Done!"
}
@@ -44,5 +44,8 @@
"graph-ve-dialog-edit-unknown-graph-type-warning": "Warning label about unsupported graph types",
"graph-ve-no-spec": "Label to display on a graph node when no spec is found",
"graph-ve-vega-error": "Label to display on a graph node when Vega encounters an unknown error",
"graph-ve-vega-error-no-render": "Label to display on a graph node when Vega is unable to render"
"graph-ve-vega-error-no-render": "Label to display on a graph node when Vega is unable to render",
"graph-switch-button": "Button to turn the static graph image into an interactive graph",
"graph-loading": "Label to indicate when the interactive graph is loading",
"graph-loading-done": "Label to indicate when the interactive graph has loaded"
}
@@ -0,0 +1,69 @@
( function ( $, mw ) {

mw.hook( 'wikipage.content' ).add( function () {

// Make graph containers clickable
$( '#mw-content-text' ).on( 'click', '.mw-graph-container.mw-graph-static', function () {
var $this = $( this );

// Add a class to decorate loading
$this.addClass( 'mw-graph-loading' );

// Replace the image with the graph
loadAndReplaceWithGraph( $this );
} );

/**
* Takes a graph container and renders the vega graph inside.
*
* @param {jQuery} $el Graph container.
* @param {Function} callback Method called when the graph is loaded.
*/
function renderGraph( $el, callback ) {
new mw.Api().get( {
action: 'graph',
// TODO: is this the right way to get current page title?
title: mw.config.get( 'wgPageName' ),
hash: $el.data( 'graphId' )
} ).done( function ( data ) {
vg.parse.spec( data.graph, function ( chart ) {
if ( chart ) {
chart( { el: $el[ 0 ] } ).update();
}
callback();
} );
} );
}

/**
* Replace a graph image by the vega graph.
*
* If dependencies aren't loaded yet, they are loaded first
* before rendering the graph.
*
* @param {jQuery} $el Graph container.
*/
function loadAndReplaceWithGraph( $el ) {
// TODO, Performance BUG: loading vega and calling api should happen in parallel
// Lazy loading dependencies
mw.loader.using( 'ext.graph.vega2', function () {
var $img = $el.find( 'img' ),
$button = $el.find( '.mw-graph-switch-button' );

$button.text( mw.message( 'graph-loading' ).text() );
$button.addClass( 'loading' );
renderGraph( $el, function () {
$button.text( mw.message( 'graph-loading-done' ).text() );
setTimeout( function () {
$el.removeClass( 'mw-graph-loading' );
$el.removeClass( 'mw-graph-static' );
$button.remove();
}, 1500 );
$img.remove();
} );
} );
}

} );

}( jQuery, mediaWiki ) );
@@ -3,7 +3,7 @@
mw.hook( 'codeEditor.configure' ).add( function ( session ) {
function refreshGraph() {
var spec,
el = $( '.mw-wiki-graph' ).get( 0 ),
el = $( '.mw-graph' ).get( 0 ),
content = session.getValue();

if ( typeof vg === 'undefined' || oldContent === content ) {
@@ -46,8 +46,8 @@
};
}

$content.find( '.mw-wiki-graph' ).each( function () {
var graphId = $( this ).data( 'graph-id' ),
$content.find( '.mw-graph' ).each( function () {
var graphId = $( this.parentNode ).data( 'graph-id' ),
el = this;
if ( !specs[ graphId ] ) {
mw.log.warn( graphId );

0 comments on commit b735f63

Please sign in to comment.
You can’t perform that action at this time.