Skip to content

Commit

Permalink
Merge pull request #79 from ede0m/thumbnail_FV_34
Browse files Browse the repository at this point in the history
Thumbnail fv_34 with JSDOM
  • Loading branch information
mbucknell authored Jul 31, 2017
2 parents 9c91158 + 7e7b94e commit eed3755
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,7 @@ node_modules/

*floodviz/static/geojson/*
!states.json

# Thumbnail Files #
*floodviz/thumbnail/*.json
*floodviz/thumbnail/*.png
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
'scale': None,
}

# Thumbnail Support
THUMBNAIL = False

deployed_url_base = os.environ.get('DEPLOYED_BASE_URL')
if deployed_url_base:
FREEZER_BASE_URL = deployed_url_base
Expand Down
3 changes: 3 additions & 0 deletions examples/iowa.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
'height': '4',
'width': '7.5'
}

# Thumbnail Support
THUMBNAIL = False
3 changes: 1 addition & 2 deletions floodviz/templates/hydrograph.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@

</body>

</html>

</html>
248 changes: 248 additions & 0 deletions floodviz/thumbnail/hydro_thumbnail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@

'use strict';
/**
* @param {Object} options - holds options for the configuration of the hydrograph
* Non-optional Keys include:
* @prop 'height' v(int) - height of the graph
* @prop 'width' v(int) - width of the graph
* @prop 'data' v(list) - A list of objects representing data points
* @prop 'div_id' v(string) - id for the container for this graph
*
* hydromodule is a module for creating hydrographs using d3. Pass it a javascript object
* specifying config options for the graph. Call init() to create the graph. Linked
* interaction functions for other figures should be passed to init in and object.
*
*/
var hydromodule = function (options) {

var self = {};

var margin = {top: 30, right: 20, bottom: 30, left: 50};
var height = options.height - margin.top - margin.bottom;
var width = options.width - margin.left - margin.right;

// Adds the svg canvas
var svg = null;
// Focus for hydrograph hover tooltip
var focus = null;
// Voronoi layer
var voronoi_group = null;
// Define the voronoi
var voronoi = d3.voronoi()
.x(function (d) {
return x(d.time_mili);
})
.y(function (d) {
return y(d.value);
})
.extent([[-margin.left, -margin.top], [width + margin.right, height + margin.bottom]]);
// Define the line
var line = d3.line()
.x(function (d) {
return x(d.time_mili);
})
.y(function (d) {
return y(d.value);
});
// Set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLog().range([height, 0]);

/**
* Filters a set of data based on the ids listed in display_ids
* @returns {Array} The entries of the original `data` whose `key` values are elements of display_ids.
*/
var subset_data = function (full_data) {
var toKeep = [];
full_data.forEach(function (d) {
if (options.display_ids.indexOf(d.key) !== -1) {
toKeep.push(d);
}
});
return toKeep;
};
/**
*
* Draws the svg, scales the range of the data, and draws the line for each site
* all based on the data set as it was passed in. Called as needed
* when data changes (as in removal of a line).
*
*/
var update = function () {

// Cut the data down to sites we want to display
var sub_data = subset_data(options.data);
// Remove the current version of the graph if one exists
var current_svg = d3.select(options.div_id + ' svg');
if (current_svg) {
current_svg.remove();
}
// recreate svg
svg = d3.select(options.div_id)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform',
'translate(' + margin.left + ',' + margin.top + ')');

var graph_data = sub_data.map(function (d) {
return {
'date': d.date,
'key': d.key,
'name': d.name,
'time': d.time,
'time_mili': d.time_mili,
'timezone': d.timezone,
'value': Number(d.value)
};
});

// Scale the range of the data
x.domain(d3.extent(graph_data, function (d) {
return d.time_mili;
}));
y.domain([d3.min(graph_data, function (d) {
return d.value;
}), d3.max(graph_data, function (d) {
return d.value;
})]);
// Nest the entries by site number
var dataNest = d3.nest()
.key(function (d) {
return d.key;
})
.entries(graph_data);
// Loop through each symbol / key
dataNest.forEach(function (d) {
svg.append('g')
.attr('class', 'hydro-inactive')
.append('path')
.attr('id', 'hydro' + d.key)
.attr('d', line(d.values));
console.log('Here IN DATANEST');
});
// Add the X Axis
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x).tickFormat(d3.timeFormat('%B %e')));

// Add the Y Axis
svg.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y).ticks(10, '.0f'));

// Tooltip
focus = svg.append('g')
.attr('transform', 'translate(-100,-100)')
.attr('class', 'focus');
focus.append('circle')
.attr('r', 3.5);

focus.append('text')
.attr('y', -10);

// Voronoi Layer
voronoi_group = svg.append('g')
.attr('class', 'voronoi');
voronoi_group.selectAll('path')
.data(voronoi.polygons(d3.merge(dataNest.map(function (d) {
return d.values
}))))
.enter().append('path')
.attr('d', function (d) {
return d ? 'M' + d.join('L') + 'Z' : null;
})
.on('mouseover', function (d) {
self.linked_interactions.hover_in(d.data.name, d.data.key);
self.activate_line(d.data.key);
self.series_tooltip_show(d);
})
.on('mouseout', function (d) {
self.linked_interactions.hover_out();
self.deactivate_line(d.data.key);
self.series_tooltip_remove(d.data.key);
})
.on('click', function (d) {
self.linked_interactions.click(d.data.key);
self.remove_series(d.data.key);
});

};

/**
* Initialize the Hydrograph.
*
*@param {Object} linked_interactions - Object holding functions that link to another figure's interactions.
* Pass null if there are no such interactions to link.
* @prop 'hover_in' - linked interaction function for hover_in events on this figure.
* @prop 'hover_out' - linked interaction function for hover_out events on this figure.
* @prop 'click' - linked interaction function for click events on this figure.
*
*
*/
self.init = function (linked_interactions) {
self.linked_interactions = linked_interactions;
update();
return self;
};

/**
* Returns the svg element node. Primarily used for thumb-nailing.
*/
self.get_svg_elem = function () {
return d3.select(options.div_id);
};
/**
* Displays tooltip for hydrograph at a data point in addition to
* corresponding map site tooltip.
*/
self.series_tooltip_show = function (d) {
focus.attr('transform', 'translate(' + x(d.data.time_mili) + ',' + y(d.data.value) + ')');
focus.select('text').html(d.data.key + ': ' + d.data.value + ' cfs ' + ' ' + d.data.time + ' ' + d.data.timezone);
};

/**
* Removes tooltip view from the hydrograph series
* as well as the correspond mapsite tooltip.
*/
self.series_tooltip_remove = function (sitekey) {
focus.attr('transform', 'translate(-100,-100)');
};

/**
* Removes a line from the hydrograph. This resizes data
* appropriately and removes accents from the corresponding
* site on the map.
*/
self.remove_series = function (sitekey) {
var keep_ids = FV.hydrograph_display_ids;
keep_ids.splice(FV.hydrograph_display_ids.indexOf(sitekey), 1);
self.change_lines(keep_ids);
};
/**
* Update the value of display_ids and call update to redraw the graph to match.
* @param new_display_ids The new set of gages to be displayed.
*/
self.change_lines = function (new_display_ids) {
FV.hydrograph_display_ids = new_display_ids;
update();
};
/**
* Highlight a line.
* @param sitekey the site number of the line to be highlighted
*/
self.activate_line = function (sitekey) {
d3.select('#hydro' + sitekey).attr('class', 'hydro-active');
};
/**
* Un-highlight a line
* @param sitekey the site number of the line to be un-highlighted
*/
self.deactivate_line = function (sitekey) {
d3.select('#hydro' + sitekey).attr('class', 'hydro-inactive');
};

return self
};
100 changes: 100 additions & 0 deletions floodviz/thumbnail/thumbnail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
*
* This Script is intended to be run after a flask freeze during the build process.
*
* Its main objective is to dynamically create thumbnails for the site figures
* based on the data obtained from our server side flask services.
*
* */



// Dependency Import
var fs = require('fs');
var jsdom = require('jsdom/lib/old-api.js');
var svg2png = require('svg2png');
// Data imports
var data_hydro = require('../thumbnail/hydrograph_data.json');

// Collect script arguments for external css
var style_path = null;
var args = process.argv.splice(process.execArgv.length + 2);
if (args.length > 2) {
console.log('\nUsage: node thumbnail.js ' +
'\n\nOptional flag: -css path/to/css/file.css\n');
process.exit();
} else {
if (args[0] === '-css') {
style_path = args[1];
} else {
console.log('\nUnrecognized argument: ' + args[0] + '\n');
process.exit();
}
}

// Headless Browser Start for DOM
jsdom.env(

// create DOM hook
"<html><body><div id='hydrograph'></div>" +
"<div id='map'></divid>" +
"</body></html>",

// load local assets into window environment
[
'./floodviz/static/bower_components/d3/d3.js',
'./floodviz/static/bower_components/proj4/dist/proj4.js',
'./floodviz/thumbnail/hydro_thumbnail.js'
],

function (err, window) {
var hydro_figure = window.hydromodule(
{
'height': 300,
'width': 560,
'div_id': '#hydrograph',
'data': data_hydro,
"display_ids": ['05471200', '05476750', '05411850', '05454220',
'05481950', '05416900', '05464500', '05487470']
// Refactor Later. I'm assuming this will change with references.json
}
);
convert(hydro_figure,window, 'floodviz/static/css/hydrograph.css', 'floodviz/thumbnail/thumbnail_hydro.png');
}
);

// Wrapper around svg2png that injects custom css to inline svg before conversion
function convert(figure, window, css_path, filename) {
var style_ext = null;
var svg_string = null;
var svg = figure.get_svg_elem().node();
var style_default = fs.readFileSync(css_path, 'utf8');
figure.init();
if (style_path !== null) {
try {
style_ext = fs.readFileSync(style_path, 'utf8');
} catch(error) {
console.log('\nError: external css file path not found.\nUsing only default style.\n\n' + error);
style_ext = null;
}
svg_string = inject_style(style_default, style_ext, svg, window);
} else {
svg_string = inject_style(style_default, null, svg, window);
}
// Takes care of canvas conversion and encodes base64
svg2png(svg_string)
.then(buffer => fs.writeFile(filename, buffer))
.then(console.log('\nConverted D3 figure to PNG successfully... \n'))
.catch(e => console.error(e));
}

// Hook style to inline svg string.
function inject_style(style_string, ext_style, svgDomElement, window) {
var s = window.document.createElement('style');
s.setAttribute('type', 'text/css');
s.innerHTML = "<![CDATA[\n" + style_string + ext_style + "\n]]>";
var defs = window.document.createElement('defs');
defs.appendChild(s);
svgDomElement.insertBefore(defs, svgDomElement.firstChild);
return svgDomElement.parentElement.innerHTML;
}
Loading

0 comments on commit eed3755

Please sign in to comment.