Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@boeric
Last active May 23, 2020 20:06
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 boeric/cbb5b416091e74c70b8480e68b6d21e1 to your computer and use it in GitHub Desktop.
Save boeric/cbb5b416091e74c70b8480e68b6d21e1 to your computer and use it in GitHub Desktop.
CSS Box Model and Flexbox Using D3

CSS Box Model and Flexbox Demo

The Gist demos the following:

  1. The effects of margin, border, padding and inner content dimensions of the overall size of DOM elements (here, span elements)
  2. That outline has no effect on the element size and layout
  3. The horizontal or vertical layout of elements
  4. The optional use of Flexbox in laying out elements
  5. The use of various justify-content settings when using Flexbox
  6. The effect of setting dimensions on the item container div
  7. How to create a DOM structure including input and select elements with event handlers using D3
  8. How to calculate the full dimension of an element
  9. How to extract current inline styles of elements

The Gist is live here: https://bl.ocks.org/boeric

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS Box Model and Flexbox Demo</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
<script src="index.js" defer></script>
</head>
<body>
<div class="outerContainer">
<div class="subTitleContainer">Controls</div>
<div class="controlsContainer"></div>
<div class="subTitleContainer">Container</div>
<div class="contentContainer"></div>
</div>
<div class="overlay"></div>
</body>
/* By Bo Ericsson, https://www.linkedin.com/in/boeric00/ */
/* eslint-disable no-multi-spaces, operator-linebreak, indent, no-plusplus, no-nested-ternary,
func-names */
/* global d3 */
/*
Please note that indentation rules follow D3 praxis, that is, any method that alters the
selection is indented with two spaces, and all other methods are indented with four spaces.
There is no easy eslint rule to fix this...
*/
const itemCount = 9;
const data = [];
// Body dimensions
const bodyMargin = 0;
const bodyWidth = 940; // To fit https://bl.ocks.org width requirement
const bodyHeight = 480; // To fit https://bl.ocks.org height requirement
const bodyPadding = 10;
// Title and subtitle container height
const titleHeight = 20;
// Controls container dimensions
const controlsHeight = 20;
const controlsMarginBottom = 30;
// Content container dimensions
const contentContainerMarginTop = 10;
const contentContainerMaxHeight =
bodyHeight - // Total height of body
titleHeight - // Height of title container
controlsHeight - controlsMarginBottom - // Total height of controls container
titleHeight - // Height of subtitle
contentContainerMarginTop; // Content container top margin
// Set body dimensions
d3.select('body')
.style('margin', `${bodyMargin}px`)
.style('height', `${bodyHeight}px`)
.style('width', `${bodyWidth}px`)
.style('padding', `${bodyPadding}px`);
// Item dimensions
const width = 40;
const height = 15;
const margin = 10;
const border = 3;
const padding = 8;
const outline = 1;
// Create data
for (let i = 0; i < itemCount; i++) {
data.push(`Item ${i}`);
}
// Options for the select control
const justifyContentOptions = [
{ value: 'space-evenly', default: false, idx: 0 },
{ value: 'space-between', default: true, idx: 1 },
{ value: 'space-around', default: false, idx: 2 },
{ value: 'center', default: false, idx: 3 },
{ value: 'flex-end', default: false, idx: 4 },
{ value: 'flex-start', default: false, idx: 5 },
];
// Set initial flexbox direction
let direction = 'row';
// Set default flexbox direction
const defaultJustifyContent = justifyContentOptions.find(d => d.default);
const defaultJustifyIdx = defaultJustifyContent.idx;
let justifyContent = defaultJustifyContent.value;
// Define the input controls
const controls = {
useMargin: {
type: 'checkbox',
pos: 0,
label: 'Use Margin',
checked: true,
title: 'Use Margin',
},
useBorder: {
type: 'checkbox',
pos: 1,
label: 'Use Border',
checked: true,
title: 'Use Border',
},
usePadding: {
type: 'checkbox',
pos: 2,
label: 'Use Padding',
checked: true,
title: 'Use Porder',
},
useOutline: {
type: 'checkbox',
pos: 3,
label: 'Use Outline',
checked: true,
title: 'Use Outline',
},
useScroll: {
type: 'checkbox',
pos: 4,
label: 'Overflow scroll',
checked: true,
title: 'Scroll or hide overflow',
},
useMaxSize: {
type: 'checkbox',
pos: 5,
label: 'Max Container Size',
checked: true,
title: 'Use Max Size of Container',
},
flexRow: {
type: 'radio',
pos: 6,
label: 'Row',
checked: direction === 'row',
title: 'Row Direction',
},
flexColumn: {
type: 'radio',
pos: 7,
label: 'Column',
checked: direction === 'column',
title: 'Column Direction',
},
useFlex: {
type: 'checkbox',
pos: 8,
label: 'Use Flexbox',
checked: true,
title: 'Use Flex',
},
};
// Update the visualization
function updateViz() {
// Update container
const flex = controls.useFlex.checked;
const maxSize = controls.useMaxSize.checked;
const scroll = controls.useScroll.checked;
// Compute dimensions
const marginUsed = controls.useMargin.checked ? margin : null;
const borderUsed = controls.useBorder.checked ? border : null;
const paddingUsed = controls.usePadding.checked ? padding : null;
const outlineUsed = controls.useOutline.checked ? outline : null;
// Generate dimension strings
const totalMargin = (marginUsed || 0) * 2;
const totalBorder = (borderUsed || 0) * 2;
const totalPadding = (paddingUsed || 0) * 2;
const totalWidth = width + totalPadding + totalBorder + totalMargin;
const totalHeight = height + totalPadding + totalBorder + totalMargin;
const coreStr = `Core: ${width}px/${height}px`;
const marginStr = `Margin: ${totalMargin}px`;
const borderStr = `Border: ${totalBorder}px`;
const paddingStr = `Padding: ${totalPadding}px`;
const totalStr = `Total: ${totalWidth}px/${totalHeight}px`;
const dimStr = `${coreStr}, ${paddingStr}, ${borderStr}, ${marginStr}, ${totalStr}`;
// Update content container
const contentContainer = d3.select('.contentContainer')
.style('display', flex ? 'flex' : 'inline-block')
// .style('display', flex ? 'flex' : 'inline')
.style('flex-direction', flex ? direction : null)
.style('justify-content', flex ? justifyContent : null)
.style('width', maxSize ? '100%' : 'auto')
.style('height', maxSize ? `${contentContainerMaxHeight}px` : null)
.style('overflow', scroll ? 'scroll' : 'hidden');
// Determine selection
const updateSelection = contentContainer.selectAll('span').data(data);
const enterSelection = updateSelection.enter();
const enterSelectionSize = enterSelection.size();
const selection = enterSelectionSize === 0 ? updateSelection : enterSelection.append('span');
// Update items
selection
.attr('class', 'innerElem')
.style('margin', marginUsed !== null ? `${marginUsed}px` : null)
.style('border', borderUsed !== null ? `${borderUsed}px solid #2196f3` : null)
.style('padding', paddingUsed !== null ? `${paddingUsed}px` : null)
.style('outline', outlineUsed !== null ? `${outlineUsed}px solid red` : null)
.style('width', `${width}px`)
.style('height', `${height}px`)
.style('display', direction === 'row' ? 'inline-block' : 'block')
.text((d) => d);
// Update overlay
const overlay = d3.select('.overlay');
overlay.selectAll('*').remove();
const overlayItem = overlay
.append('div')
.style('display', 'flex')
.style('flex-direction', 'row');
// Add an outer wrapper to the 'Item' that will be added below
const itemContainer = overlayItem
.append('div')
.style('outline', '1px solid orange')
.style('display', 'inline-block');
// Add an item instance inside the item container
itemContainer.append('span')
.attr('class', 'innerElem')
.style('margin', marginUsed !== null ? `${marginUsed}px` : null)
.style('border', borderUsed !== null ? `${borderUsed}px solid #2196f3` : null)
.style('padding', paddingUsed !== null ? `${paddingUsed}px` : null)
.style('outline', outlineUsed !== null ? `${outlineUsed}px solid red` : null)
.style('width', `${width}px`)
.style('height', `${height}px`)
.style('display', direction === 'row' ? 'inline-block' : 'block')
.text('Item');
// Add text to the right of the item
overlayItem.append('div')
.style('margin-left', '10px')
.text('Orange rectangle represents total dimension');
const infoContainer = overlay.append('div')
.attr('class', 'infoContainer');
const itemDimensionContainer = infoContainer.append('div')
.attr('class', 'textBold')
.style('margin-top', '10px')
.text('Item Dimensions');
itemDimensionContainer.append('div')
.attr('class', 'textNormal')
.style('margin-left', '10px')
.text(dimStr);
const itemStyleContainer = infoContainer.append('div')
.attr('class', 'textBold')
.style('margin-top', '10px')
.text('Item Inline Styles');
itemStyleContainer.append('div')
.attr('class', 'textNormal')
.style('margin-left', '10px')
.text(selection.nodes()[0].style.cssText);
const contentStyleContainer = infoContainer.append('div')
.attr('class', 'textBold')
.style('margin-top', '10px')
.text('Container Inline Styles');
contentStyleContainer.append('div')
.attr('class', 'textNormal')
.style('margin-left', '10px')
.text(contentContainer.nodes()[0].style.cssText);
}
// Set dimensions of the container that contains the input controls (checkboxes, radios and select)
const controlsContainer = d3.select('.controlsContainer')
.style('height', `${controlsHeight}px`)
.style('margin-bottom', `${controlsMarginBottom}px`);
// Create span elem wrapper for each input control
const keys = Object.keys(controls).sort((a, b) => (a.pos > b.pos ? 1 : a.pos < b.pos ? -1 : 0));
const spans = controlsContainer.selectAll('span')
.data(keys)
.enter()
.append('span')
.style('margin-right', '5px');
// Create checkboxes and radios, and add event handler
spans.append('input')
.attr('type', (d) => controls[d].type)
.attr('name', (d) => d)
.attr('title', (d) => controls[d].title)
.property('checked', (d) => controls[d].checked)
.on('change', function(d) {
const checked = d3.select(this).property('checked');
// Manage radios without form
const radios = d3.selectAll('input[type="radio"]');
switch (d) {
case 'useFlex':
controls[d].checked = checked;
// Disable select if flexbox is not used
d3.select('.justifySelect').property('disabled', !checked);
break;
case 'flexRow':
direction = 'row';
controls.flexRow.checked = true;
controls.flexColumn.checked = false;
// Update the radios
radios.nodes()[0].checked = true;
radios.nodes()[1].checked = false;
break;
case 'flexColumn':
direction = 'column';
controls.flexRow.checked = false;
controls.flexColumn.checked = true;
// Update the radios
radios.nodes()[0].checked = false;
radios.nodes()[1].checked = true;
break;
default:
controls[d].checked = checked;
}
// Update content
updateViz(data);
});
// Create labels for the checkboxes and radios
spans.append('label')
.attr('for', (d) => d)
.attr('title', (d) => controls[d].title)
.text((d) => controls[d].label);
// Create select control
const selectControl = controlsContainer.append('select')
.attr('class', 'justifySelect')
.on('change', function () {
const option = d3.select(this).node().value;
justifyContent = option;
updateViz();
});
// Add options to select
selectControl.selectAll('option')
.data(justifyContentOptions)
.enter()
.append('option')
.text((d) => d.value);
// Set default (pre-preselected value) of select
selectControl.property('selectedIndex', defaultJustifyIdx);
// Add height to title container
d3.select('.titleContainer')
.style('height', `${titleHeight}px`);
// Add height to subtitle container
d3.select('.subTitleContainer')
.style('height', `${titleHeight}px`);
// Set top margin and background color of content container
d3.select('.contentContainer')
.style('margin-top', `${contentContainerMarginTop}px`);
// Initial update
updateViz();
body {
font-family: helvetica;
font-size: 12px;
outline: 1px solid lightgray;
}
span {
height: max-content;
width: max-content;
}
select {
font-size: 12px;
}
.innerElem {
background-color: lightblue;
}
.subTitleContainer {
font-size: 15px;
font-weight: bold;
}
.contentContainer {
background-color: aliceblue;
outline: 1px solid lightgray;
overflow: scroll;
}
.overlay {
position: absolute;
top: 200px;
left: 125px;
width: 450px;
height: 240px;
border: 1px solid #bbb;
border-radius: 1px;
background-color: white;
padding: 10px;
}
.infoContainer {
position: absolute;
top: 70px;
left: 0px;
width: 450px;
height: 170px;
margin: 10px;
}
.textBold {
font-weight: bold;
}
.textNormal {
font-weight: normal;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment