Last active
September 13, 2021 19:08
-
-
Save CyberShadow/6b6ecfde854ec7d991f8774bc35bbce5 to your computer and use it in GitHub Desktop.
Illustrations for btdu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /draw | |
| /illustrations.svg |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import std.algorithm.comparison; | |
| import std.algorithm.iteration; | |
| import std.algorithm.searching; | |
| import std.array; | |
| import std.conv; | |
| import std.format; | |
| import std.stdio : toFile; | |
| import std.traits : EnumMembers; | |
| import ae.utils.meta : enumLength; | |
| import ae.utils.text : doubleToString; | |
| import ae.utils.xmlbuild; | |
| void main() | |
| { | |
| struct Illustration | |
| { | |
| string name; | |
| struct File | |
| { | |
| string name; | |
| int start, end; | |
| } | |
| File[] files; | |
| int size() const { return files.map!(f => f.end).reduce!max; } | |
| } | |
| static immutable Illustration[] illustrations = [ | |
| { | |
| "single", | |
| [ | |
| { "a", 0, 1 }, | |
| ], | |
| }, | |
| { | |
| "clone", | |
| [ | |
| { "a", 0, 1 }, | |
| { "b", 0, 1 }, | |
| ], | |
| }, | |
| { | |
| "overlap", | |
| [ | |
| { "a", 0, 2 }, | |
| { "b", 1, 3 }, | |
| ], | |
| }, | |
| ]; | |
| enum maxDiskSize = illustrations.map!(i => i.size).reduce!max; | |
| enum maxFileCount = illustrations.map!(i => i.files.length).reduce!max; | |
| enum SampleType { representative, exclusive, shared_, distributed } | |
| enum margin = 5; | |
| enum spaceUnitSize = 50; | |
| enum infoColumnWidth = 50; | |
| enum numInfoColumns = 1 + enumLength!SampleType; | |
| enum headerHeight = 50; // Table headers, space ruler | |
| enum footerHeight = 50; // Totals | |
| enum diskMargin = 25; | |
| enum fileMargin = 4; | |
| enum fileHeader = 40; | |
| enum fileFooter = 40; | |
| enum fileDataHeight = 1 * spaceUnitSize; | |
| enum fileHeight = fileHeader + fileDataHeight + fileFooter; | |
| foreach (ref illustration; illustrations) | |
| { | |
| auto svg = newXml().svg(); | |
| svg.xmlns = "http://www.w3.org/2000/svg"; | |
| svg["version"] = "1.1"; | |
| // svg.width = text( | |
| // margin + | |
| // diskMargin + | |
| // spaceUnitSize * maxDiskSize + | |
| // diskMargin + | |
| // infoColumnWidth * 4 + | |
| // margin | |
| // ); | |
| svg.style = "font-family: Arial, sans-serif"; | |
| auto defs = svg.defs(); | |
| defs.style()[] = q"EOF | |
| svg { | |
| --bg: white; | |
| --fg: black; | |
| color-scheme: light dark; | |
| } | |
| @media (prefers-color-scheme:dark) { | |
| svg { | |
| --bg: #0d1117; | |
| --fg: #aaa; | |
| } | |
| } | |
| svg { | |
| background-color: var(--bg); | |
| } | |
| EOF"; | |
| auto tableHeight = | |
| headerHeight + | |
| illustration.files.length * fileHeight + | |
| footerHeight; | |
| auto g = svg.g(); | |
| g.transform = format!"translate(%d %d)"( | |
| 0 + margin, | |
| 0 + margin, | |
| ); | |
| auto diskColumnWidth = | |
| diskMargin + | |
| spaceUnitSize * illustration.size + | |
| diskMargin; | |
| auto tableWidth = | |
| diskColumnWidth + | |
| infoColumnWidth * numInfoColumns; | |
| string tableLines = "M 0 0 v %1$d h %2$d v -%1$d z".format(tableHeight, tableWidth); | |
| { | |
| int x; | |
| void col(string title, int width, int size) | |
| { | |
| auto lines = title.split("\n"); | |
| auto lineHeight = size; | |
| auto y = (headerHeight - (lines.length - 1) * lineHeight) / 2 + (size / 3); | |
| foreach (line; lines) | |
| { | |
| g.text([ | |
| "x" : text(x + width / 2), | |
| "y" : text(y), | |
| "font-size" : text(size) ~ "px", | |
| "font-weight" : "bold", | |
| "text-anchor" : "middle", | |
| "fill" : "var(--fg)", | |
| ])[] = line; | |
| y += lineHeight; | |
| } | |
| x += width; | |
| tableLines ~= " M %d 0 v %d".format(x, tableHeight - (x == width ? footerHeight : 0)); | |
| } | |
| col("Disk layout\n\n" , diskColumnWidth, headerHeight / 4); | |
| col("File" , infoColumnWidth, headerHeight / 4); | |
| col("Repr.\nsize" , infoColumnWidth, headerHeight / 4); | |
| col("Dist.\nsize" , infoColumnWidth, headerHeight / 4); | |
| col("Excl.\nsize" , infoColumnWidth, headerHeight / 4); | |
| col("Shared\nsize", infoColumnWidth, headerHeight / 4); | |
| } | |
| string formatSize(double o) | |
| { | |
| if (o == 0) return "0"; | |
| return doubleToString(o) ~ "M"; | |
| } | |
| // Disk ruler | |
| { | |
| auto ruler = g.g(); | |
| ruler.transform = "translate(%d, %d)".format(diskMargin, headerHeight * 7 / 8); | |
| string path = "M 0 0 h %d".format(illustration.size * spaceUnitSize); | |
| enum rulerBip = spaceUnitSize / 10; | |
| foreach (o; 0 .. illustration.size + 1) | |
| { | |
| path ~= " M %d 0 v -%d".format(o * spaceUnitSize, rulerBip); | |
| ruler.text([ | |
| "x" : text(o * spaceUnitSize), | |
| "y" : text(-rulerBip * 3 / 2), | |
| "font-size" : text(spaceUnitSize / 4) ~ "px", | |
| "text-anchor" : "middle", | |
| "font-family" : "monospace", | |
| "fill" : "#888", | |
| ])[] = formatSize(o); | |
| } | |
| ruler.path([ | |
| "d" : path, | |
| "stroke" : "#888", | |
| "stroke-width" : "2px", | |
| "fill" : "none", | |
| ]); | |
| } | |
| // Dimensions: | |
| // - File index | |
| // - Sample type | |
| // - Offset of 1M block within file | |
| // - Offset within 1M block (# of segments == # of files) | |
| auto allLayers = new bool[maxFileCount][maxDiskSize][enumLength!SampleType][illustration.files.length]; | |
| auto ty = headerHeight; | |
| tableLines ~= " M 0 %d h %d".format(ty, tableWidth); | |
| foreach (fileIndex, ref file; illustration.files) | |
| { | |
| auto fileLayers = allLayers[fileIndex][]; | |
| foreach (o; file.start .. file.end) | |
| { | |
| // Representative - only if we are the first file using it | |
| fileLayers[SampleType.representative][o][0 .. illustration.files.length] = true; | |
| foreach (otherFile; illustration.files[0 .. fileIndex]) | |
| if (o >= otherFile.start && o < otherFile.end) | |
| fileLayers[SampleType.representative][o] = false; | |
| // Exclusive - only if no other file is using it | |
| fileLayers[SampleType.exclusive][o][0 .. illustration.files.length] = true; | |
| foreach (otherFileIndex, otherFile; illustration.files) | |
| if (otherFileIndex != fileIndex && o >= otherFile.start && o < otherFile.end) | |
| fileLayers[SampleType.exclusive][o] = false; | |
| // Shared - always counted | |
| fileLayers[SampleType.shared_][o][0 .. illustration.files.length] = true; | |
| // Distributed - only this file's share is counted | |
| fileLayers[SampleType.distributed][o][0 .. illustration.files.length] = true; | |
| foreach (otherFileIndex, otherFile; illustration.files) | |
| if (otherFileIndex != fileIndex && o >= otherFile.start && o < otherFile.end) | |
| fileLayers[SampleType.distributed][o][otherFileIndex] = false; | |
| } | |
| g.rect([ | |
| "x" : text(diskMargin + file.start * spaceUnitSize), | |
| "y" : text(ty + fileHeader), | |
| "width" : text((file.end - file.start) * spaceUnitSize), | |
| "height" : text(fileDataHeight ), | |
| "fill" : "none", | |
| "stroke" : "var(--fg)", | |
| "stroke-width" : "4px", | |
| ]); | |
| foreach (o; file.start .. file.end) | |
| { | |
| g.rect([ | |
| "x" : text(diskMargin + o * spaceUnitSize + fileMargin), | |
| "y" : text(ty + fileHeader + fileMargin), | |
| "width" : text(spaceUnitSize - fileMargin * 2), | |
| "height" : text(fileDataHeight - fileMargin * 2), | |
| "fill" : "var(--fg)", | |
| ]); | |
| foreach (row; 0 .. 2) | |
| { | |
| g.text([ | |
| "x" : text(diskMargin + o * spaceUnitSize + spaceUnitSize / 2), | |
| "y" : text(ty + fileHeader + fileMargin + spaceUnitSize * 3 / 8 + spaceUnitSize * 3 / 8 * row), | |
| "font-size" : text(spaceUnitSize / 3) ~ "px", | |
| "font-weight" : "bold", | |
| "text-anchor" : "middle", | |
| "font-family" : "monospace", | |
| "fill" : "var(--bg)", | |
| ])[] = "%1$d%1$d%1$d%1$d".format(1 + o); | |
| } | |
| } | |
| int x; | |
| void col(string title, int width, int size, string color = "var(--fg)") | |
| { | |
| auto y = ty + (fileHeight / 2) + (size / 3); | |
| g.text([ | |
| "x" : text(x + width / 2), | |
| "y" : text(y), | |
| "font-size" : text(size) ~ "px", | |
| "font-weight" : "bold", | |
| "text-anchor" : "middle", | |
| "fill" : color, | |
| ])[] = title; | |
| x += width; | |
| } | |
| col(null , diskColumnWidth, infoColumnWidth / 2); | |
| col(file.name , infoColumnWidth, infoColumnWidth / 3 * 2); | |
| void doType(SampleType type, string color, int y1, int dy) | |
| { | |
| string typePath; | |
| auto o = 0; | |
| foreach (gr; fileLayers[type][] | |
| .map!((ref chunk) => chunk[0 .. illustration.files.length]) | |
| .joiner | |
| .group()) | |
| { | |
| auto start = o; | |
| auto end = start + gr[1]; | |
| o = end; | |
| if (!gr[0]) | |
| continue; | |
| typePath ~= " M %d %d".format(diskMargin + start * spaceUnitSize / illustration.files.length, ty + y1); | |
| typePath ~= " v -%1$s l %1$s %1$s l -%1$s %1$s v -%1$s".format(spaceUnitSize / 100.0); | |
| typePath ~= " h %d".format((end - start) * spaceUnitSize / illustration.files.length); | |
| typePath ~= " v -%1$s l -%1$s %1$s l %1$s %1$s v -%1$s".format(spaceUnitSize / 100.0); | |
| typePath ~= " M %d %d v %d H %d V %d".format( | |
| diskMargin + (start + end) * spaceUnitSize / illustration.files.length / 2, ty + y1, | |
| fileHeight * 4 / 64 * dy, | |
| x + infoColumnWidth / 2, | |
| ty + fileHeight / 2 + (fileHeight * 5 / 32 * dy), | |
| ); | |
| enum arrowSize = infoColumnWidth / 8; | |
| typePath ~= " m 0 0 l -%d %d h %d l -%d %d z".format( | |
| arrowSize, arrowSize * +dy, | |
| arrowSize * 2, | |
| arrowSize, arrowSize * -dy, | |
| ); | |
| } | |
| g.path([ | |
| "d" : typePath, | |
| "stroke" : color, | |
| "stroke-width" : "4px", | |
| "fill" : "none", | |
| ]); | |
| auto size = fileLayers[type][].map!(fileBits => fileBits[].count!(b => b)).sum / double(illustration.files.length); | |
| col(formatSize(size), infoColumnWidth, infoColumnWidth * 2 / 5, color); | |
| } | |
| doType(SampleType.representative, "#44f", fileHeight * 8 / 32, -1); | |
| doType(SampleType.distributed , "#777", fileHeight * 4 / 32, -1); | |
| doType(SampleType.exclusive , "#f00", fileHeight * 24 / 32, 1); | |
| doType(SampleType.shared_ , "#0c0", fileHeight * 28 / 32, 1); | |
| ty += fileHeight; | |
| tableLines ~= " M 0 %d h %d".format(ty, tableWidth); | |
| } | |
| // Total | |
| { | |
| int x; | |
| void col(string title, int width, int size, string color = "var(--fg)") | |
| { | |
| auto y = ty + (footerHeight / 2) + (size / 3); | |
| g.text([ | |
| "x" : text(x + width / 2), | |
| "y" : text(y), | |
| "font-size" : text(size) ~ "px", | |
| "font-weight" : "bold", | |
| "text-anchor" : "middle", | |
| "fill" : color, | |
| ])[] = title; | |
| x += width; | |
| tableLines ~= " M %d 0 v %d".format(x, tableHeight); | |
| } | |
| col("Total" , diskColumnWidth + infoColumnWidth, infoColumnWidth / 2); | |
| void doType(SampleType type, string color) | |
| { | |
| auto size = allLayers.map!(fileLayers => fileLayers[type][].map!(fileBits => fileBits[].count!(b => b)).sum / double(illustration.files.length)).sum; | |
| col(formatSize(size), infoColumnWidth, infoColumnWidth / 2, color); | |
| } | |
| doType(SampleType.representative, "#44f"); | |
| doType(SampleType.distributed , "#888"); | |
| doType(SampleType.exclusive , "#f00"); | |
| doType(SampleType.shared_ , "#0c0"); | |
| } | |
| g.path([ | |
| "d" : tableLines, | |
| "stroke" : "var(--fg)", | |
| "stroke-width" : "4px", | |
| "fill" : "none", | |
| ]); | |
| svg.width = text( | |
| margin + | |
| tableWidth + | |
| margin | |
| ); | |
| svg.height = text( | |
| margin + | |
| tableHeight + | |
| margin | |
| ); | |
| svg.toPrettyString().toFile(illustration.name ~ ".svg"); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment