Skip to content

Instantly share code, notes, and snippets.

@hhrogersii
Last active January 3, 2018 19:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hhrogersii/cd3ff2a1031204276522 to your computer and use it in GitHub Desktop.
Save hhrogersii/cd3ff2a1031204276522 to your computer and use it in GitHub Desktop.
Maintain equidistant text contrast against background palette gradient
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=320" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Visualizing Text/Background Color Contrast</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sucrose@0.7.5/build/sucrose.min.css">
<script>
less = {
globalVars: {
env: 'production',
// color palette
PALETTE: [
{ color: '#de3839', name: 'red' },
{ color: '#f19547', name: 'orange' },
{ color: '#f3cc25', name: 'yellow' },
{ color: '#3c910e', name: 'army' },
{ color: '#61d523', name: 'green' },
{ color: '#2fdbca', name: 'mint' },
{ color: '#24e7f4', name: 'teal' },
{ color: '#3ab5f5', name: 'pacific' },
{ color: '#166e90', name: 'ocean' },
{ color: '#1285d5', name: 'blue' },
{ color: '#09467c', name: 'night' },
{ color: '#956cd6', name: 'purple' },
{ color: '#da8ee5', name: 'pink' },
{ color: '#d35db1', name: 'coral' },
{ color: '#555555', name: 'gray' }
],
// number of swatches per gradient
SWATCHCOUNT: 21,
// height of gradient row
ROWHEIGHT: '48px'
}
};
</script>
<style type="text/less">
body {
padding: 10px 20px 20px;
margin: 0;
font: normal 12px/18px Helvetica, Arial, sans-serif;
color: #555;
background-color: #fff;
box-sizing: border-box;
}
h1 {
font-size: 15px;
display: inline-block;
}
.toggle {
display: inline-block;
margin: 0 0 0 20px;
background-color: #fff;
padding: 4px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 3px;
color: blue;
&:hover {
text-decoration: underline;
color: purple;
}
&.selected {
border-color: #777;
}
}
.palette {
position: relative;
}
.gradient {
height: @ROWHEIGHT;
span {
display: block;
line-height: 12px;
}
div {
height: @ROWHEIGHT;
float: left;
text-align: center;
font-size: 10px;
font-weight: bold;
box-sizing: border-box;
&.label {
padding-top: (@ROWHEIGHT - 20) / 2;
width: 6%;
}
&.swatch {
@width: ~"100% / @{SWATCHCOUNT}";
width: calc(@width);
padding-top: (@ROWHEIGHT - 12) / 2;
}
}
}
.swatches-wrap {
position: relative;
width: 94%;
}
.sparkline-wrap {
position: absolute;
width: 100%;
top: 0;
left: 0;
z-index: 1;
background-color: transparent;
pointer-events: none;
&.show {
display: block;
}
&.hide {
display: none;
}
}
.sparkline-chart {
display: block;
width: 100%;
height: 100%;
path.sc-line,
.sc-point-paths path {
stroke-width: 1px;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sucrose@0.7.5/build/sucrose.js"></script>
</head>
<body>
<!-- Maintain equidistant text contrast against background palette gradients -->
<h1>Visualizing Text/Background Color Contrast</h1>
<button class="toggle" onclick="toggleGraph(event)">toggle graph</button>
<button class="toggle" onclick="toggleContrast(event)">no contrast adjustment</button>
<div class="palette"></div>
<script>
var config, scale, lineChart, palette, gradientsEnter, gradients;
var dispatch, noContrastAdjust, tooltip;
// adjust contrast of text color relative to background
function getTextContrast(backColor, callback) {
var backLab, backLumen, brighter, darker, textLumen, textLab, text;
backLab = d3.lab(backColor);
backLumen = backLab.l;
brighter = noContrastAdjust === 'false' ?
backLab.brighter(4 + (18 - backLumen) / 25).l :
255; // (0..50)[3..1]
darker = noContrastAdjust === 'false' ?
backLab.darker(4 + (backLumen - 75) / 25).l :
0; // (50..100)[1..3]
textLumen = backLumen > 60 ? darker : brighter;
textLab = d3.lab(textLumen, 0, 0);
text = textLab.toString();
callback(backLumen, textLumen);
return text;
}
// chart main
function renderChart(viz, chart, data) {
var width, swatchHalfWidth;
width = parseInt(d3.select('.swatches-wrap').style('width'), 10);
swatchHalfWidth = width / config.SWATCHCOUNT / 2;
// reset chart width and margin to align points with swatch center
chart
.width(width)
.margin({
top: 5,
right: swatchHalfWidth,
bottom: 5,
left: swatchHalfWidth
});
// instantiate chart
viz
.datum(data)
.call(chart);
}
// resize swatches and chart on window resize
function windowResize(fn) {
if (window.attachEvent) {
window.attachEvent('onresize', fn);
} else if (window.addEventListener) {
window.addEventListener('resize', fn, true);
}
}
// draw the palette swatches and chart
function paint(gradients) {
// for each gradient row
gradients.each(function(g) {
var backData, textData, lineData, colors, gradient, sparkline;
// color data stores for visualization
backData = [];
textData = [];
lineData = [];
// setup color gradient based on background-color
colors = d3.scaleLinear()
.range(['#000000', g.color, '#FFFFFF'])
.domain([0, 50, 100])
.interpolate(d3.interpolateLab);
// current gradient row
gradient = d3.select(this);
// line chart svg
sparkline = gradient.select('.sparkline-chart');
// gradient color details
gradient.select('.label').html(function(d) {
return '<span>@' + d.name + '</span><span>' + d.color + '</span>';
});
// for each swatch set data stores and swatch attributes
gradient.selectAll('.swatch').each(function(d, i) {
var data = {};
// get back color from color array
data.backColor = colors(scale(i));
// calculate text color based on back color lumen
data.textColor = getTextContrast(data.backColor, function(b, t) {
// store color values for visualization
data.backLumen = b;
data.textLumen = t;
backData.push({
x: i,
y: b
});
textData.push({
x: i,
y: t
});
});
// set background and text color
d3.select(this).datum(data)
.style('background-color', data.backColor)
.select('span')
.style('color', data.textColor);
});
lineData = [{
'key': g.name,
'values': backData,
'color': '#00f2ff'
}, //cyan
{
'key': 'text',
'values': textData,
'color': '#ff00ca'
} //magenta
];
renderChart(sparkline, lineChart, lineData);
// rerender chart on window resize
windowResize(function() {
renderChart(sparkline, lineChart, lineData);
});
});
}
// -------------------------------------------------------------------------
// Main
config = less.options.globalVars;
noContrastAdjust = 'false';
// for converting swatch index to lumen
scale = d3.scaleLinear()
.domain([0, config.SWATCHCOUNT - 1])
.range([0, 100]);
// sparkline model
lineChart = sucrose.models.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.useVoronoi(false)
.height(parseInt(config.ROWHEIGHT, 10));
// preset toggle button state
d3.selectAll('.toggle').datum({selected: 'false'});
// outer container
palette = d3.select('.palette');
// a gradient row for each color in palette
gradientsEnter = palette.selectAll('.gradient')
.data(config.PALETTE)
.enter().append('div')
.attr('class', 'gradient');
gradients = palette.selectAll('.gradient').merge(gradientsEnter);
// build palette with swatches
gradientsEnter
.append('div').attr('class', 'label');
gradientsEnter
.append('div').attr('class', 'swatches-wrap')
.selectAll('.swatches').data(d3.range(0, config.SWATCHCOUNT))
.enter().append('div')
.attr('class', 'swatch')
.html('<span>TEXT</span>');
// line chart container
gradientsEnter.select('.swatches-wrap')
.append('div').attr('class', 'sparkline-wrap hide')
.append('svg').attr('class', 'sparkline-chart')
.attr('id', function(d, i) { return 'sparkline-' + i; });
// draw the palette swatches and chart for each gradient row
gradients.call(paint);
// -------------------------------------------------------------------------
// Buttons
// handle toggling of button states
function toggleState(e) {
var el, s;
el = d3.select(e.srcElement);
s = el.datum().selected === 'false' ? 'true' : 'false';
el.datum({selected: s});
el.classed('selected', s === 'true');
return s;
}
// hide/show visualization
function toggleGraph(e) {
var s = toggleState(e);
d3.selectAll('.sparkline-wrap').classed('hide', s === 'false');
}
// toggle use of text contrast adjustment
function toggleContrast(e) {
var s = toggleState(e);
noContrastAdjust = s;
gradients.call(paint);
}
// -------------------------------------------------------------------------
// Tooltips
function buildEventObject(e, d, i) {
return {
e: e,
point: d,
pointIndex: i
};
}
function tooltipContent(d) {
return '<p><i>Back Lumen: </i>' + Math.round(d.backLumen, 2) + '</p>' +
'<p><i>Text Lumen: </i>' + Math.round(d.textLumen, 2) + '</p>' +
'<p><i>Back Color: </i>' + d.backColor + '</p>' +
'<p><i>Text Color: </i>' + d.textColor + '</p>';
}
function showTooltip(eo, offsetElement) {
var content = tooltipContent(eo.point);
tooltip = sucrose.tooltip.show(eo.e, content, 'n', null, offsetElement);
}
tooltip = null;
dispatch = d3.dispatch('Mouseover', 'Mousemove', 'Mouseout');
dispatch.on('Mouseover.tooltip', function(e) {
showTooltip(e, palette.node());
});
dispatch.on('Mousemove.tooltip', function(e) {
if (tooltip) {
sucrose.tooltip.position(palette.node(), tooltip, e);
}
});
dispatch.on('Mouseout.tooltip', function(e) {
if (tooltip) {
sucrose.tooltip.cleanup();
}
});
d3.selectAll('.swatch')
.on('mouseover', function(d, i) {
var eo = buildEventObject(d3.event, d, i);
d3.select(this).classed('hover', true);
dispatch.call('Mouseover', this, eo);
})
.on('mousemove', function(d, i) {
var e = d3.event;
dispatch.call('Mousemove', this, e);
})
.on('mouseout', function(d, i) {
var eo = buildEventObject(d3.event, d, i);
d3.select(this).classed('hover', false);
dispatch.call('Mouseout', this, eo);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment