Skip to content

Instantly share code, notes, and snippets.

@vaab
Last active October 13, 2015 21:28
Show Gist options
  • Save vaab/4258891 to your computer and use it in GitHub Desktop.
Save vaab/4258891 to your computer and use it in GitHub Desktop.
A google chart like Canvas widget for drawing circles and coloring region in "La Réunion"
.tooltip_map {
font-size: 12px;
width: 200px;
position: absolute;
display: none;
z-index: 999;
padding: 3px;
margin: 0px;
border: 1px solid #efefef;
border-radius: 4px;
background-color: #fff;
opacity: 0.95;
text-align: left;
}
.tooltip_map h3 {
margin-top: 0.5em;
padding-left: 1em;
}
dl.select_box {
color: #222;
border-radius: 4px;
background: #ddd;
margin: 0;
padding: 4px;
width: 200px;
font-size: 12px;
position: absolute;
top: 0px;
right: 0px;
display: block;
text-align: left;
}
dl.select_box dt {
margin: 0;
clear: left;
float: left;
font-weight: bold;
}
dl.select_box dd {
margin: 0;
text-align: right;
border-bottom: 1px gray dotted;
padding: 0;
}
dl.select_box dd select.select_col_indicator {
height: none;
border: 0;
color: #446;
background: none;
padding: 0;
margin:0;
height: 16px;
}
.tooltip_map dl {
margin: 0;
padding: 0px;
width: 100%;
}
.tooltip_map dt {
margin: 0;
clear: left;
float: left;
font-weight: bold;
width: 120px;
border-bottom: 1px gray dotted;
white-space: nowrap;
}
.tooltip_map dd {
margin: 0;
text-align: right;
border-bottom: 1px gray dotted;
}
function is_undef(a) {
return (typeof a === "undefined");
}
function rgb2htmlrgb(rgb) {
return "rgb(" + parseInt(rgb[0], 10) + ", " +
parseInt(rgb[1], 10) + ", " +
parseInt(rgb[2], 10) + ")";
}
var MapCanvas = function(element) {
var zip2name = {
"97411": "Saint-Denis",
"97418": "Sainte-Marie",
"97420": "Sainte-Suzanne",
"97409": "Saint-André",
"97402": "Bras-Panon",
"97410": "Saint-Benoît",
"97419": "Sainte-Rose",
"97417": "Saint-Philippe",
"97412": "Saint-Joseph",
"97405": "Petite-Île",
"97416": "Saint-Pierre",
"97414": "Saint-Louis",
"97404": "L'Étang-Salé",
"97401": "Les Avirons",
"97413": "Saint-Leu",
"97423": "Trois Bassins",
"97415": "Saint Paul",
"97407": "Le Port",
"97408": "La Possession",
"97421": "Salazie",
"97424": "Cilaos",
"97403": "Entre-Deux",
"97422": "Le Tampon",
"97406": "La Plaine-des-Palmistes"
};
var coords_path = {
"97411": "M285.279,165.089L273.112,169.681L256.13,142.777L229.229,128.963L209.504,134.01L209.659,126.163L208.907,118.298L173.145,33.056L192.225,14.712L204.44,7.707L231.1,3.099L251.659,1.692L259.524,0.94L275.536,15.748L299.077,16.211L322.01,16.014L322.01,16.014L328.995,60.88L300.089,118.281L304.538,137.691L285.418,164.893",
"97418": "M322.01,16.014L328.995,60.88L300.089,118.281L304.538,137.691L285.279,165.089L304.667,161.848L309.129,149.859L329.688,148.452L334.646,157.305L352.5,155.241L364,153.958L401.122,78L398.497,26.802L398.794,26.822L358.706,24.027L343.129,17.682L331.086,15.936L322.01,16.014",
"97420": "M461.178,45.905L459.401,62.238L480.301,74.123L468.299,85.661L458.76,94.834L450.682,91.051L434.436,111.111L419.736,113.993L394.938,146.716L364,153.958L401.122,78L398.497,26.802L398.794,26.822L431.067,29.073L443.47,43.206L460.653,45.825",
"97409": "M539.235,113.936L527.156,117.314L499.135,129.745L484.832,120.104L411.65,156.706L411.65,156.707L394.938,146.716L419.736,113.993L434.436,111.111L450.682,91.051L458.76,94.834L468.299,85.661L480.301,74.123L459.401,62.238L461.178,45.905L460.653,45.825L484.411,49.446L512.221,78.374L524.889,94.323L539.393,109.102L539.235,113.935",
"97402": "M537.886,154.963L530.645,154.822L520.987,154.631L504.37,170.608L505.895,185.13L490.65,192.68L459.676,201.733L412.52,250.623L382.944,234.643L387.75,205.148L382.407,200.515L387.254,168.908L484.832,120.104L499.135,129.745L527.156,117.314L539.235,113.936L539.235,113.935",
"97410": "M382.944,234.643L348.258,247.851L343.795,259.839L316.821,280.444L327.671,312.057L363.285,312.758L405.018,309.352L416.73,297.204L422.163,297.31L429.98,298.974L495.078,274.29L558.275,346.182L577.721,339.924L582.833,295.038L615.402,266.39L591.615,232.409L577.709,217.944L577.792,213.718L578.746,211.322L577.959,205.268L550.147,176.339L537.886,154.963L530.645,154.822L520.987,154.631L504.37,170.608L505.895,185.13L490.65,192.68L459.676,201.733L412.52,250.623",
"97419": "M659.1,439.346L519.189,429.953L485.495,423.855L492.762,391.994L497.237,379.402L558.275,346.182L577.721,339.924L582.833,295.038L615.402,266.39L637.557,291.282L661.879,298.1L686.91,314.897L698.269,351.351L698.163,356.784L684.777,361.955L673.801,397.971",
"97417": "M552.215,589.395L527.494,498.955L534.656,472.527L522.122,465.034L519.189,429.953L659.1,439.346L667.969,540.967L667.886,545.193L630.317,582.497",
"97412":"M421.135,593.922L426.307,547.083L419.449,542.721L420.793,520.405L436.641,512.869L432.205,492.852L429.519,475.892L440.638,401.837L445.657,392.274L470.69,378.275L492.762,391.994L485.495,423.855L519.189,429.953L522.122,465.034L534.656,472.527L527.494,498.955L552.215,589.395L526.88,591.632L518.729,606.871L442,608.984L424.176,594.141L420.125,593.852",
"97405":"M358.055,585.087L371.663,531.515L407.526,519.54L423.151,492.674L432.205,492.852L436.641,512.869L420.793,520.405L419.449,542.721L426.307,547.083L421.135,593.922L420.788,593.899L371.715,590.391L357.603,584.913",
"97416":"M358.055,585.087L371.663,531.515L407.526,519.54L423.151,492.674L432.205,492.852L429.519,475.892L414.915,481.643L409.776,497.242L357.934,523.395L354.383,519.702L337.755,505.487L306.217,451.123L290.429,455.644L281.479,480.83L273.054,479.454L213.875,510.294L229.697,534.759L267.565,543.657L276.387,555.605L282.423,555.724L358.184,585.137",
"97414":"M196.665,372.278L204.229,356.123L213.568,341.813L220.146,344.961L257.497,380.116L274.825,374.118L280.375,383.585L273.054,479.454L213.875,510.294L189.399,495.923L201.426,467.78",
"97404":"M128.627,454.271L156.43,453.005L164.574,438.07L153.42,421.849L179.969,399.875L186.403,372.076L196.665,372.278L201.426,467.78L189.399,495.923L140.152,482.275L132.567,468.841",
"97401":"M119.419,446.543L211.415,328.488L213.568,341.813L204.229,356.123L196.665,372.278L186.403,372.076L179.969,399.875L153.42,421.849L164.574,438.07L156.43,453.005L128.627,454.271",
"97413":"M63.599,321.958L72.129,318.198L120.935,292.891L210.933,291.643L219.699,297.364L219.566,297.851L211.415,328.488L119.419,446.543L90.47,429.367L80.803,398.984L75.809,391.941L86.806,370.118L77.741,339.747L66.439,331.07",
"97423":"M63.599,321.958L72.129,318.198L120.935,292.891L210.933,291.643L220.472,297.869L228.402,293.797L229.68,290.2L204.709,270.385L165.58,264.181L105.219,262.994L76.587,275.714L47.931,289.644",
"97415":"M118.574,71.52L125.634,84.656L113.229,101.319L112.897,118.221L138.662,128.389L163.896,134.924L182.535,154.312L207.146,177.139L202.718,187.317L210.752,224.007L223.765,230.152L211.064,254.207L229.148,271.168L254.536,254.458L276.266,254.886L276.173,259.715L270.963,278.935L264.271,281.521L229.68,290.2L204.709,270.385L165.58,264.181L105.219,262.994L76.587,275.714L47.931,289.644L8.581,248.714L8.705,242.376L9.014,226.682L9.168,218.835L1.068,208.562L6.252,198.247L24.102,181.086L31.345,181.229L39.795,181.395L63.113,162.531L69.978,151.193L75.814,130.776L68.382,109.496L78.256,68.024L113.136,75.353",
"97407":"M118.574,71.52L125.634,84.656L113.229,101.319L112.897,118.221L96.802,107.638L68.382,109.496L78.256,68.024L113.136,75.353",
"97408":"M273.112,169.681L273.514,179.954L273.365,187.5L264.257,190.038L266.316,208.194L266.079,220.266L265.054,226.284L262.503,233.178L268.681,241.452L276.266,254.886L254.536,254.458L229.148,271.168L211.064,254.207L223.765,230.152L210.752,224.007L202.718,187.317L207.146,177.139L182.535,154.312L163.896,134.924L138.662,128.389L112.897,118.221L113.229,101.319L125.634,84.656L118.574,71.52L173.145,33.056L208.907,118.298L209.659,126.163L209.504,134.01L229.229,128.963L256.13,142.777",
"97421":"M276.216,257.484L276.266,254.886L268.681,241.452L262.503,233.178L265.054,226.284L266.079,220.266L266.316,208.194L264.257,190.038L273.365,187.5L273.514,179.954L273.112,169.681L285.279,165.089L304.667,161.848L309.129,149.859L329.688,148.452L334.646,157.305L352.5,155.241L364,153.958L394.938,146.716L411.65,156.707L411.65,156.706L387.254,168.908L382.407,200.515L387.75,205.148L382.944,234.643L348.258,247.851L343.795,259.839L316.821,280.444L276.226,256.999",
"97424":"M213.568,341.813L211.415,328.488L219.566,297.851L220.472,297.869L228.402,293.797L229.68,290.2L264.271,281.521L270.963,278.935L276.173,259.715L276.213,257.655L276.226,256.999L316.821,280.444L327.671,312.057L312.104,366.702L280.375,383.585L274.825,374.118L257.497,380.116L220.146,344.961",
"97403":"M327.671,312.057L312.104,366.702L280.375,383.585L273.1,478.864L273.054,479.454L281.479,480.83L290.429,455.644L306.217,451.123L337.712,415.514L334.896,405.193L358.427,375.463L374.76,373.972L381.584,364.748L363.285,312.758",
"97422":"M363.285,312.758L381.584,364.748L374.76,373.972L358.427,375.463L334.896,405.193L337.712,415.514L306.217,451.123L307.267,452.931L337.755,505.487L354.383,519.702L357.934,523.395L409.776,497.242L414.915,481.643L429.519,475.892L440.638,401.837L445.657,392.274L470.69,378.275L492.762,391.994L497.237,379.402L470.844,339.632L460.126,347.271L459.889,359.343L450.249,365.948L440.082,368.616L420.78,352.235L405.018,309.352L363.695,312.725",
"97406":"M557.961,346.353L497.721,379.138L497.237,379.402L470.844,339.632L460.126,347.271L459.889,359.343L450.249,365.948L440.082,368.616L420.78,352.235L405.018,309.352L416.73,297.204L422.163,297.31L429.98,298.974L495.078,274.29L558.275,346.182"
};
var coords_circle = {
"97411":Array(270,90),
"97418":Array(360,100),
"97420":Array(430,80),
"97409":Array(510,95),
"97402":Array(460,170),
"97410":Array(560,250),
"97419":Array(645,370),
"97417":Array(610,530),
"97412":Array(490,550),
"97405":Array(395,565),
"97416":Array(310,545),
"97414":Array(245,460),
"97404":Array(180,460),
"97401":Array(170,398),
"97413":Array(120,360),
"97423":Array(120,280),
"97415":Array(70,190),
"97407":Array(90,90),
"97408":Array(175,110),
"97421":Array(330,210),
"97424":Array(290,340),
"97403":Array(340,370),
"97422":Array(395,460),
"97406":Array(490,330)
};
var generalOutline = "M577.792,213.718L577.709,217.944L591.615,232.409L615.402,266.39L637.557,291.282L661.879,298.1L686.91,314.897L698.269,351.351L698.163,356.784L684.777,361.955L673.801,397.971L659.1,439.346L667.969,540.967L667.886,545.193L630.317,582.497L526.88,591.632L518.729,606.871L442,608.984L424.176,594.141L371.715,590.391L282.423,555.724L276.387,555.605L267.565,543.657L229.697,534.759L213.875,510.294L189.399,495.923L140.152,482.275L132.567,468.841L128.627,454.271L119.419,446.543L90.47,429.367L80.803,398.984L75.809,391.941L86.806,370.118L77.741,339.747L66.439,331.07L63.599,321.958L47.931,289.644L8.581,248.714L8.705,242.376L9.014,226.682L9.168,218.835L1.068,208.562L6.252,198.247L24.102,181.086L31.345,181.229L39.795,181.395L63.113,162.531L69.978,151.193L75.814,130.776L68.382,109.496L78.256,68.024L113.136,75.353L173.145,33.056L192.225,14.712L204.44,7.707L231.1,3.099L251.659,1.692L259.524,0.94L275.536,15.748L299.077,16.211L331.086,15.936L343.129,17.682L358.706,24.027L431.067,29.073L443.47,43.206L484.411,49.446L512.221,78.374L524.889,94.323L539.393,109.102L537.886,154.963L550.147,176.339L577.959,205.268L578.746,211.322";
function drawOuterGeneralOutline(paper) {
paper.path(generalOutline);
}
function drawInnerOutlines(paper, coords_path) {
return $.each(coords_path, function(_index, value) {
paper.path(value);
});
}
/*
* Indicators
*/
// XXXvlab: these two common_xxx should go in a common parent for
// indicators objects. Didn't yet choose an independant object model.
// might use: http://ejohn.org/blog/simple-javascript-inheritance/
// Indicator.indicator_draw
//
// Main method to draw current Indicator object. It could be a square
// a region, a bar or anything situated anyware on the canvas. And reacting
// to any events (mouseover, click...).
function common_indicator_draw(rows, columns) {
var self = this;
var col_nb = this.col_nb;
if (col_nb === 0) return; // nothing to do (this indicator is disabled)
var column = columns[col_nb];
var $elt = this._map.$elt;
// Create the tooltip element if not already existent.
var tooltip_id = 'tooltip_map_col_' + col_nb;
var $tooltip = $elt.find("#" + tooltip_id);
if ($tooltip.length === 0) {
$elt.append($('<div class="tooltip_map"' +
'id="' + tooltip_id +
'"></div>'));
$tooltip = $elt.find("#" + tooltip_id);
}
_(rows).each(function(elt, row_nb) {
var header = elt[0]; // row name
var value = parseInt(elt[col_nb], 10);
var ratio = value / column.max;
// draws an individual widget for one data identified
// by it's ratio to the max of the same column.
var p = self.draw_id_ratio(header, ratio);
if (p === false) return; // something went wrong
var str = "<h3>" + zip2name[header] + "</h3><dl>";
_(columns).each(function(column, idx) {
if (idx !== 0) {
str += "<dt>" + column.label + "</dt> ";
str += "<dd>" + parseInt(elt[idx], 10) + "</dd>";
}
});
str += "</dl>";
p.data("text", str);
p.data("row", row_nb); // storing value for click event
p.mouseover(function (e) {
self.style_higlighted(this);
// Compatibility code for firefox
var x = e.hasOwnProperty('offsetX') ? e.offsetX : e.layerX;
var y = e.hasOwnProperty('offsetY') ? e.offsetY : e.layerY;
$tooltip.html(this.data('text'))
.css({
top: (y - 20) + 'px',
left: (x + 20) + 'px'
})
.fadeIn(300);
});
p.mouseout(function () {
self.style_unhighlighted(this);
$tooltip.stop(true, true).hide();
});
p.click(function(_event) {
if (!is_undef(self._map.on_click_row))
self._map.on_click_row(this.data('row'));
});
});
this.draw_legend.apply(this, [col_nb, column]);
}
// Indicator.draw_select
//
// Draws the widget to select the column to be feeded in
// current indicator.
function common_draw_select (columns, label) {
var self = this;
var $select = $("<select class='select_col_indicator'>");
_(columns).each(function(column, idx) {
var label = "";
if (idx !== 0)
label = column.label;
var selected = false;
if (idx === self.col_nb)
selected = true;
var $opt = $('<option value="' + idx + '" />')
.text(label)
.attr('selected', selected);
$select.append($opt);
});
var $dt = $('<dt />').text(label);
var $dd = $('<dd />').append($select);
this._map.$select_box.append($dt).append($dd);
$select.change(function () {
var sel = parseInt($select.find('option:selected').val(), 10);
self.col_nb = sel;
self._map.real_draw();
});
}
CircleIndicator = function() {
var obj = {
// Indicator.init
//
// Initialize Indicator data structure. Should default options received
//
// Arguments:
// map: <map object> the current map on which this Indicator will be part of.
// col_nb: from which default column this widget will take values to render ?
init: function (map, col_nb) {
this._map = map;
this.col_nb = col_nb;
this.paper = this._map.paper;
options = this._map.options;
if (is_undef(options.circleMaxRadius))
options.circleMaxRadius = 40;
if (is_undef(options.circleMinRadius))
options.circleMinRadius = 3;
this.options = options;
},
draw: common_indicator_draw,
// Indicator.draw_id_ratio
//
// Draw a single indicator thanks to an identifier and a ratio.
//
// id: (any type) identifier for the current indicator (ie: a zip code)
// ratio: (float between 0 and 1) ratio to the max value in rendered set of values.
draw_id_ratio: function (id, ratio) {
var coords = coords_circle[id]; // convert identifier to canvas positions
if (is_undef(coords)) {
console.warn("skip drawing circle for '" + id +"'.");
return false;
}
return this.draw_pos_ratio(coords[0], coords[1],
ratio);
},
// Indicator.draw_id_ratio
//
// Draw a single indicator thanks to a canvas position and a ratio.
draw_pos_ratio: function (x, y, ratio) {
options = this.options;
var radiusRange = options.circleMaxRadius -
options.circleMinRadius;
var radius = options.circleMinRadius + (ratio * radiusRange);
var c = this.paper.circle(x, y, radius);
this.style_unhighlighted(c);
return c;
},
// Indicator.style_unhighlighted
//
// Apply unhighlight style to the given elt
style_unhighlighted: function (elt) {
elt.attr("fill", "#eee");
elt.attr("stroke", "#777");
elt.attr("stroke-width", 1);
elt.attr("fill-opacity", 0.9);
},
// Indicator.style_highlighted
//
// Apply highlight style to the given elt
style_higlighted: function (elt) {
elt.attr("fill", "#fee");
elt.attr("stroke", "#444");
elt.attr("stroke-width", 2);
elt.attr("fill-opacity", 0.95);
},
// Indicator.draw_legend
//
// Draw full legend for this indicator
draw_legend: function (col_nb, column) {
this.paper.text(550, 630, column.label + " :")
.attr({"font-size": 18});
var self = this;
_([[450, 0 , column.min],
[550, 0.5, column.max/2],
[650, 1 , column.max]]).each(function (elt) {
var x = elt[0];
var ratio = elt[1];
var value = elt[2];
self.draw_pos_ratio(x, 700, ratio);
self.paper.text(x, 650, value)
.attr({"font-size": 18});
});
},
common_draw_select: common_draw_select,
draw_select: function (columns) {
this.common_draw_select(columns, "cercle");
}
};
obj.init.apply(obj, arguments);
return obj;
};
RegionIndicator = function() {
var obj = {
init: function (map, col_nb) {
this._map = map;
this.col_nb = col_nb;
this.paper = this._map.paper;
options = this._map.options;
if (is_undef(options.regionColorStart))
options.regionColorStart = [255, 255, 255];
if (is_undef(options.regionColorStop))
options.regionColorStop = [55, 155, 255];
this.options = options;
},
draw: common_indicator_draw,
draw_id_ratio: function (id, ratio) {
var path = coords_path[id];
if (is_undef(path)) {
console.warn("skip drawing region for '" + id +"'.");
return false;
}
return this.draw_pos_ratio(path, ratio);
},
draw_pos_ratio: function (path, ratio) {
var options = this.options;
var rgb = _(options.regionColorStart).map(function(start, idx) {
var end = options.regionColorStop[idx];
return parseInt(start + (end - start) * ratio, 10);
});
var p = this.paper.path(path);
p.attr("fill", rgb2htmlrgb(rgb));
this.style_unhighlighted(p);
return p;
},
style_unhighlighted: function (elt) {
elt.attr("stroke-width", 1);
elt.attr("fill-opacity", 1);
},
style_higlighted: function (elt) {
elt.attr("stroke-width", 3);
elt.attr("fill-opacity", 1);
},
draw_legend: function (col_nb, column) {
var options = this.options;
var left = 50;
var top = 660;
var width = 200;
var r = this.paper.rect(left, top + 40, width, 10);
r.attr("fill", "0-" + rgb2htmlrgb(options.regionColorStart) +
"-" + rgb2htmlrgb(options.regionColorStop));
r.attr("stroke-width", 0);
this.paper.text(left + width/2, top, column.label + " :")
.attr({"font-size": 18});
this.paper.text(left, top + 25, column.min)
.attr({"font-size": 18});
this.paper.text(left + width, top + 25, column.max)
.attr({"font-size": 18});
},
common_draw_select: common_draw_select,
draw_select: function (columns) {
this.common_draw_select(columns, "couleur");
}
};
obj.init.apply(obj, arguments);
return obj;
};
// final Map object
var MapObject = {
init: function(element) {
this.$elt = $(element);
this.$select_box = $("<dl class='select_box' />");
this.$elt.append(this.$select_box);
},
/*
* Setup canvas to the given div size
*/
setup_canvas: function(options) {
// XXXvlab: must scale all resolution parameters
var origWidth = 700;
var origHeight = 810;
var width = options.width || $(element).width();
var height = options.height || $(element).height();
if (height !== 0 || width !== 0) {
// Maintain aspect ratio ?
var origRatio = parseFloat(origWidth) / origHeight;
if (height === 0 || height * origRatio > width) {
height = parseInt(width / origRatio, 10);
} else {
width = parseInt(height * origRatio, 10);
}
}
this.paper = Raphael(element, origWidth + 100, origHeight);
this.paper.setViewBox(0,0,origWidth + 100,origHeight,true);
this.$svg = this.$elt.find("svg");
if (height !== 0 || width !== 0 ) {
this.$svg.attr('width', width);
this.$svg.attr('height', height);
if (height !== 0)
this.$elt.height(height);
if (width !== 0)
this.$elt.width(width);
}
this.options = options;
// list of indicators to be rendered
this.indicators = [RegionIndicator(this, 1),
CircleIndicator(this, 2)];
},
/**
* Initialize canvas and data, then call ``_draw`` which will
* effectively use both to draw all data on canvas.
*/
draw: function(data, options) {
this.setup_canvas(options);
// Get pertinent google data out from data object.
var rows = _(data.D).map(function (elt) {
return _(elt.c).map(function (subelt) {
return subelt.v;
});
});
var columns = data.z;
// extract code from first column
var replace_pattern = /^([^ ]+).*$/;
_(rows).each(function (row) {
row[0] = row[0].replace(replace_pattern, "$1");
});
// find min and max of drawable indicators
_(columns).each(function (column, col_number) {
data = _(rows).map(function (elt) {
return parseInt(elt[col_number], 10);
});
data = _(data).reject(isNaN);
column.max = _(data).max() || 0;
column.min = _(data).min();
});
this.rows = rows;
this.columns = columns;
this.real_draw();
_(this.indicators).each(function(indicator) {
indicator.draw_select(columns);
});
},
real_draw: function() {
var self = this;
this.paper.clear();
drawOuterGeneralOutline(this.paper);
_(this.indicators).each(function(indicator) {
indicator.draw(self.rows, self.columns);
});
}
};
// Launching MapObject.init method at instanciation.
MapObject.init.apply(MapObject, arguments);
return MapObject;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment