|
/* 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(); |