Public
Edited
Nov 3, 2020
10 forks
37 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
json = FileAttachment("d3-lafe-example@14.json").json()
Insert cell
Insert cell
Insert cell
lafe = init(json)
Insert cell
function init(data){
let lafe = {}

//deep clone input data to keep orginal json object untouched
lafe.data = JSON.parse(JSON.stringify(data))

// set the dimensions and margins of the graph
lafe.margin = {top: 10, right: 10, bottom: 10, left: 10}
lafe.width = lafe.data.layout.width - lafe.margin.left - lafe.margin.right
lafe.height = lafe.data.layout.height - lafe.margin.top - lafe.margin.bottom

lafe.zoom = false //Initial zoom state of the graph

lafe.px = 1.0/lafe.width //Relative size of one pixel in the horizontal direction (x)
lafe.py = 1.0/lafe.height //Relative size of one pixel in the vertical direction (y)

//Graph options, managed by checkboxes or labels displayed directly within the graph
lafe.options = [
{ "code":"dShw",
"type":"checkbox",
"value":true,
"title":"Show intermediate demands",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"lclick",
"type":"label",
"title":"Left-click on a node to highlight genealogy",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"pGrp",
"type":"checkbox",
"value":true,
"title":"Group by product",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"dArg",
"type":"checkbox",
"value":true,
"title":"Auto-arrange demands",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"rclick",
"type":"label",
"title":"Right-click on a node to toggle zoom",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0},
{ "code":"pNrm",
"type":"checkbox",
"value":true,
"title":"Normalize by product",
"x":0.0,"y":0.0,"dx":0.0,"dy":0.0}
]
lafe.oMap = getMap(lafe.options)

//Keeping only consistent data and storing them
//into arrays (to keep original order) and maps (for convenient access)
lafe.demands = lafe.data.demands
.filter(d=>lafe.data.lot2demand.filter(l2d=>l2d.odemand==d.code).length>0)
lafe.dMap = getMap(lafe.demands)

lafe.resources = lafe.data.resources
.filter(r=>lafe.data.lots.filter(l=>l.resource==r.code).length>0)
lafe.rMap = getMap(lafe.resources)

lafe.stages = lafe.data.stages
.filter(s=>lafe.data.products.filter(p=>p.stage==s.code).length>0)
lafe.sMap = getMap(lafe.stages)

lafe.products = lafe.data.products
.filter(p=>lafe.sMap.has(p.stage))
.filter(p=>lafe.data.lots.filter(l=>l.product==p.code).length>0)
lafe.pMap = getMap(lafe.products)
lafe.lots = lafe.data.lots
.filter(l=>lafe.pMap.has(l.product)&&lafe.rMap.has(l.resource))
lafe.lMap = getMap(lafe.lots)
lafe.l2l = lafe.data.lot2lot
.filter(l2l=>lafe.lMap.has(l2l.ilot)&&lafe.lMap.has(l2l.olot))
lafe.l2d = lafe.data.lot2demand
.filter(l2d=>lafe.lMap.has(l2d.ilot)&&lafe.dMap.has(l2d.odemand))

//building relationships between objects and adding required properties
lafe.demands.forEach(d=>{
d.il2dMap = d3.map() //input l2d links
d.size = 0
})
lafe.stages.forEach(s=>{
s.bdepth=0; //backward depth of the stage
s.lMap = d3.map() //lots of the stage
s.pMap = d3.map() //products of the stage
s.pCount = //total number of products of the stage
s.lCount = //total number of lots of the stage
s.lSizeMax = 0.0 //maximum size of all lots of the stage
s.x = 0.0 //relative x coordinate of the stage
s.y = 0.0 //relative y coordinate of the stage
s.dx = 0.0 //relative width of the stage
s.dy = 0.0 //relative height of the stage
});
lafe.products.forEach(p=>{
p.stage = lafe.sMap.get(p.stage) //stage the product belongs to
p.lMap = d3.map() //lots of the product
p.lCount = //total number of lots of the product
p.lSizeMax = 0.0 //maximum size of all lots of the product
p.x = 0.0 //relative x coordinate of the product
p.y = 0.0 //relative y coordinate of the product
p.dx = 0.0 //relative width of the product
p.dy = 0.0 //relative height of the product
p.visible = true //visible state of the product
})
lafe.lots.forEach(l=>{
l.resource = lafe.rMap.get(l.resource); //resource the lot belongs to
l.product = lafe.pMap.get(l.product); //product the lot belongs to
l.date = new Date(l.date) //make date object from date string
l.il2lMap = d3.map(); //input l2l links
l.ol2lMap = d3.map(); //output l2l links
l.ol2dMap = d3.map(); //output l2d links
l.il2lSizes = d3.map() //total size of input l2l links per input product
l.ol2lSize = 0.0 //total size of output l2l links
l.ol2dSize = 0.0 //total size of output l2d links
l.x = 0.0 //relative x coordinate of the lot
l.y = 0.0 //relative y coordinate of the lot
l.dx = 0.0 //relative width of the lot
l.dy = 0.0 //relative height of the lot
l.selected = false //selected state of the lot
l.visible = true //visible state of the lot

//filling parent properties
l.product.lMap.set(l.code,l)
l.product.stage.lMap.set(l.code,l)
l.product.stage.pMap.set(l.product.code,l.product)
if(l.size>l.product.lSizeMax) l.product.lSizeMax = l.size
if(l.size>l.product.stage.lSizeMax) l.product.stage.lSizeMax = l.size

})
lafe.l2l.forEach(l2l=>{
l2l.ilot = lafe.lMap.get(l2l.ilot); //input (origin) lot of the lot 2 lot link
l2l.olot = lafe.lMap.get(l2l.olot); //output (destination) lot of the lot 2 lot link
l2l.ix = 0.0 //relative x coordinate of the input of the lot 2 lot link (upper-left corner of the band)
l2l.iy = 0.0 //relative y coordinate of the input of the lot 2 lot link (upper-left corner of the band)
l2l.idy = 0.0 //relative height of the input of the lot 2 lot link (left part of the band)
l2l.ox = 0.0 //relative x coordinate of the output of the lot 2 lot link (upper-right corner of the band)
l2l.oy = 0.0 //relative y coordinate of the output of the lot 2 lot link (upper-right corner of the band)
l2l.ody = 0.0 //relative height of the output of the lot 2 lot link (right part of the band)

//filling parent properties
l2l.ilot.ol2lMap.set(l2l.olot.code,l2l)
l2l.olot.il2lMap.set(l2l.ilot.code,l2l)
l2l.ilot.ol2lSize += l2l.size
addValue(l2l.olot.il2lSizes,l2l.ilot.product.code,l2l.size)

})
lafe.l2d.forEach(l2d=>{
l2d.ilot = lafe.lMap.get(l2d.ilot); //input (orgin) lot of the lot 2 demand link
l2d.odemand = lafe.dMap.get(l2d.odemand); //output (destinaion) demand of the lot 2 demand link
l2d.ix = 0.0 //relative x coordinate of the input of the lot 2 demand link (upper-left corner of the band)
l2d.iy = 0.0 //relative y coordinate of the input of the lot 2 demand link (upper-left corner of the band)
l2d.idy = 0.0 //relative height of the input of the lot 2 demand (left part of the band)
l2d.ox = 0.0 //relative x coordinate of the output of the lot 2 demand (upper-right corner of the band)
l2d.oy = 0.0 //relative y coordinate of the output of the lot 2 demand (upper-right corner of the band)
l2d.ody = 0.0 //relative height of the output of the lot 2 lot link (right part of the band)

//filling parent properties
l2d.odemand.size += l2d.size
l2d.ilot.ol2dMap.set(l2d.odemand.code,l2d)
l2d.odemand.il2dMap.set(l2d.ilot.code,l2d)
l2d.ilot.ol2dSize += l2d.size

})

//Recursively computing backward depth of stages,
//starting from demands and propagating backward
//considering also possible lots with no output links
lafe.demands.forEach(d=>bPropagate(d.il2dMap,0))
lafe.lots.filter(l=>(l.ol2lMap.size()+l.ol2dMap.size())==0).forEach(l=>bPropagate(l.il2lMap,1))
//Defining groups according to backward depths
//groups are used to group stages in such a way to
//minimize overlaping of links
//all lots inside a group will be positioned separately along the y axis
lafe.gCount = d3.set(lafe.stages.map(s=>s.bdepth)).size()
lafe.groups = [...Array(lafe.gCount).keys()].map(i=>{
let group = {
"code":"group-"+(i+1),
"index":i,
"bdepth":lafe.gCount-1-i,
"first":(i==0),
"last":(i==lafe.gCount-1),
"sCount":0, //number of stages in the group
"pCount":0, //number of products in the group
"lCount":0, //number of lots in the group
"tCount":0, //number of targets in the group
}
return group
})
lafe.gMap =getMap(lafe.groups)

//Assigning stages to groups and splitting demands into targets.
//One target is created for each couple group x demand for which l2d links exist
lafe.groups.forEach(g=>{
g.sMap = d3.map(lafe.stages.filter(s=>s.bdepth==g.bdepth),s=>s.code)
g.sCount = g.sMap.size()

g.tMap = d3.map() //targets of the group

g.sMap.each(s=>{
s.group = g
s.lMap.each(l=>{
l.ol2dMap.each(l2d=>{
//l2d is a lot to demand link with a lot part of the group
//a target is created for that demand within the group if not existing yet
let target = g.tMap.get(l2d.odemand.code)
if (target===undefined){
//create a new target (a target is a group-demand couple)
target = {
"code":g.code+"|"+l2d.odemand.code,
"sortkey":l2d.odemand.sortkey,
"group":g,
"demand":l2d.odemand,
"l2dMap":d3.map(), //map of all l2d links with this target as destination
"size":0, //total demand size of the target
"sizes":d3.map(), //demand size per product of the the target
"selected":false, //selected state of the target
"visible":true, //visible state of the target
"x":0.0, //relative x coordinate of the target
"y":0.0, //relative y coordinate of the target
"dx":0.0, //relative width of the target
"dy":0.0 //relative height of the target
}
g.tMap.set(l2d.odemand.code,target)
}
l2d.target = target
target.l2dMap.set(l2d.ilot.code,l2d)
target.size += l2d.size
addValue(target.sizes,l2d.ilot.product.code,l2d.size)

})
})
})

})

lafe.targets = lafe.groups.map(g=>sortedValues(g.tMap)).flat() //all targets of the graph

lafe.oCount = lafe.oMap.size() //total number of options
lafe.sCount = lafe.sMap.size() //total number of stages
lafe.pCount = lafe.pMap.size() //total number of products
lafe.lCount = lafe.lMap.size() //total number of lots
//elementary y padding space is the smallest y relative space used for vertical blank spacing between nodes //it corresponds to two pixels if possible, depending on the preferred height of the chart
//and the maxium number of nodes (bars) to align vertically
lafe.lPad = 1 //number of elementary y paddings used for lots
lafe.pPad = 2 //number of elementary y paddings used for products
lafe.sPad = 2 //number of elementary y paddings used for stages
lafe.tPad = 2 //number of elementary y paddings used for targets

return lafe
}
Insert cell
Insert cell
function computeSizes() {

//Reseting max sizes
lafe.groups.forEach(g=>{
g.tSizeMax = 0.0 //maximum size of all visible targets of the group
g.tSizeTot = 0.0 //total size of all visible targets of the group
})
lafe.stages.forEach(s=>{
s.lSizeMax = 0.0 //maximum size of all visible lots of the stage
})
lafe.products.forEach(p=>{
p.lSizeMax = 0.0 //maximum size of all visible lots of the product
})
//maximum value over all groups of the total number of elementary y paddings when grouping per stage
lafe.sPadMax = 0
//maximum value over all groups of the total number of elementary y paddings when grouping per product
lafe.pPadMax = 0

//Computing maximum and total sizes and based on visibility
lafe.targets.filter(t=>t.visible).forEach(t=>{
if(t.size>t.group.tSizeMax) t.group.tSizeMax = t.size
t.group.tSizeTot += t.size
})
lafe.lots.filter(l=>l.visible).forEach(l=>{
if(l.size>l.product.lSizeMax) l.product.lSizeMax = l.size
if(l.size>l.product.stage.lSizeMax) l.product.stage.lSizeMax = l.size
})
//counting visible lots, products, stages at the various levels,
//counting amount of elementary y padding spaces
//computing normalized sizes (normalization can be done over product or stage dimension),
lafe.groups.forEach(g=>{
g.sCount = 0 //number of visible stages of the group
g.pCount = 0 //number of visible products of the group
g.lCount = 0 //number of visible lots of the group
g.tCount = 0 //number of visible targets of the group

g.nsSize = 0 //Total normalized size of lots of the group when normalization is per stage
g.npSize = 0 //Total normalized size of lots of the group when normalization is per product

g.nsSizeL = 0 //Total normalized size of lots allocated to subsequent lots, normalization per stage
g.npSizeL = 0 //Total normalized size of lots allocated to subsequent lots, normalization per product
g.nsSizeD = 0 //Total normalized size of lots allocated to demands, normalization per stage
g.npSizeD = 0 //Total normalized size of lots allocated to demands, normalization per product

g.sPad = 0 //Total number of elementary y paddings of the group when lots are grouped per stage
g.pPad = 0 //Total number of elementary y paddings of the group when lots are grouped per product
g.sMap.each(s=>{
s.pCount = 0
s.nsSize = 0
s.npSize = 0
s.nsSizeL = 0
s.npSizeL = 0
s.nsSizeD = 0
s.npSizeD = 0
s.pMap.values().filter(p=>p.visible).forEach(p=>{
s.pCount += 1
p.first = g.first
p.last = g.last
p.lCount = 0
p.nsSize = 0
p.npSize = 0
p.nsSizeL = 0
p.npSizeL = 0
p.nsSizeD = 0
p.npSizeD = 0
p.lMap.values().filter(l=>l.visible).forEach(l=>{
p.lCount += 1
l.nsSize = s.lSizeMax>0?(l.size/s.lSizeMax):0.0 //normalizing lot sizes wrt to stage
l.npSize = p.lSizeMax>0?(l.size/p.lSizeMax):0.0 //normalizing lot sizes wrt to product
l.nsSizeL = s.lSizeMax>0?(l.ol2lSize/s.lSizeMax):0.0 //normalizing alloc. to lots wrt to stage
l.npSizeL = p.lSizeMax>0?(l.ol2lSize/p.lSizeMax):0.0 //normalizing alloc. to lots wrt to product
l.nsSizeD = s.lSizeMax>0?(l.ol2dSize/s.lSizeMax):0.0 //normalizing alloc. to demands wrt to stage
l.npSizeD = p.lSizeMax>0?(l.ol2dSize/p.lSizeMax):0.0 //normalizing alloc. to demands wrt to product
p.nsSize += l.nsSize
p.npSize += l.npSize
p.nsSizeL += l.nsSizeL
p.npSizeL += l.npSizeL
p.nsSizeD += l.nsSizeD
p.npSizeD += l.npSizeD
g.sPad += 2*lafe.lPad //counting upper and lower paddings around the lot (grouping per stage)
g.pPad += 2*lafe.lPad //counting upper and lower paddings around the lot (grouping per product)
},p)
s.lCount += p.lCount
s.nsSize += p.nsSize
s.npSize += p.npSize
s.nsSizeL += p.nsSizeL
s.npSizeL += p.npSizeL
s.nsSizeD += p.nsSizeD
s.npSizeD += p.npSizeD
g.pPad += 2*lafe.pPad //counting upper and lower paddings around the product (grouping per product)
//no paddings around product to account for when grouping per stage
},s)
g.lCount += s.lCount
g.pCount += s.pCount
g.sCount += 1
g.nsSize += s.nsSize
g.npSize += s.npSize
g.nsSizeL += s.nsSizeL
g.npSizeL += s.npSizeL
g.nsSizeD += s.nsSizeD
g.npSizeD += s.npSizeD
g.sPad += 2*lafe.sPad //counting upper and lower paddings around the stage (grouping per stage)
g.pPad += 2*lafe.sPad //counting upper and lower paddings around the stage (grouping per product)
},g)

g.tMap.values().filter(t=>t.visible).forEach(t=>{
t.last = g.last
t.nSize = g.tSizeMax>0?(t.size/g.tSizeMax):0.0
g.tCount += 1
})

if (lafe.sPadMax<g.sPad) lafe.sPadMax = g.sPad
if (lafe.pPadMax<g.pPad) lafe.pPadMax = g.pPad

})

//Computing space to be used for nodes and paddings
//(node are vertical bars used to represent lots and targets)
// number of vertical columns
//a column is used to veticaly align lots from a stage or targets from a group
lafe.cCount = lafe.sCount+1
//x space used for one node (10 pixels if not using more than 50% of the total x space, less otherwise)
lafe.nDX = Math.min(10*lafe.px,0.5/lafe.cCount)
//x space used between groups
lafe.gDX = (1.0-lafe.nDX)/lafe.gCount

//Number of columns used to arrange options
lafe.oCols = 3
//Number of rows used to arrange options
lafe.oRows = Math.ceil(lafe.oCount/lafe.oCols)
//y space used at the top part (for options) (10 pixels + 10 pixels per row)
lafe.topDY = (10+10*lafe.oRows)*lafe.py
//y space used at the bottom part (for graph)
lafe.botDY = 1.0-lafe.topDY

//y spacing used by one elementary padding
//2 pixels if not using more than 75% of bottom part y space, less otherwise
lafe.padY = Math.min(2*lafe.py,lafe.botDY*0.75/lafe.pPadMax)
lafe.lPadY = lafe.lPad*lafe.padY //relative padding y space used around lot
lafe.pPadY = lafe.pPad*lafe.padY //relative padding y space used around product
lafe.sPadY = lafe.sPad*lafe.padY //relative padding y space used around stage
lafe.tPadY = lafe.tPad*lafe.padY //relative padding y space used around targets

lafe.pPadX = 4*lafe.px //relative padding x space used around product
lafe.pDX = lafe.pPadX

}
Insert cell
Insert cell
function computePositions(){

let dShw = lafe.oMap.get("dShw").value //value of option "show intermediate demands"
let dArg = lafe.oMap.get("dArg").value //value of option "auto-arrange demands"
let pGrp = lafe.oMap.get("pGrp").value //value of option "group by product"
let pNrm = lafe.oMap.get("pNrm").value //value of option "normalize by product"

//Placing options
let dyy = lafe.topDY/lafe.oRows
lafe.options.forEach((o,i)=>{
let col = (i%lafe.oCols) //Column index of the option
let row = Math.floor(i/lafe.oCols) //Row index of the option

o.last = col==(lafe.oCols-1) //Option is place in the last column

o.x = col*(1.0/(lafe.oCols-1)) + (o.last?-lafe.pPadX:lafe.pPadX)
o.y = row*dyy
o.dx = 8*lafe.px // using 8x8 pixels to draw checkbox
o.dy = 8*lafe.py

})

//Computing x-y positions of nodes (lots and targets) and links
let x = 0.0
sortedValues(lafe.gMap).forEach(g=>{

//computing y space used by unitary normalized lot size for each group,
//depending on the type of grouping (per product or stage) and
//the type of normalization (per product or stage)
//y space used for the demand part is computed based on the ratio
//of the normalized size allocated to demands

//y space used for the intermediate demand part is computed based
//on the ratio of the normalized size allocated to demands
g.dDY = lafe.botDY *
(pNrm?
(((g.npSizeL+g.npSizeD)>0)?(g.npSizeD/(g.npSizeL+g.npSizeD)):0.0): //normalization per product
(((g.nsSizeL+g.nsSizeD)>0)?(g.nsSizeD/(g.nsSizeL+g.nsSizeD)):0.0) //normalization per stage
)

//Make sure that the part used for intermediate demands should at least occupy 2% of y space
if (g.tSizeTot>0){
g.dDY = Math.max(
g.dDY,
(0.02 + 2*(lafe.sPadY + (pGrp?lafe.pPadY:0)) + 2*g.tCount*lafe.tPadY)
)
}
//If intermediate demands are not displayed or if the group is the last one, reset related y space to 0
if (!dShw&&!g.last) g.dDY = 0

//y space used for empty space above lot part is computed based
//on the y space used for the demand part of the previous group
g.eDY = g.first?0.0:lafe.groups[g.index-1].dDY

//y space used for the lot part
g.lDY = lafe.botDY - g.eDY

//y space used for a lot node of unitary size
g.nDY = (g.lDY-(pGrp?g.pPad:g.sPad)*lafe.padY)/(pNrm?g.npSize:g.nsSize)

g.x = x
g.y = lafe.topDY

//Computing x-y positions of all lots, products, stages within the group
let y = g.y + g.eDY

//All stages within the group are positioned in the first 20% x space
//g.sDX is the x space between each stage of the group
g.sDX = g.sCount>1?((0.2*lafe.gDX)/(g.sCount-1)):0.0

let xx = x

sortedValues(g.sMap).forEach(s=>{
s.x = xx
s.dx = lafe.pDX

y += lafe.sPadY //Adding top padding space for the stage

s.y = y

if(pGrp){

//grouping per product
sortedValues(s.pMap).forEach(p=>{

p.x = s.x
p.dx = lafe.pDX

y += (p.visible?lafe.pPadY:0) //Adding top padding space for product if visible

p.y = y

//Sorting lots of the product according to date
p.lMap.values().sort((l1,l2)=>l1.date.getTime()-l2.date.getTime()).forEach(l=>{

y += (l.visible?lafe.lPadY:0) //Adding top padding space for lot if visible

l.x = p.x + lafe.pPadX //Adding left padding space between product (bracket) and lot
l.dx = lafe.nDX
l.y = y

//relative height of the lot is its normalized size (depending on the normalization mode)
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
l.dy = (l.visible?(g.nDY*(pNrm?l.npSize:l.nsSize)):0)

y += l.dy //move current y position by the lot height
y += (l.visible?lafe.lPadY:0) //add bottom padding space for lot if visible

},p)

p.dy = y - p.y //total height of the product

y += (p.visible?lafe.pPadY:0) //adding bottom padding space for product if visible

},s)

} else {

//grouping per stage

//Sorting lots of the stage according to date
s.lMap.values().sort((l1,l2)=>l1.date.getTime()-l2.date.getTime()).forEach(l=>{

y += (l.visible?lafe.lPadY:0) //adding top padding for the lot if visible

l.x = s.x + lafe.pPadX //Adding left padding space between product (bracket) and lot
l.dx = lafe.nDX

l.y = y
//relative height of the lot is its normalized size (depending on the normalization mode)
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
l.dy = (l.visible?(g.nDY*(pNrm?l.npSize:l.nsSize)):0)

y += l.dy //move current y position by the lot height
y += (l.visible?lafe.lPadY:0) //add bottom padding space for lot if visible

},s)

}

s.dy = y - s.y //total height of the stage

y += lafe.sPadY //Adding bottom padding space for the stage
xx += g.sDX //Moving xx coordinate by the space between stages within the group

},g)

x += lafe.gDX //Moving x coordinate by the space between groups

//at this level y should be equal to 1
//console.log("y = " + y)

//Computing y weight of all targets
//y weight of a target is the gravity center in the vertical direction
//of the y position of the inputs of all links allocated to the target.
//When auto-arranging demands is used, demands are ordered according to
//this weight in order to minimize crossing of lot to demand flows
g.tMap.each(t =>{
t.yweight = 0.0
t.l2dMap.each(l2d=>{
t.yweight += l2d.size*l2d.ilot.y
},t)
t.yweight = t.yweight/t.size
})

//Computing x-y positions of all targets
y = g.y + lafe.sPadY + (pGrp?lafe.pPadY:0) //Adding top padding of stage and product if visible

//y space used for a target node of unitary size
dyy = (g.tSizeTot>0&&g.dDY>0)?
(g.dDY - 2*(lafe.sPadY + (pGrp?lafe.pPadY:0)) - 2*g.tCount*lafe.tPadY)/g.tSizeTot
:0.0
//Sorting targets of the group according to demand original order or
//according to the weight (if auto-arrange is active)
g.tMap.values().sort(
(t1,t2)=>dArg?(t1.yweight-t2.yweight):(t1.demand.sortkey-t2.demand.sortkey)
).forEach(t => {
let show = t.visible&&(dShw||t.last)

y += (show?lafe.tPadY:0) //Adding top padding for target if visible
t.x = x + lafe.pPadX
t.y = y
t.dx = lafe.nDX //Using same relative width for targets and lots

//relative height of the target is its size multiplied by space of unitary size
//If not visible, height is set to zero to cause shrinking effect when toggling zoom
t.dy = (show?(dyy*t.size):0)

y += t.dy //move current y position by the target height
y += (show?lafe.tPadY:0) //add bottom padding space for the target if visible

})

})
//Computing x-y positions of all links
//(ix,iy) are the coordinates of the upper-left corner of the band (input of the band)
//(ox,oy) are the coordinates of the upper-right corner of the band (output of the band)
//idy is the height of the input of the band
//ody is the height of the output of the band
lafe.gMap.each(g=>{
g.sMap.each(s=>{
s.pMap.each(p=>{
p.lMap.each(l=>{
let y = l.y
//sorting output lot to demand links wrt link output target y position
l.ol2dMap.values().sort((ol2d1,ol2d2)=>ol2d1.target.y-ol2d2.target.y).forEach(ol2d=>{
ol2d.ix = l.x + l.dx
ol2d.iy = y
//relative height of the link input is the fraction of the input lot height
//corresponding to the ratio of allocation of the lot to the demand
ol2d.idy = ((l.size>0)?ol2d.size/l.size:0.0)*l.dy
y += ol2d.idy //moving y position by the link input height
},l)
//sorting output lot to lot links wrt y positions of link output lot
l.ol2lMap.values().sort((ol2l1,ol2l2)=>(ol2l1.olot.y-ol2l2.olot.y)).forEach(ol2l=>{
ol2l.ix = l.x + l.dx
ol2l.iy = y
//relative height of the link input is the fraction of the input lot height
//corresponding to the ratio of allocation of the input lot to the output lot
ol2l.idy = ((l.size>0)?ol2l.size/l.size:0.0)*l.dy
y += ol2l.idy //moving y position by the link input height
},l)
//for each input product, sorting related input lot to lot links
//wrt to y positions of the link input lot
//y coordinate is reset to the top of the lot at each product to cause overlapping of links
//when multiple products are used as input of the lot
l.il2lSizes.keys().forEach(code=>{
y = l.y
//for one product, input links are stacked
//isolating links related to the product and
//sorting them according to the y position of the input lot
l.il2lMap.values().filter(il2l=>il2l.ilot.product.code==code).sort(
(il2l1,il2l2)=>il2l1.ilot.y - il2l2.ilot.y
).forEach(il2l=>{
il2l.ox = l.x
il2l.oy = y
//relative height of the link output is the fraction of the output lot height
//corrresponding to the ratio of contribution of the link to the total contribution
//of all links supplying the same product
il2l.ody = ((l.il2lSizes.get(code)>0)?il2l.size/l.il2lSizes.get(code):0.0)*l.dy
y += il2l.ody //moving y position by the link output height
},p)
},l)

},p)
},s)
},g)

g.tMap.each(t=>{
x = t.x
let y = t.y
//sorting input lot to demand links wrt link input lot y position
t.l2dMap.values().sort((l2d1,l2d2)=>l2d1.iy-l2d2.iy).forEach(l2d=>{
l2d.ox = x
l2d.oy = y
//relative height of the link output is the fraction of the output target height
//corresponding to the ratio of allocation of the lot to the demand
l2d.ody = (t.size>0?(l2d.size/t.size):0.0)*t.dy
y += l2d.ody
})
},g)

})

}
Insert cell
Insert cell
function setTitles(){

//Setting titles for lots
lafe.lots.forEach(l=>{
l.title =
l.code + "\n" + "\n" +
"stage: " + l.product.stage.code + "\n" +
"product: " + l.product.code + "\n" +
"resource: " + l.resource.code + "\n" + "\n" +
"date: " + formatDate(l.date) + "\n" + "\n" +
"size: " + formatSize(l.size,l.product.unit) + "\n" +
" → allocated to demands: " + formatSize(l.ol2dSize) + "\n" +
" → allocated to lots: " + formatSize(l.ol2lSize) + "\n" +
" → unallocated: " + formatSize(l.size-l.ol2lSize-l.ol2dSize) + "\n"

})
//Setting titles for targets
lafe.targets.forEach(t=>{
t.title =
t.demand.code + "\n" + "\n" +
"size: " + "\n" +
t.sizes.entries().map(e=>" → "+e.key+": "+formatSize(e.value,lafe.pMap.get(e.key).unit)+"\n").join('')
},lafe)
//Setting titles for lot to lot links
lafe.l2l.forEach(l2l=>{
l2l.title =
l2l.ilot.code + " → " + l2l.olot.code + "\n" + "\n" +
"size: " + formatSize(l2l.size,l2l.ilot.product.unit) + "\n" +
" → consuming " + formatRatio(l2l.ilot.size==0?0.0:l2l.size/l2l.ilot.size) + " of " + l2l.ilot.code + "\n" +
" → providing " + formatRatio(l2l.olot.il2lSizes.get(l2l.ilot.product.code)==0?0.0:l2l.size/l2l.olot.il2lSizes.get(l2l.ilot.product.code)) + " of " + l2l.olot.code + " need " + " \n"
},lafe)

//Setting titles for lot to demand links
lafe.l2d.forEach(l2d=>{
l2d.title =
l2d.ilot.code + " → " + l2d.odemand.code + "\n" + "\n" +
"size: " + "\n" +
formatSize(l2d.size,l2d.ilot.product.unit) + " = " + formatRatio(l2d.ilot.size==0?0.0:l2d.size/l2d.ilot.size) + "\n"
})

}
Insert cell
Insert cell
function update(){

//Update all positions
computePositions()
let dShw = lafe.oMap.get("dShw").value //value of option "show intermediate demands"
let dArg = lafe.oMap.get("dArg").value //value of option "auto-arrange demands"
let pGrp = lafe.oMap.get("pGrp").value //value of option "group by product"
let pNrm = lafe.oMap.get("pNrm").value //value of option "normalize by product"

var duration = 2000 //duration of the transitions

//Animate (un)activation of option checkboxes
lafe.oElements.select("path").transition().duration(duration)
.attr("fill",o=>o.value?"#aaa":"#fff")

//Hide/show stuff
lafe.pBrackets.transition().delay(p=>pShow(p)?(duration/2):0).duration(p=>pShow(p)?(duration/2):0)
.attr("opacity",p=>pShow(p)?1.0:0.0)

lafe.lNodes.transition().delay(l=>lShow(l)?0:duration)
.attr("visibility",l=>lShow(l)?"visible":"hidden")

lafe.dNodes.transition().delay(t=>tShow(t)?0:duration)
.attr("visibility",t=>tShow(t)?"visible":"hidden")

lafe.l2lLinks.transition().delay(l2l=>l2lShow(l2l)?0:duration)
.attr("visibility",l2l=>l2lShow(l2l)?"visible":"hidden")

lafe.l2dLinks.transition().delay(l2d=>l2dShow(l2d)?0:duration)
.attr("visibility",l2d=>l2dShow(l2d)?"visible":"hidden")

//Update positions
lafe.pBrackets.select("path").transition().duration(duration)
.attr("d",p=>pBracketD(p,lafe.width,lafe.height))

lafe.pBrackets.select("text").transition().duration(duration)
.attr("x",p=>pBracketTextX(p,lafe.width))
.attr("y",p=>pBracketTextY(p,lafe.height))

lafe.l2lLinks.select("path").transition().duration(duration)
.attr("d",l2l=>l2lLinkD(l2l,lafe.width,lafe.height))

lafe.l2dLinks.select("path").transition().duration(duration)
.attr("d",l2d=>l2dLinkD(l2d,lafe.width,lafe.height))

lafe.lNodes.select("path.top").transition().duration(duration)
.attr("d",l=>lNodeTopD(l,lafe.width,lafe.height))

lafe.lNodes.select("path.bottom").transition().duration(duration)
.attr("d",l=>lNodeBottomD(l,lafe.width,lafe.height))

lafe.lNodes.select("text").transition().duration(duration)
.attr("x",l=>lNodeTextX(l,lafe.width))
.attr("y",l=>lNodeTextY(l,lafe.height))

lafe.dNodes.select("path").transition().duration(duration)
.attr("d",t=>dNodeD(t,lafe.width,lafe.height))
lafe.dNodes.select("text").transition().duration(duration)
.attr("x",t=>dNodeTextX(t,lafe.width))
.attr("y",t=>dNodeTextY(t,lafe.height))


}
Insert cell
Insert cell
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sept","Oct","Nov","Dec"];
Insert cell
format0 = d3.format(",.0f")

Insert cell
format2 = d3.format(",.2f")
Insert cell
function formatDate(date){

var year = date.getFullYear();
var month = months[date.getMonth()];
var day = date.getDate();

return day + '-' + month + '-' + year;

}
Insert cell
function formatSize(size,unit){
return format0(size) + (unit===undefined?"":(" [" + unit +"]"))
}
Insert cell
function formatRatio(ratio){
return format2(100*ratio) + "%"
}
Insert cell
//Assuming an array of objects containing each a code property
//Adds sortkey property to each object and return a map of the objects using code as key
function getMap(array){
array.forEach((o,i)=>o.sortkey=i)
return d3.map(array,o=>o.code)
}
Insert cell
//Assuming a map of objects containing each a sortkey property
//Returns an array of objects sorted according to the sortkey
function sortedValues(map){
return map.values().sort((o1,o2)=>o1.sortkey-o2.sortkey)
}
Insert cell
//Assuming a map with numeric values
//Increments value for key, if key not found create it with 0.0
function addValue(map,key,value){
map.set(key,(map.has(key)?map.get(key):0.0)+value)
}
Insert cell
//Builds the "d" attribute of a path representing a bracket [
//(x,y) are the relative coordinate of the upper-left corner of the bracket
//(dx,dy) are the relative width and height of the bracket
//width and height are the absolute dimensions of the container of the bracket
function bracketD(x,y,dx,dy,width,height){

//absolute coordinates of the bracket [
let x0 = Math.round(width*x)
let y0 = Math.round(height*y)
let x1 = Math.round(width*(x+dx))
let y1 = Math.round(height*y)
let x2 = Math.round(width*(x+dx))
let y2 = Math.round(height*(y+dy))
let x3 = Math.round(width*x)
let y3 = Math.round(height*(y+dy))

let d = ""
d += "M " + x1 + " " + y1 //Move to (x1,y1)
d += " L " + x0 + " " + y0 //Line to (x0,y0)
d += " L " + x3 + " " + y3 //Line to (x3,y3)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
return d

}
Insert cell
//Builds the "d" attribute of a path representing a rectangle
//(x,y) are the relative coordinate of the upper-left corner of the rectangle
//(dx,dy) are the relative width and height of the rectangle
//width and height are the absolute dimensions of the container of the rectangle
function rectangleD(x,y,dx,dy,width,height){

//absolute coordinates of the rectangle
let x0 = Math.round(width*x)
let y0 = Math.round(height*y)
let x1 = Math.round(width*(x+dx))
let y1 = Math.round(height*y)
let x2 = Math.round(width*(x+dx))
let y2 = Math.round(height*(y+dy))
let x3 = Math.round(width*x)
let y3 = Math.round(height*(y+dy))

let d = ""
d += "M " + x0 + " " + y0 //Move to (x0,y0)
d += " L " + x1 + " " + y1 //Line to (x1,y1)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
d += " L " + x3 + " " + y3 //Line to (x3,y3)
d += " L " + x0 + " " + y0 //Line to (x0,y0)
return d

}
Insert cell
//Builds the "d" attribute of a path representing a band with vertical left and right bondaries
//and curvy top and bottom bondaries
//(ix,iy) are the relative coordinate of the upper-left corner of the band (input)
//(ox,oy) are the relative coordinate of the upper-right corner of the band (output)
//(idy,ody) are the relative heights of the left (input) and right (output) boundaries
//width and height are the absolute dimensions of the container of the band
function bandD(ix,iy,idy,ox,oy,ody,width,height){

var curvature = 0.5
//absolute coordinates of the band
let x0 = Math.round(width*ix)
let y0 = Math.round(height*iy)
let x1 = Math.round(width*ox)
let y1 = Math.round(height*oy)
let x2 = Math.round(width*ox)
let y2 = Math.round(height*(oy + ody))
let x3 = Math.round(width*ix)
let y3 = Math.round(height*(iy + idy))
let dx = Math.round((x1-x0)*curvature)
let d = ""

d += "M " + x0 + " " + y0 //Move to (x0,y0)
d += " C " + (x0+dx) + " " + y0 + " " + (x1-dx) + " " + y1 + " "+ x1 + " " + y1 //Cubic curve to (x1,y1)
d += " L " + x2 + " " + y2 //Line to (x2,y2)
d += " C " + (x2-dx) + " " + y2 + " " + (x3+dx) + " " + y3 + " "+ x3 + " " + y3 //Cubic curve to (x3,y3)
d += " L " + x0 + " " + y0 //Line to (x0,y0)

return d

}
Insert cell
Insert cell
//propagate backward depth
function bPropagate(linkMap,depth){
linkMap.each(link =>{
if (link.ilot.product.stage.bdepth<depth) link.ilot.product.stage.bdepth = depth
bPropagate(link.ilot.il2lMap,depth+1)
})
}
Insert cell
function oCheckboxD(o,width,height){
let x = o.x
let y = o.y
let dx = o.dx
let dy = o.dy

return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function oCheckboxX(o,width){
return Math.round(width*(o.x + (o.last?-0.5*o.dx:1.5*o.dx)))
}
Insert cell
function oCheckboxY(o,height){
return Math.round(height*o.y)
}
Insert cell
function pBracketD(p,width,height){
let x = p.x
let y = p.y
let dx = p.dx
let dy = p.dy
return bracketD(x,y,dx,dy,width,height)
}
Insert cell
function pBracketTextX(p,width){
return Math.round(width*(p.first?(p.x+2*p.dx+lafe.nDX):(p.x-p.dx)))
}
Insert cell
function pBracketTextY(p,height){
return Math.round(height*p.y)
}
Insert cell
function lNodeTopD(l,width,height){
let ry = (l.size>0)?((l.ol2lSize+l.ol2dSize)/l.size):0.0
let x = l.x
let y = l.y
let dx = l.dx
let dy = l.dy*ry
return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function lNodeBottomD(l,width,height){
let ry = (l.size>0)?((l.ol2lSize+l.ol2dSize)/l.size):0.0
let x = l.x
let y = l.y+l.dy*ry
let dx = l.dx
let dy = l.dy*(1-ry)
return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function lNodeTextX(l,width){
return Math.round(width*(l.x+1.2*l.dx))
}
Insert cell
function lNodeTextY(l,height){
return Math.round(height*(l.y+0.5*l.dy))
}
Insert cell
function dNodeD(t,width,height){
let x = t.x
let y = t.y
let dx = t.dx
let dy = t.dy

return rectangleD(x,y,dx,dy,width,height)
}
Insert cell
function dNodeTextX(t,width){
return Math.round(width*(t.x+(t.last?-0.2:1.2)*t.dx))
}
Insert cell
function dNodeTextY(t,height){
return Math.round(height*(t.y+0.5*t.dy))
}
Insert cell
function l2lLinkD(l2l,width,height){
let ix = l2l.ix
let iy = l2l.iy
let idy = l2l.idy
let ox = l2l.ox
let oy = l2l.oy
let ody = l2l.ody

return bandD(ix,iy,idy,ox,oy,ody,width,height)
}
Insert cell
function l2dLinkD(l2d,width,height){
let ix = l2d.ix
let iy = l2d.iy
let idy = l2d.idy
let ox = l2d.ox
let oy = l2d.oy
let ody = l2d.ody

return bandD(ix,iy,idy,ox,oy,ody,width,height)
}
Insert cell
function l2lShowFwd(l2l){

l2l.olot.visible = true
l2l.olot.product.visible = true
l2l.olot.ol2lMap.each(ol2l=>{
l2lShowFwd(ol2l)
})
l2l.olot.ol2dMap.each(ol2d=>{
l2dShowFwd(ol2d)
})


}
Insert cell
function l2lShowBwd(l2l){

l2l.ilot.visible = true
l2l.ilot.product.visible = true
l2l.ilot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
}
Insert cell
function l2dShowFwd(l2d){

l2d.target.visible = true

}
Insert cell
function l2dShowBwd(l2d){

l2d.ilot.visible = true
l2d.ilot.product.visible = true
l2d.ilot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
}
Insert cell
function l2lRevealFwd(l2l){

lafe.l2lLinks.filter(x=>x==l2l).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2l.olot).classed("reveal",true)

l2l.olot.ol2lMap.each(ol2l=>{
l2lRevealFwd(ol2l)
})
l2l.olot.ol2dMap.each(ol2d=>{
l2dRevealFwd(ol2d)
})


}
Insert cell
function l2lRevealBwd(l2l){

lafe.l2lLinks.filter(x=>x==l2l).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2l.ilot).classed("reveal",true)

l2l.ilot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
}
Insert cell
function l2dRevealFwd(l2d){

lafe.l2dLinks.filter(x=>x==l2d).classed("reveal",true)
lafe.dNodes.filter(t=>t==l2d.target).classed("reveal",true)

}
Insert cell
function l2dRevealBwd(l2d){

lafe.l2dLinks.filter(x=>x==l2d).classed("reveal",true)
lafe.lNodes.filter(l=>l==l2d.ilot).classed("reveal",true)

l2d.ilot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
}
Insert cell
function resetVisible(){

lafe.products.forEach(p=>p.visible=!lafe.zoom)
lafe.lots.forEach(l=>l.visible=!lafe.zoom)
lafe.targets.forEach(t=>t.visible=!lafe.zoom)

}
Insert cell
function resetSelection(){

//Clear selected flags
lafe.lots.forEach(l=>l.selected=false)
lafe.targets.forEach(t=>t.selected=false)

//Clear reveal and select classes
lafe.lNodes.classed("select",false)
lafe.lNodes.classed("reveal",false)
lafe.dNodes.classed("select",false)
lafe.dNodes.classed("reveal",false)
lafe.l2lLinks.classed("reveal",false)
lafe.l2dLinks.classed("reveal",false)
}
Insert cell
function lClickL(lot){

let selected = lot.selected
resetSelection()
if(!selected){
lot.selected = true
lafe.lNodes.filter(l=>l==lot).classed("select",true)
lot.il2lMap.each(il2l=>{
l2lRevealBwd(il2l)
})
lot.ol2lMap.each(ol2l=>{
l2lRevealFwd(ol2l)
})
lot.ol2dMap.each(ol2d=>{
l2dRevealFwd(ol2d)
})
}

}
Insert cell
function lClickR(lot){

lafe.zoom = !lafe.zoom
resetSelection()
resetVisible()

if(lafe.zoom){

lot.visible = true
lot.product.visible = true
lot.il2lMap.each(il2l=>{
l2lShowBwd(il2l)
})
lot.ol2lMap.each(ol2l=>{
l2lShowFwd(ol2l)
})
lot.ol2dMap.each(ol2d=>{
l2dShowFwd(ol2d)
})

}

computeSizes()
update()

}
Insert cell
function tClickL(target){

let selected = target.selected
resetSelection()
if(!selected){
target.selected = true
lafe.dNodes.filter(t=>t==target).classed("select",true)
target.l2dMap.each(il2d=>{
l2dRevealBwd(il2d)
})
}

}
Insert cell
function tClickR(target){

lafe.zoom = !lafe.zoom
resetSelection()
resetVisible()

if(lafe.zoom){

target.visible = true
target.l2dMap.each(il2d=>{
l2dShowBwd(il2d)
})
}

computeSizes()
update()

}
Insert cell
function pShow(p){
return lafe.oMap.get("pGrp").value&&p.visible
}
Insert cell
function lShow(l){
return l.visible
}
Insert cell
function tShow(t){
return (lafe.oMap.get("dShw").value||t.last)&&t.visible
}
Insert cell
function l2lShow(l2l){
return l2l.ilot.visible&&l2l.olot.visible
}
Insert cell
function l2dShow(l2d){
return (lafe.oMap.get("dShw").value||l2d.target.last)&&l2d.target.visible&&l2d.ilot.visible
}
Insert cell
function fillSVG(svg,scale){

let maxWidth = (lafe.width + lafe.margin.left + lafe.margin.right)
let maxHeight = (lafe.height + lafe.margin.top + lafe.margin.bottom)

svg.attr("class", "lafe")

if (scale)
svg.attr("width","100%")
.attr("viewBox", "0 0 " + maxWidth + " " + maxHeight)
else
svg.attr("width",maxWidth)
.attr("height",maxHeight)

let g = svg
.append("g")
.attr("transform",
"translate(" + lafe.margin.left + "," + lafe.margin.top + ")");

// add the option elements
lafe.oElements = g.append("g").selectAll(".oElement")
.data(lafe.options)
.enter()
.append("g")
.attr("class", "oElement")

// add the option element checkbox shapes
lafe.oElements.filter(o=>o.type=="checkbox").append("path")
.attr("fill",o=>o.value?"#aaa":"#fff")
.attr("d",o=>oCheckboxD(o,lafe.width,lafe.height))
.on("click",function(o){
o.value = !o.value
update()
})

// add the option element texts
lafe.oElements.append("text")
.attr("x",o=>oCheckboxX(o,lafe.width))
.attr("y",o=>oCheckboxY(o,lafe.height))
.attr("dy",".7em")
.attr("text-anchor",o=>o.last?"end":"start")
.text(o=>o.title)

// add the product brackets
lafe.pBrackets = g.append("g").selectAll(".pBracket")
.data(lafe.products)
.enter()
.append("g")
.attr("class", "pBracket")

// add the product bracket shapes
lafe.pBrackets.append("path")
.attr("d",p=>pBracketD(p,lafe.width,lafe.height))
.append("title")
.text(p=>p.title);

// add the product bracket labels
lafe.pBrackets.append("text")
.attr("x",p=>pBracketTextX(p,lafe.width))
.attr("y",p=>pBracketTextY(p,lafe.height))
.attr("dy",".7em")
.attr("text-anchor",p=>(p.first?"start":"end"))
.text(p=>p.code)

// add the l2l links
lafe.l2lLinks = g.append("g").selectAll(".l2lLink")
.data(lafe.l2l)
.enter()
.append("g")
.attr("class", "l2lLink")

// add the l2l link shapes
lafe.l2lLinks.append("path")
.attr("d",l2l=>l2lLinkD(l2l,lafe.width,lafe.height))
.append("title")
.text(l2l=>l2l.title);

//add the l2d links
lafe.l2dLinks = g.append("g").selectAll(".l2dLink")
.data(lafe.l2d)
.enter()
.append("g")
.attr("class", "l2dLink")

// add the l2d link shapes
lafe.l2dLinks.append("path")
.attr("d",l2d=>l2dLinkD(l2d,lafe.width,lafe.height))
.append("title")
.text(l2d=>l2d.title);

// add the lot nodes
lafe.lNodes = g.append("g").selectAll(".lNode")
.data(lafe.lots)
.enter()
.append("g")
.attr("class", "lNode")
.on("click",function(l){
lClickL(l)
})
.on("contextmenu",function(l){
d3.event.preventDefault();
lClickR(l)
})

// add the lot node top shapes
lafe.lNodes.append("path")
.attr("d",l=>lNodeTopD(l,lafe.width,lafe.height))
.attr("class", "top")

//add the lot node bottom shapes
lafe.lNodes.append("path")
.attr("d",l=>lNodeBottomD(l,lafe.width,lafe.height))
.attr("class", "bottom")

//add the lot node titles
lafe.lNodes.append("title")
.text(l=>l.title);

// add the lot node labels
lafe.lNodes.append("text")
.attr("x",l=>lNodeTextX(l,lafe.width) )
.attr("y",l=>lNodeTextY(l,lafe.height))
.attr("dy",".35em")
.attr("text-anchor","start")
.text(l=>l.code)

// add the demand (target) nodes
lafe.dNodes = g.append("g").selectAll(".dNode")
.data(lafe.targets)
.enter()
.append("g")
.attr("class", "dNode")
.on("click",function(t){
tClickL(t)
})
.on("contextmenu",function(t){
d3.event.preventDefault();
tClickR(t)
})

// add the demand (target) node shapes
lafe.dNodes.append("path")
.attr("d",t=>dNodeD(t,lafe.width,lafe.height))
.append("title")
.text(t=>t.title);
//add the demand (target) node labels
lafe.dNodes.append("text")
.attr("x",t=>dNodeTextX(t,lafe.width))
.attr("y",t=>dNodeTextY(t,lafe.height))
.attr("dy",t=>".35em")
.attr("text-anchor",t=>t.last?"end":"start")
.text(t=>t.demand.code)

}
Insert cell
Insert cell
getSVG = function(){
let svg = d3.create("svg")

computeSizes()
computePositions()
setTitles()
fillSVG(svg,true)

return svg
}
Insert cell
Insert cell
html`
<style>
:root {
--oelement-color: black;
--label-color: grey;
--p-color-h: 077;
--p-color-s: 50%;
--p-color-l: 50%;
--l-color-h: 190;
--l-color-s: 80%;
--l-color-l: 40%;
--d-color-h: 033;
--d-color-s: 76%;
--d-color-l: 64%;
--r-color-h: 300;
--r-color-s: 80%;
--r-color-l: 80%;
}

.lafe {
font-size: 10px;
text-shadow: 0 1px 0 #fff;
}

.oElement {
--color: var(--oelement-color);
}

.oElement:hover {
font-weight: bold;
}

.oElement path {
stroke: var(--color);
}

.oElement text {
fill: var(--label-color);
pointer-events: none;
}

.pBracket {
--color-h: var(--p-color-h);
--color-s: var(--p-color-s);
--color-l: var(--p-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
}

.pBracket path{
stroke: var(--color);
fill: none;
}

.pBracket text {
fill: var(--color);
pointer-events: none;
}

.lNode {
--color-h: var(--l-color-h);
--color-s: var(--l-color-s);
--color-l: var(--l-color-l);
--darken: 1.0;
--color: hsl(var(--color-h),var(--color-s),calc(var(--color-l)*var(--darken)));
}

.lNode.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
}

.lNode:hover {
font-weight: bold;
--darken: 0.7;
}

.lNode.select{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l)*0.5;
}

.lNode .top{
stroke: var(--color);
fill: var(--color)
}

.lNode .bottom{
stroke: var(--color);
fill: white;
}

.lNode text {
fill: var(--label-color);
pointer-events: none;
}


.dNode {
--color-h: var(--d-color-h);
--color-s: var(--d-color-s);
--color-l: var(--d-color-l);
--darken: 1.0;
--color: hsl(var(--color-h),var(--color-s),calc(var(--color-l)*var(--darken)));
}

.dNode.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
}

.dNode:hover {
font-weight: bold;
--darken: 0.7;
}

.dNode.select{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l)*0.5;
}

.dNode path{
stroke: var(--color);
fill: var(--color);
}

.dNode text {
fill: var(--label-color);
}

.l2lLink {
--color-h: var(--l-color-h);
--color-s: var(--l-color-s);
--color-l: var(--l-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
--opacity: .1;

fill: var(--color);
fill-opacity: var(--opacity);
stroke: none;
}

.l2lLink.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
--opacity: .3;
}

.l2lLink:hover {
--opacity: .4;
}

.l2dLink {
--color-h: var(--d-color-h);
--color-s: var(--d-color-s);
--color-l: var(--d-color-l);
--color: hsl(var(--color-h),var(--color-s),var(--color-l));
--opacity: .1;

fill: var(--color);
opacity: var(--opacity);
stroke: none;
}

.l2dLink.reveal{
--color-h: var(--r-color-h);
--color-s: var(--r-color-s);
--color-l: var(--r-color-l);
--opacity: .3;
}

.l2dLink:hover {
--opacity: .4;
}
</style>
`
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more