Skip to content

Instantly share code, notes, and snippets.

@nbremer
Last active October 3, 2022 03:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nbremer/5cd07f2cb4ad202a9facfbd5d2bc842e to your computer and use it in GitHub Desktop.
Save nbremer/5cd07f2cb4ad202a9facfbd5d2bc842e to your computer and use it in GitHub Desktop.
Linear SVG Gradient - A hexagonal SOM heatmap with color legend

This is an example from my blog on Creating a smooth color legend with an SVG gradient. The color legend below is just a simple rectangle filled with an SVG gradient. But in for this particular data it works well, because you are mostly interested in trends, to get a general sense of then numbers. Therefore, it is not imperative to be able to read the exact value that each color represents. And in those cases, when you work with a quantitative color scale, I prefer to use smooth color legends.

The map you see is the visual output from a Machine Learning Technique to cluster data called Self-Organizing Maps. If you want to learn more about this fabolous technique, see my SOM blog series

You can other SVG legend gradient examples here:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- D3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<!-- Google Font -->
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,400,700' rel='stylesheet' type='text/css'>
<style>
html { font-size: 62.5%; }
body {
font-size: 1.6rem;
font-family: 'Open Sans', sans-serif;
font-weight: 300;
fill: #7A7A7A;
text-align: center;
}
.title {
font-size: 3.6rem;
fill: #4F4F4F;
font-weight: 300;
text-anchor: middle;
}
.subtitle {
font-size: 1.6rem;
fill: #AAAAAA;
font-weight: 300;
text-anchor: middle;
}
.legendTitle {
text-anchor: middle;
font-size: 2.2rem;
fill: #4F4F4F;
font-weight: 300;
}
.axis path,
.axis tick,
.axis line {
fill: none;
stroke: none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="script.js"></script>
</script>
</body>
</html>
///////////////////////////////////////////////////////////////////////////
//////////////////// Set up and initiate svg containers ///////////////////
///////////////////////////////////////////////////////////////////////////
var somData = [
38.2193453101216,33.020120773029,31.7807924365245,28.5162228652405,25.8765115311897,26.9870442843523,25.8636404573413,27.2351023013936,27.0202182049555,28.3801398707935,27.8799349753258,26.8105382815229,25.4953695573239,26.7254481293495,25.0818572157539,25.3024031084765,25.6680861415162,26.5607820147225,25.3280531699125,25.7540413884673,26.2954356904748,27.973405863739,26.8887942590823,26.5219167761773,26.043630062134,26.4409220954635,26.6577004058886,26.710482864653,25.2664776221984,27.2323899354711,34.4111906719027,32.4234996440721,29.8963606493154,29.1280730761604,26.3815186159375,25.8229712673559,27.8548836288696,28.8044647499776,27.5856400318691,27.4609343722648,28.2049017676136,25.2161052624013,24.8224315714855,24.806563463086,26.1151582491239,25.5211166909408,26.0180904856892,23.8035962261487,25.6463678132482,26.8530457276178,25.7784168660232,24.7430413707192,27.4371137867384,25.378860838464,27.8414255748023,25.4091439989787,24.4229420455188,23.9162218490174,25.7065956827934,25.6780148186109,38.7729640031501,34.2056301572248,30.4102894759445,30.7844202937604,29.0486528273772,28.9270918776383,22.4201141160791,24.8753774948952,28.1152874980377,26.9186678446674,26.6958273044124,27.6343779233213,25.3431041081171,25.1202953148256,24.8697683570792,25.0789109772934,25.2366317571879,23.2985254272131,25.8998720926769,27.4676912774205,25.7656822365713,24.0977702377503,24.5454043228993,25.578729899993,27.3186002351764,27.5285147680744,27.0571623652399,24.3269316618918,25.1389882048621,25.9965860536751,39.4953376400115,31.5334886309482,29.3736337226769,30.3542382210226,28.6902348387851,26.0668586047144,25.475659924738,24.7868089556462,26.010215869303,26.6037999924197,27.1741481277207,26.2434788121475,23.7415205871645,23.1362388910774,24.5332718510122,23.8998414729929,23.9457313143074,24.7146415600113,25.8170221531247,27.5007220251667,24.3875311327747,25.1737070322293,24.3927686486866,23.4245395344888,26.0187353516123,28.3297808420317,26.4348686326519,24.6101678778941,24.6845557756389,22.9418860001318,29.9652353385065,30.2053000365783,31.4606537558742,30.1479629058347,27.7213516072432,28.8661250609261,26.4112657332978,23.5098719481587,24.9473979740222,27.2541161230829,27.0013696364907,27.9647129546899,25.1375338531071,24.3529854682751,23.9312513957895,26.0343028379746,23.87302635663,22.7928727260949,26.0179726040772,27.3771578518824,25.4734109653208,24.9012197739925,25.0736572744134,25.2767205867819,25.1280939331434,24.3405355038782,25.5595180841604,23.9664522643439,22.8057125647914,22.9239128041527,29.2369638021695,29.1151824200556,29.4305712323595,27.318185896347,28.0707969854306,26.424935275968,26.1186702027417,23.4866323369142,25.1680665089741,27.948846051622,25.7616612275365,27.1580016059151,25.3629867060756,25.5462253458742,24.2829910043312,24.9153934469106,24.3771104039249,24.5376670182499,26.1957622609739,25.3515978871827,25.2160134575926,24.7164071063405,26.0683527448076,26.127541326265,25.2154706799988,28.7281637347421,28.7771176183584,24.4203392505691,24.4525987158063,21.615297382137,32.3694426633845,28.8054563531785,29.2311382826617,27.9375065226088,26.9610622072733,26.3518488346519,25.0165402747009,25.9532107158998,24.4008663245514,26.5287095006756,24.9300289404218,27.0423780437278,24.8529696712915,26.0005516675389,24.6694538091611,24.654792991387,24.487211280643,24.6887418424073,26.8566495691254,26.3392428345064,24.5941493430392,24.0128206959246,25.6936911094293,27.2089909077337,26.9801525730117,27.0113133921983,28.0452768341477,24.7726233616637,24.9750266240046,21.9628863966104,30.9906169199273,28.884075879866,27.3952074612876,26.9523356032079,24.9049046739381,25.5404026246996,24.2505498045073,26.4834766471769,25.8639161319873,26.9500585361948,25.7871261572919,26.6142003757156,25.3484132415404,28.2297292411913,25.071800858827,25.9750743361363,24.5587997939683,24.8846434484375,25.5070634348068,24.1134129114272,23.3825060749807,25.0592029771182,24.8757092418092,25.3688776274598,27.0861906696249,24.5443859970733,27.1415831186987,22.2563941189098,24.7172727701805,21.8130376266888,30.7643981753338,28.1237100886953,28.7221804660127,27.4669438650398,25.653518301863,25.313617405818,24.7332604220806,25.5232043620192,26.1678658793307,24.9683665497029,26.8480212801311,24.92497171727,27.8251619140298,27.20186179127,26.6091318771311,24.861351698699,25.433897064457,25.2284724371369,23.5210207695295,24.8162801638591,23.7747210164635,24.0791076153014,24.7699567851052,23.7328217218489,24.6043054318472,25.3547171774806,27.1967669350554,23.4767266146955,23.3293614328209,21.9632202620083,31.9120673654394,30.2221528204371,27.0489273477743,25.5949675729736,25.3017143662808,25.5253344054198,24.6225501009872,26.9215873193399,27.2708937931791,28.6121007126393,26.5027046154687,27.3704012551716,26.1993161478538,26.4040536457275,25.8838661574317,25.0950681912339,25.2432549814615,25.9358651288229,24.2413768200348,23.6766462933874,23.8722026023734,24.8343049034927,25.0324396431489,22.5916067939201,24.3584671035339,22.7285122185464,21.8671667812295,20.4654989281976,23.6954453252688,23.6817423595568,34.4728405287312,33.1510223691606,32.9793462292923,29.0358776048129,28.6054665675248,28.9827278787359,29.3638845023286,26.7987899801893,28.402424265323,27.3804655352126,27.2534013632306,27.1368264684383,27.7825687850757,25.9497162988162,25.3547979751792,26.1591110925415,25.0691517447405,25.3986389483316,24.2641330810026,22.6786139186354,22.7762123564837,23.6836866652087,24.0384316361549,23.8800213007139,22.2672716118434,23.5485351885966,21.0421931486547,22.2569920040668,22.8503753555803,25.8647660641653,34.7401105200476,32.7992401396413,33.6289279987359,31.8010995374308,29.5485109117933,29.1938280257727,30.0313394890792,29.4380594444791,28.3509202953959,26.8064832021604,28.0462337408102,27.6727547070175,26.5304539737713,25.1887275557566,26.949127330485,25.9626708893491,24.0924090591614,23.7539273763693,22.9387195912807,22.2429719234857,21.0337051117656,23.8106789857633,25.1039048402591,24.3157512758701,22.4213288267696,20.8819420056309,23.0553840748856,22.0262762949859,24.2379349967116,24.8616724971045,34.9607382814996,37.6515074391123,34.1983208183156,35.2842959508722,29.68300939208,29.2574688553368,28.6325851742033,28.6337340117232,27.748499475592,29.0706574988211,26.7442851274947,27.3449896695197,26.7143905741314,24.1274144097127,25.4228805001795,25.0392953215515,23.3226710580088,24.553780056859,24.4178949300561,23.8141144065405,20.7220105172926,21.8480279328673,25.6056054929952,24.2472035257108,24.4210885085978,22.8223798200544,22.4141285310558,23.2619625684504,23.6801672732722,25.1450137746962,37.8025375170248,36.8015715001263,33.3660040245553,28.9081364816202,29.3277731988661,31.5421228812257,30.464320885463,31.0126989257048,27.6305151785901,27.6063079270634,26.3162214448371,25.3178391736306,25.4684057340495,23.5316865574673,23.7256416689121,24.5871079128915,23.4258905208838,22.7148944075199,23.5006920723998,22.5021456898926,20.5993361665308,21.6111168796026,22.989223296074,24.5547239050824,23.9378506040749,20.8033610842521,23.7604311445806,24.4850017511201,25.4269949437035,26.7540417597703,37.0336469307728,36.7340786262131,34.4915460371071,31.1790111439649,31.8488974230787,31.0517536248522,32.820453426231,32.7310136439433,29.7823163882009,32.4436282133301,26.1634971544153,26.783707411989,25.8528944349204,22.8529520259067,25.2648897404528,24.475596132505,23.634096959408,22.6104013190684,24.7110224497352,22.1884804678685,23.3632206839582,22.9302280598597,23.3275984195399,22.4662205498488,21.6104584092015,23.1526413852675,22.1476075615046,22.7362007271532,24.3761273806013,25.4726101561521,40.2354526685028,37.0895464008698,35.4209063406966,33.7364553993894,30.946613595036,35.0917429964027,33.2164320701731,34.1032829548588,31.862637924954,29.8316520819322,27.6869203978869,26.3672600316582,26.0940185367167,24.2385126880488,23.9721867018572,24.062665648531,23.3979348562403,22.809191803248,20.4033532086702,20.9300978771792,21.2529140872146,21.0976684307715,23.1048428738577,20.5346114520467,19.8723434073117,23.7432011110265,23.6768765019415,23.3246436790924,23.9074929825757,27.4922300014401,40.122347559094,39.8379210277334,36.7006933825997,36.606629789619,33.2118893448596,31.0419140775224,31.9765572873663,32.3327578680919,34.6396275861878,30.8611302186963,29.0948285048773,27.6556890805151,27.3834806915894,26.461704742091,24.0154404956285,24.5005724674976,23.0573950648667,22.2405457918698,20.4395910456644,20.1707338561524,22.0282316340877,21.4215312693068,22.8901614766977,22.3717503898389,23.8280712951884,23.7530660491994,23.1198821467912,24.2441356538891,22.6190922146902,24.8673080257243,39.0154783876639,39.2040954726829,34.5868168887056,33.2947676889519,32.4176366425211,29.6811081541038,30.6111536267832,32.5045972109552,30.958873322424,29.8845064003457,28.4364689348902,27.3195812326622,26.2381407143913,25.8208116781197,25.7943336642706,24.027939664061,23.9835607196053,20.7371703425297,20.4266295584787,22.4365129811028,23.6890497988181,22.7479520285407,23.8344532517078,22.9679179347516,23.8560464858167,22.5412291427257,24.4535568471538,22.8425781065271,23.9878114903815,23.0219762430535,36.2039643685563,36.777064818767,40.1865626495828,38.0277296734647,31.3621007947969,30.9553974840096,29.0174438061303,25.5571378212106,28.7818097896778,28.3196741122843,29.0287021174777,26.2359729056487,26.5612465924285,23.9152636123562,23.3979652979572,23.766463501079,22.7607722949597,21.6921166115195,19.8544780082748,21.3196482921865,21.9816972739804,24.5896954181483,23.6224512054549,23.2086596538105,22.801772849191,21.053622982771,23.5999410843533,22.3715636306793,21.1875483146076,21.3594885401651,32.276905952956,35.9222623252586,39.1729627736458,35.899819890848,34.5789947358694,30.8866080112616,29.7738651724678,25.4642475345368,23.9325004752954,28.6512269466668,31.0168929992435,24.4897991957707,22.9444377693144,22.3473415579198,24.0757529087556,22.8910055084283,21.1821119758902,19.4849420391304,19.2010446520031,19.8374524993514,24.3092688586111,25.8308666502423,23.9811102657072,21.9649317546019,22.1733682959731,20.5861343023296,21.727638340022,22.361422189204,19.3144562892348,19.6751313173216
];
var MapColumns = 30,
MapRows = 20;
var margin = {
top: 140,
right: 30,
bottom: 120,
left: 30
};
//First try for width
var width = Math.max(Math.min(window.innerWidth, 1000), 500) - margin.left - margin.right - 20;
var height = window.innerHeight - margin.top - margin.bottom - 20;
//The maximum radius the hexagons can have to still fit the screen
var hexRadius = d3.min([width/(Math.sqrt(3)*MapColumns), height/(MapRows*1.5)]);
//Set the new height and width based on the max possible
var width = MapColumns*hexRadius*Math.sqrt(3);
var height = MapRows*1.5*hexRadius+0.5*hexRadius;
//SVG container
var svg = d3.select('#chart')
.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 + ")");
//Reset the overall font size
var newFontSize = width * 62.5 / 800;
d3.select("html").style("font-size", newFontSize + "%");
//Format to display numbers
var formatPercent = d3.format("%");
//Needed for gradients
var defs = svg.append("defs");
///////////////////////////////////////////////////////////////////////////
//////////////// Calculate hexagon centers and put into array /////////////
///////////////////////////////////////////////////////////////////////////
var SQRT3 = Math.sqrt(3),
hexWidth = SQRT3 * hexRadius,
hexHeight = 2 * hexRadius;
var hexagonPoly = [[0,-1],[SQRT3/2,0.5],[0,1],[-SQRT3/2,0.5],[-SQRT3/2,-0.5],[0,-1],[SQRT3/2,-0.5]];
var hexagonPath = "m" + hexagonPoly.map(function(p){ return [p[0]*hexRadius, p[1]*hexRadius].join(','); }).join('l') + "z";
var points = [];
for (var i = 0; i < MapRows; i++) {
for (var j = 0; j < MapColumns; j++) {
var a;
var b = (3 * i) * hexRadius / 2;
if (i % 2 === 0) {
a = SQRT3 * j * hexRadius;
} else {
a = SQRT3 * (j - 0.5) * hexRadius;
}//else
points.push({x: a, y: b});
}//for j
}//for i
///////////////////////////////////////////////////////////////////////////
//////// Get continuous color scale for the Yellow-Green-Blue fill ////////
///////////////////////////////////////////////////////////////////////////
var coloursYGB = ["#FFFFDD","#AAF191","#80D385","#61B385","#3E9583","#217681","#285285","#1F2D86","#000086"];
var colourRangeYGB = d3.range(0, 1, 1.0 / (coloursYGB.length - 1));
colourRangeYGB.push(1);
//Create color gradient
var colorScaleYGB = d3.scale.linear()
.domain(colourRangeYGB)
.range(coloursYGB)
.interpolate(d3.interpolateHcl);
//Needed to map the values of the dataset to the color scale
var colorInterpolateYGB = d3.scale.linear()
.domain(d3.extent(somData))
.range([0,1]);
///////////////////////////////////////////////////////////////////////////
///////////////////// Create the YGB color gradient ///////////////////////
///////////////////////////////////////////////////////////////////////////
//Calculate the gradient
defs.append("linearGradient")
.attr("id", "gradient-ygb-colors")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%")
.selectAll("stop")
.data(coloursYGB)
.enter().append("stop")
.attr("offset", function(d,i) { return i/(coloursYGB.length-1); })
.attr("stop-color", function(d) { return d; });
///////////////////////////////////////////////////////////////////////////
//////////// Get continuous color scale for the Rainbow ///////////////////
///////////////////////////////////////////////////////////////////////////
var coloursRainbow = ["#2c7bb6", "#00a6ca","#00ccbc","#90eb9d","#ffff8c","#f9d057","#f29e2e","#e76818","#d7191c"];
var colourRangeRainbow = d3.range(0, 1, 1.0 / (coloursRainbow.length - 1));
colourRangeRainbow.push(1);
//Create color gradient
var colorScaleRainbow = d3.scale.linear()
.domain(colourRangeRainbow)
.range(coloursRainbow)
.interpolate(d3.interpolateHcl);
//Needed to map the values of the dataset to the color scale
var colorInterpolateRainbow = d3.scale.linear()
.domain(d3.extent(somData))
.range([0,1]);
///////////////////////////////////////////////////////////////////////////
//////////////////// Create the Rainbow color gradient ////////////////////
///////////////////////////////////////////////////////////////////////////
//Calculate the gradient
defs.append("linearGradient")
.attr("id", "gradient-rainbow-colors")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%")
.selectAll("stop")
.data(coloursRainbow)
.enter().append("stop")
.attr("offset", function(d,i) { return i/(coloursRainbow.length-1); })
.attr("stop-color", function(d) { return d; });
///////////////////////////////////////////////////////////////////////////
//////////////////////////// Draw Heatmap /////////////////////////////////
///////////////////////////////////////////////////////////////////////////
//Append title to the top
svg.append("text")
.attr("class", "title")
.attr("x", width/2-10)
.attr("y", -80)
.text("Clustering of Supermarkets");
svg.append("text")
.attr("class", "subtitle")
.attr("x", width/2-10)
.attr("y", -58)
.text("based on demographics");
svg.append("text")
.attr("class", "subtitle")
.attr("x", width/2-10)
.attr("y", -30)
.style("font-weight", 800)
.style("fill", "#676767")
.text("click anywhere to switch colors");
svg.append("g")
.selectAll(".hexagon")
.data(points)
.enter().append("path")
.attr("class", "hexagon")
.attr("d", function (d) { return "M" + d.x + "," + d.y + hexagonPath; })
.style("stroke", "#fff")
.style("stroke-width", "1px")
.style("fill", "white")
.on("mouseover", mover)
.on("mouseout", mout);
///////////////////////////////////////////////////////////////////////////
////////////////////////// Draw the legend ////////////////////////////////
///////////////////////////////////////////////////////////////////////////
var legendWidth = width * 0.6,
legendHeight = 10;
//Color Legend container
var legendsvg = svg.append("g")
.attr("class", "legendWrapper")
.attr("transform", "translate(" + (width/2 - 10) + "," + (height+50) + ")");
//Draw the Rectangle
legendsvg.append("rect")
.attr("class", "legendRect")
.attr("x", -legendWidth/2)
.attr("y", 10)
//.attr("rx", legendHeight/2)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "none");
//Append title
legendsvg.append("text")
.attr("class", "legendTitle")
.attr("x", 0)
.attr("y", -2)
.text("Store Competition Index");
//Set scale for x-axis
var xScale = d3.scale.linear()
.range([0, legendWidth])
.domain([0,100]);
//.domain([d3.min(pt.legendSOM.colorData)/100, d3.max(pt.legendSOM.colorData)/100]);
//Define x-axis
var xAxis = d3.svg.axis()
.orient("bottom")
.ticks(5) //Set rough # of ticks
//.tickFormat(formatPercent)
.scale(xScale);
//Set up X axis
legendsvg.append("g")
.attr("class", "axis") //Assign "axis" class
.attr("transform", "translate(" + (-legendWidth/2) + "," + (10 + legendHeight) + ")")
.call(xAxis);
///////////////////////////////////////////////////////////////////////////
////////////////////////// Mouse Interactions /////////////////////////////
///////////////////////////////////////////////////////////////////////////
//Function to call when you mouseover a node
function mover(d) {
var el = d3.select(this)
.transition()
.duration(10)
.style("fill-opacity", 0.3);
}
//Mouseout function
function mout(d) {
var el = d3.select(this)
.transition()
.duration(1000)
.style("fill-opacity", 1);
};
///////////////////////////////////////////////////////////////////////////
////////////////////////// Color Interactions /////////////////////////////
///////////////////////////////////////////////////////////////////////////
//On click transition
d3.select("body").on("click", function() {
if(currentFill === "rainbow") {
updateYGB();
currentFill = "YGB";
} else {
updateRainbow();
currentFill = "rainbow";
}//else
});
//Update the colors to a more light yellow-green-dark blue
function updateYGB() {
//Fill the legend rectangle
svg.select(".legendRect")
.style("fill", "url(#gradient-ygb-colors)");
//Transition the hexagon colors
svg.selectAll(".hexagon")
.transition().duration(1000)
.style("fill", function (d,i) { return colorScaleYGB(colorInterpolateYGB(somData[i])); });
}//updateYGB
//Transition the colors to a rainbow
function updateRainbow() {
//Fill the legend rectangle
svg.select(".legendRect")
.style("fill", "url(#gradient-rainbow-colors)");
//Transition the hexagon colors
svg.selectAll(".hexagon")
.transition().duration(1000)
.style("fill", function (d,i) { return colorScaleRainbow(colorInterpolateRainbow(somData[i])); })
}//updateRainbow
//Start set-up
updateRainbow();
var currentFill = "rainbow";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment