prographs 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +6 -4
- package/dist/AutoGraph-PDPEAIXL.cjs +145 -0
- package/dist/GraphType.js +19 -0
- package/dist/autograph/chartIndex.js +521 -0
- package/dist/autograph/chartModules.js +2 -1
- package/dist/autograph/index.cjs +98 -93
- package/dist/catalog-DI4XZ46L.cjs +142 -0
- package/dist/catalog.js +2 -1
- package/dist/charts/biologycharts/BiologySamples.js +59 -0
- package/dist/charts/biologycharts/PunnettSquare.js +342 -0
- package/dist/charts/biologycharts/biologyEnums.js +22 -1
- package/dist/charts/biologycharts/index.js +5 -3
- package/dist/charts/doecharts/DoeSamples.js +78 -0
- package/dist/charts/doecharts/SipocDiagram.js +453 -0
- package/dist/charts/doecharts/index.js +4 -2
- package/dist/charts/fieldcharts/SpeciesAccumulationCurve.js +350 -0
- package/dist/charts/fieldcharts/fieldEnums.js +8 -1
- package/dist/charts/fieldcharts/index.js +5 -3
- package/dist/charts/signals/AntennaPatternChart.js +492 -0
- package/dist/charts/signals/SignalSamples.js +17 -0
- package/dist/charts/signals/index.js +5 -3
- package/dist/charts/softwarediagrams/ComponentDiagram.js +504 -0
- package/dist/charts/softwarediagrams/DeploymentDiagram.js +419 -0
- package/dist/charts/softwarediagrams/SoftwareSamples.js +283 -1
- package/dist/charts/softwarediagrams/index.js +7 -3
- package/dist/charts/softwarediagrams/softwareEnums.js +45 -1
- package/dist/charts/sportscharts/PowerDurationCurve.js +301 -0
- package/dist/charts/sportscharts/SportsSamples.js +102 -0
- package/dist/charts/sportscharts/TrainingLoadChart.js +325 -0
- package/dist/charts/sportscharts/index.js +6 -2
- package/dist/charts/strategycharts/BassDiffusionChart.js +362 -0
- package/dist/charts/strategycharts/BassModel.js +96 -0
- package/dist/charts/strategycharts/BurnRunwayChart.js +284 -0
- package/dist/charts/strategycharts/CapTableChart.js +366 -0
- package/dist/charts/strategycharts/KanbanBoard.js +324 -0
- package/dist/charts/strategycharts/OkrTree.js +339 -0
- package/dist/charts/strategycharts/StrategyPane.js +27 -0
- package/dist/charts/strategycharts/StrategySamples.js +251 -0
- package/dist/charts/strategycharts/index.js +15 -3
- package/dist/charts/threefields/BsfcMap3DChart.js +231 -0
- package/dist/charts/threefields/CompressorMap3DChart.js +217 -0
- package/dist/charts/threefields/FieldGrid.js +50 -0
- package/dist/charts/threefields/LossLandscape3DChart.js +201 -0
- package/dist/charts/threefields/MapSheet.js +77 -0
- package/dist/charts/threefields/TerrainRoute3DChart.js +201 -0
- package/dist/charts/threefields/TerrainRouteMath.js +40 -0
- package/dist/charts/threefields/ThreeFieldBase.js +117 -0
- package/dist/charts/threefields/ThreeFieldsSamples.js +80 -0
- package/dist/charts/threefields/WaferMap3DChart.js +218 -0
- package/dist/charts/threefields/index.js +19 -0
- package/dist/charts/threesurfaces/index.js +2 -2
- package/dist/charts/tradingcharts/KeltnerChannelChart.js +171 -0
- package/dist/charts/tradingcharts/TradingMath.js +20 -0
- package/dist/charts/tradingcharts/index.js +4 -2
- package/dist/{chunk-ZL6XDXMK.cjs → chunk-26PSLEP4.cjs} +8 -8
- package/dist/{chunk-W3US6EFP.cjs → chunk-2BO5DZUP.cjs} +12 -12
- package/dist/{chunk-5DP7OUSE.cjs → chunk-2GG5YYSC.cjs} +6 -6
- package/dist/{chunk-7FG4JSVO.cjs → chunk-2GL2VNJU.cjs} +253 -33
- package/dist/{chunk-LCWMRCLF.cjs → chunk-2PYOI7OY.cjs} +21 -21
- package/dist/{chunk-JFWJAEBD.cjs → chunk-3U4MCSKI.cjs} +4 -4
- package/dist/{chunk-CUPLHZRR.cjs → chunk-3ZMM3FSK.cjs} +4 -4
- package/dist/{chunk-L2AV62DP.cjs → chunk-4767WQJ7.cjs} +6 -6
- package/dist/{chunk-DA2EYIIP.cjs → chunk-4E7FSSBF.cjs} +1210 -168
- package/dist/{chunk-VS5LQEZB.cjs → chunk-526STI2S.cjs} +5 -5
- package/dist/{chunk-RXIMEK6C.cjs → chunk-55767YPI.cjs} +20 -20
- package/dist/{chunk-JXLSKIUV.cjs → chunk-5FBQOMMT.cjs} +26 -26
- package/dist/{chunk-27PZV5BR.cjs → chunk-5N4Z7MTS.cjs} +4 -4
- package/dist/{chunk-KEWCUIQA.cjs → chunk-5WEV6YWA.cjs} +22 -22
- package/dist/chunk-5XOVVC5X.cjs +3281 -0
- package/dist/{chunk-HK5J7Q2W.cjs → chunk-6HLXVWXU.cjs} +9 -9
- package/dist/{chunk-7JG47RW4.cjs → chunk-6LQT4WJ5.cjs} +25 -25
- package/dist/{chunk-OCVTI6X4.cjs → chunk-6YTWOUFD.cjs} +329 -36
- package/dist/{chunk-W7IQCAVN.cjs → chunk-7F6W6HKL.cjs} +11 -11
- package/dist/{chunk-F2DCJXDM.cjs → chunk-7L6KYU5A.cjs} +21 -21
- package/dist/{chunk-GTGR6LPR.cjs → chunk-A4GIN4FE.cjs} +17 -17
- package/dist/{chunk-B2CETSR6.cjs → chunk-A4US7SA7.cjs} +5 -5
- package/dist/{chunk-3DIF2YYW.cjs → chunk-AOXPBLPW.cjs} +10 -10
- package/dist/{chunk-7TTR4UF5.cjs → chunk-ATI45Z4M.cjs} +2 -2
- package/dist/{chunk-5BFCIX2V.cjs → chunk-CPPMO4IQ.cjs} +434 -148
- package/dist/{chunk-AYZZQ6AC.cjs → chunk-CZ5KGF6Q.cjs} +9 -9
- package/dist/{chunk-7THJ3ZBK.cjs → chunk-D4I35MCE.cjs} +2 -2
- package/dist/{chunk-MCPPFRLN.cjs → chunk-DCGAAN3Z.cjs} +2 -2
- package/dist/{chunk-VABL6CVX.cjs → chunk-DEY36N37.cjs} +21 -21
- package/dist/{chunk-RNCKGJCC.cjs → chunk-DXHXD3DK.cjs} +3 -3
- package/dist/{chunk-A56ZGMVZ.cjs → chunk-E7B45DBZ.cjs} +11 -11
- package/dist/{chunk-DOR6L4MT.cjs → chunk-EJC6EAC5.cjs} +18 -18
- package/dist/{chunk-UPGNM7MA.cjs → chunk-EULJOOMA.cjs} +11 -11
- package/dist/{chunk-G4AZL4GO.cjs → chunk-EYM6LEIF.cjs} +474 -70
- package/dist/{chunk-NJ4X6XGV.cjs → chunk-EZTYX2GH.cjs} +5 -5
- package/dist/chunk-FDD4UP73.cjs +521 -0
- package/dist/{chunk-WJ3VH2Q4.cjs → chunk-GEAIGM3J.cjs} +110 -615
- package/dist/{chunk-P3SBHF43.cjs → chunk-GRUYVXTS.cjs} +2 -2
- package/dist/{chunk-NX4Z5LMS.cjs → chunk-H3SZXND5.cjs} +5 -5
- package/dist/{chunk-EE4S4TOE.cjs → chunk-HS45TOFJ.cjs} +21 -21
- package/dist/chunk-INGOG6OV.cjs +899 -0
- package/dist/{chunk-UDKA6DGT.cjs → chunk-J3CDXZHF.cjs} +4 -4
- package/dist/{chunk-YO6G7TKI.cjs → chunk-JAU5ADEU.cjs} +11 -11
- package/dist/{chunk-22LBK4GA.cjs → chunk-JQEBSHX3.cjs} +4 -4
- package/dist/{chunk-RXX3U7VQ.cjs → chunk-KLXGLMFU.cjs} +10 -10
- package/dist/chunk-KVOX36J3.cjs +327 -0
- package/dist/chunk-L6BGNHFL.cjs +139 -0
- package/dist/{chunk-GMER35L3.cjs → chunk-L7TB7PU5.cjs} +20 -20
- package/dist/{chunk-AFXYNBL2.cjs → chunk-LBTRYEM2.cjs} +4 -4
- package/dist/{chunk-WYYCKIOD.cjs → chunk-LLULNJSP.cjs} +11 -11
- package/dist/{chunk-J27XOSLP.cjs → chunk-LSO46AFV.cjs} +8 -8
- package/dist/{chunk-5XJO45ES.cjs → chunk-LZ2KJ7ZX.cjs} +11 -11
- package/dist/chunk-M4IQ5LIZ.cjs +422 -0
- package/dist/{chunk-Y2KIUKJF.cjs → chunk-MPDHDKYZ.cjs} +11 -11
- package/dist/{chunk-4EMPKRZU.cjs → chunk-MRQ42ZJE.cjs} +3 -3
- package/dist/{chunk-EU5D2MF4.cjs → chunk-MYV4KZWP.cjs} +9 -9
- package/dist/{chunk-XD7KN4UN.cjs → chunk-MZXVLC24.cjs} +6 -6
- package/dist/{chunk-PPSONARU.cjs → chunk-NLBVDFIR.cjs} +41 -2
- package/dist/{chunk-L3PEGMGM.cjs → chunk-NY3TBZY4.cjs} +33 -33
- package/dist/{chunk-6YNYXXB6.cjs → chunk-OGVVSFAR.cjs} +3 -3
- package/dist/{chunk-MLV7QLB6.cjs → chunk-OMTQIQMN.cjs} +6 -6
- package/dist/{chunk-BEEOSNK7.cjs → chunk-PEJJ6TXJ.cjs} +15 -144
- package/dist/{chunk-SOKOWUF5.cjs → chunk-PPGWHIU7.cjs} +2 -2
- package/dist/{chunk-IELWOM2J.cjs → chunk-PPO6SDAN.cjs} +6 -6
- package/dist/{chunk-6JTZZCSL.cjs → chunk-PUMKX5F2.cjs} +3 -3
- package/dist/{chunk-RFOXFY2U.cjs → chunk-PXBYWCNB.cjs} +190 -799
- package/dist/{chunk-3BYXOISI.cjs → chunk-Q3HB3IX6.cjs} +4 -4
- package/dist/{chunk-RXCOVONX.cjs → chunk-RBMUOIXE.cjs} +289 -34
- package/dist/{chunk-MSBNHCSE.cjs → chunk-RJHGCUUD.cjs} +6 -6
- package/dist/{chunk-I6VG7F6Z.cjs → chunk-RSONDMYW.cjs} +708 -141
- package/dist/{chunk-GZDNPHMT.cjs → chunk-SHMPFCUE.cjs} +19 -19
- package/dist/{chunk-ZIBVCOW6.cjs → chunk-SN4FCXFC.cjs} +19 -19
- package/dist/{chunk-7KM52NMN.cjs → chunk-SN5T7WYO.cjs} +2 -2
- package/dist/{chunk-7MSZR65Z.cjs → chunk-SQJZGVDR.cjs} +7 -7
- package/dist/chunk-SYABFMD7.cjs +633 -0
- package/dist/{chunk-2RRFEYFH.cjs → chunk-TICB3MAC.cjs} +16 -16
- package/dist/{chunk-AOOKHUV5.cjs → chunk-U7Z7XYR2.cjs} +5 -5
- package/dist/{chunk-JEWQAXVG.cjs → chunk-UA2THQFV.cjs} +6 -6
- package/dist/{chunk-LHZPAHU3.cjs → chunk-VIK4L2YF.cjs} +112 -521
- package/dist/{chunk-4YWMAQ4J.cjs → chunk-VM2Y6BDY.cjs} +4 -4
- package/dist/{chunk-IRRKE2ZE.cjs → chunk-VMS3KWYI.cjs} +21 -21
- package/dist/{chunk-4SLARW6F.cjs → chunk-VU5NUD46.cjs} +17 -17
- package/dist/{chunk-XHY6TVYG.cjs → chunk-VV5CVOF7.cjs} +13 -13
- package/dist/{chunk-CCIBPGPZ.cjs → chunk-VYSAWPSS.cjs} +57 -57
- package/dist/{chunk-TTNEMQ2A.cjs → chunk-W7YRWEWQ.cjs} +22 -22
- package/dist/{chunk-C7MUOGEW.cjs → chunk-X7R5SY62.cjs} +6 -6
- package/dist/{chunk-ILR5KBMB.cjs → chunk-XOK3KTSS.cjs} +6 -6
- package/dist/{chunk-2CZM2I3T.cjs → chunk-XUIX45VU.cjs} +19 -19
- package/dist/{chunk-TGRMPS3A.cjs → chunk-XWH46PVJ.cjs} +165 -43
- package/dist/{chunk-WQYXXECR.cjs → chunk-Y5XVMK3K.cjs} +6 -6
- package/dist/{chunk-AK4TX2WG.cjs → chunk-YCNKEDTG.cjs} +13 -13
- package/dist/{chunk-PJJB24DU.cjs → chunk-YEF7SE3X.cjs} +20 -20
- package/dist/{chunk-RVLV4XW6.cjs → chunk-YMTVPMU3.cjs} +22 -22
- package/dist/data/index.cjs +2 -2
- package/dist/entries/autograph-lazy.cjs +2 -2
- package/dist/entries/charts/acoustictransport.cjs +3 -3
- package/dist/entries/charts/analysischarts.cjs +3 -3
- package/dist/entries/charts/astrocharts.cjs +3 -3
- package/dist/entries/charts/bars.cjs +3 -3
- package/dist/entries/charts/bayesiancharts.cjs +3 -3
- package/dist/entries/charts/biocharts.cjs +3 -3
- package/dist/entries/charts/biologycharts.cjs +11 -3
- package/dist/entries/charts/biologycharts.d.ts +1 -1
- package/dist/entries/charts/biologycharts.js +3 -2
- package/dist/entries/charts/business.cjs +3 -3
- package/dist/entries/charts/carecharts.cjs +3 -3
- package/dist/entries/charts/chemistrycharts.cjs +3 -3
- package/dist/entries/charts/civicscharts.cjs +3 -3
- package/dist/entries/charts/clinicalcharts.cjs +3 -3
- package/dist/entries/charts/comparison.cjs +3 -3
- package/dist/entries/charts/demography.cjs +3 -3
- package/dist/entries/charts/density.cjs +3 -3
- package/dist/entries/charts/diagrams.cjs +3 -3
- package/dist/entries/charts/distribution.cjs +3 -3
- package/dist/entries/charts/doecharts.cjs +13 -3
- package/dist/entries/charts/doecharts.d.ts +1 -1
- package/dist/entries/charts/doecharts.js +2 -1
- package/dist/entries/charts/earthscience.cjs +3 -3
- package/dist/entries/charts/economicscharts.cjs +3 -3
- package/dist/entries/charts/energycharts.cjs +3 -3
- package/dist/entries/charts/engineering.cjs +3 -3
- package/dist/entries/charts/fieldcharts.cjs +7 -3
- package/dist/entries/charts/fieldcharts.d.ts +1 -1
- package/dist/entries/charts/fieldcharts.js +3 -2
- package/dist/entries/charts/financial.cjs +3 -3
- package/dist/entries/charts/flow.cjs +3 -3
- package/dist/entries/charts/frameworks.cjs +3 -3
- package/dist/entries/charts/genomics.cjs +3 -3
- package/dist/entries/charts/geoflows.cjs +3 -3
- package/dist/entries/charts/geomaps.cjs +3 -3
- package/dist/entries/charts/geosciencecharts.cjs +3 -3
- package/dist/entries/charts/hierarchy.cjs +3 -3
- package/dist/entries/charts/indicators.cjs +3 -3
- package/dist/entries/charts/inferencecharts.cjs +3 -3
- package/dist/entries/charts/layoutcharts.cjs +3 -3
- package/dist/entries/charts/lifescience.cjs +4 -4
- package/dist/entries/charts/linesareas.cjs +3 -3
- package/dist/entries/charts/logisticscharts.cjs +3 -3
- package/dist/entries/charts/machinecharts.cjs +7 -3
- package/dist/entries/charts/machinecharts.d.ts +1 -1
- package/dist/entries/charts/machinecharts.js +2 -1
- package/dist/entries/charts/massive.cjs +3 -3
- package/dist/entries/charts/mathematical.cjs +3 -3
- package/dist/entries/charts/matrix.cjs +3 -3
- package/dist/entries/charts/medicalcharts.cjs +4 -4
- package/dist/entries/charts/mlcharts.cjs +4 -4
- package/dist/entries/charts/modelcharts.cjs +6 -5
- package/dist/entries/charts/multivariate.cjs +3 -3
- package/dist/entries/charts/musiccharts.cjs +3 -3
- package/dist/entries/charts/network.cjs +3 -3
- package/dist/entries/charts/physicscharts.cjs +3 -3
- package/dist/entries/charts/pictorial.cjs +4 -4
- package/dist/entries/charts/portfoliocharts.cjs +3 -3
- package/dist/entries/charts/productcharts.cjs +3 -3
- package/dist/entries/charts/proportion.cjs +3 -3
- package/dist/entries/charts/qualitycharts.cjs +3 -3
- package/dist/entries/charts/radial.cjs +3 -3
- package/dist/entries/charts/reliabilitycharts.cjs +5 -3
- package/dist/entries/charts/scatter.cjs +3 -3
- package/dist/entries/charts/scientific.cjs +3 -3
- package/dist/entries/charts/seriescharts.cjs +3 -3
- package/dist/entries/charts/signals.cjs +7 -3
- package/dist/entries/charts/signals.d.ts +1 -1
- package/dist/entries/charts/signals.js +2 -1
- package/dist/entries/charts/softwarediagrams.cjs +21 -3
- package/dist/entries/charts/softwarediagrams.d.ts +1 -1
- package/dist/entries/charts/softwarediagrams.js +4 -2
- package/dist/entries/charts/sportscharts.cjs +13 -3
- package/dist/entries/charts/sportscharts.d.ts +1 -1
- package/dist/entries/charts/sportscharts.js +3 -1
- package/dist/entries/charts/statistics.cjs +6 -6
- package/dist/entries/charts/strategycharts.cjs +30 -3
- package/dist/entries/charts/strategycharts.d.ts +1 -1
- package/dist/entries/charts/strategycharts.js +6 -1
- package/dist/entries/charts/structure.cjs +3 -3
- package/dist/entries/charts/surveycharts.cjs +3 -3
- package/dist/entries/charts/systemscharts.cjs +3 -3
- package/dist/entries/charts/textcharts.cjs +5 -5
- package/dist/entries/charts/thematicmaps.cjs +4 -4
- package/dist/entries/charts/threebars.cjs +3 -3
- package/dist/entries/charts/threefields.cjs +53 -0
- package/dist/entries/charts/threefields.d.ts +1 -0
- package/dist/entries/charts/threefields.js +6 -0
- package/dist/entries/charts/threeglobe.cjs +3 -3
- package/dist/entries/charts/threenetwork.cjs +3 -3
- package/dist/entries/charts/threestrata.cjs +3 -3
- package/dist/entries/charts/threesurfaces.cjs +7 -3
- package/dist/entries/charts/threesurfaces.d.ts +1 -1
- package/dist/entries/charts/threesurfaces.js +2 -1
- package/dist/entries/charts/threevolume.cjs +4 -4
- package/dist/entries/charts/timeline.cjs +3 -3
- package/dist/entries/charts/tracecharts.cjs +3 -3
- package/dist/entries/charts/trackscharts.cjs +3 -3
- package/dist/entries/charts/tradingcharts.cjs +6 -4
- package/dist/entries/charts/tradingcharts.d.ts +1 -1
- package/dist/entries/charts/tradingcharts.js +2 -1
- package/dist/entries/three.cjs +60 -20
- package/dist/entries/three.d.ts +1 -1
- package/dist/entries/three.js +7 -1
- package/dist/index.cjs +258 -97
- package/dist/index.d.cts +1953 -10
- package/dist/index.d.ts +1953 -10
- package/dist/index.js +26 -5
- package/dist/page/index.cjs +2 -2
- package/dist/react/components.js +58 -1
- package/dist/react/index.cjs +135 -92
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +2 -2
- package/package.json +1 -1
- package/dist/AutoGraph-XLBJQDRB.cjs +0 -140
- package/dist/catalog-O7XGDD2H.cjs +0 -137
- package/dist/chunk-77K2HTRC.cjs +0 -3220
- package/dist/chunk-XEK47TVD.cjs +0 -323
- /package/dist/{chunk-UMEKIIHJ.cjs → chunk-LNWTC72Y.cjs} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { CartesianChart } from "../../core/CartesianChart.js";
|
|
2
|
+
import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
|
|
3
|
+
import { ZoomAxis } from "../../core/ChartViewport.js";
|
|
4
|
+
import { BarFrame } from "../bars/BarFrame.js";
|
|
5
|
+
import { FrameworkText } from "../frameworks/FrameworkText.js";
|
|
6
|
+
import { StrategyPane } from "./StrategyPane.js";
|
|
7
|
+
import { StrategyRows } from "./StrategyRows.js";
|
|
8
|
+
import { StrategySamples } from "./StrategySamples.js";
|
|
9
|
+
import { StrategyWords } from "./StrategyWords.js";
|
|
10
|
+
import { area, line, max, mean, min, range, scaleBand, scaleLinear } from "d3";
|
|
11
|
+
// src/charts/strategycharts/BurnRunwayChart.ts
|
|
12
|
+
/** The balance as a filled area, or as a line with a dot on each month. */
|
|
13
|
+
var BurnRunwayBalance = /* @__PURE__ */ function(BurnRunwayBalance) {
|
|
14
|
+
BurnRunwayBalance["Area"] = "area";
|
|
15
|
+
BurnRunwayBalance["Line"] = "line";
|
|
16
|
+
return BurnRunwayBalance;
|
|
17
|
+
}({});
|
|
18
|
+
/** Net burn in a pane of its own under the balance, or as bars on the balance's own scale. */
|
|
19
|
+
var BurnRunwayLayout = /* @__PURE__ */ function(BurnRunwayLayout) {
|
|
20
|
+
BurnRunwayLayout["Panels"] = "panels";
|
|
21
|
+
BurnRunwayLayout["Overlay"] = "overlay";
|
|
22
|
+
return BurnRunwayLayout;
|
|
23
|
+
}({});
|
|
24
|
+
var CASH = "Cash balance";
|
|
25
|
+
var BURN = "Net burn";
|
|
26
|
+
var SHARE = .64;
|
|
27
|
+
var PANE_GAP = 18;
|
|
28
|
+
var THICK = 28;
|
|
29
|
+
var TICK = 42;
|
|
30
|
+
var MONTH = new Intl.DateTimeFormat("en", {
|
|
31
|
+
month: "short",
|
|
32
|
+
year: "2-digit"
|
|
33
|
+
});
|
|
34
|
+
var figure = (value) => value.toLocaleString("en", { maximumFractionDigits: 1 });
|
|
35
|
+
var lasting = (value) => `${figure(value)} ${figure(value) === "1" ? "month" : "months"}`;
|
|
36
|
+
/**
|
|
37
|
+
* Cash balance by month with the net burn behind it, and the balance carried on at the recent burn rate to the month
|
|
38
|
+
* it would reach zero. Use it to read runway off recorded figures. The projection is arithmetic on the rows given,
|
|
39
|
+
* a straight line at the mean burn of the latest months, and not a forecast of what will be spent or raised.
|
|
40
|
+
*/
|
|
41
|
+
var BurnRunwayChart = class BurnRunwayChart extends CartesianChart {
|
|
42
|
+
static meta = {
|
|
43
|
+
id: "burn-runway",
|
|
44
|
+
name: "Burn and runway chart",
|
|
45
|
+
family: ChartFamily.Financial,
|
|
46
|
+
dimension: Dimension.Two,
|
|
47
|
+
shape: DataShape.Table,
|
|
48
|
+
description: "Plots month-end cash and monthly net burn, then extends the balance as a dashed line at the mean burn of the latest months to where it reaches zero.",
|
|
49
|
+
tags: [
|
|
50
|
+
"cash",
|
|
51
|
+
"burn rate",
|
|
52
|
+
"runway",
|
|
53
|
+
"start-up",
|
|
54
|
+
"projection",
|
|
55
|
+
"finance"
|
|
56
|
+
],
|
|
57
|
+
sample: (seed) => StrategySamples.burn(seed),
|
|
58
|
+
variants: {
|
|
59
|
+
balanceMark: ["area", "line"],
|
|
60
|
+
layout: ["panels", "overlay"],
|
|
61
|
+
window: [3, 6]
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** Months until `balance` reaches zero at `rate` a month: 0 when it already has, null when the rate is not a burn. */
|
|
65
|
+
static runway(balance, rate) {
|
|
66
|
+
if (balance <= 0) return 0;
|
|
67
|
+
return rate !== null && rate > 0 ? balance / rate : null;
|
|
68
|
+
}
|
|
69
|
+
static #date(cell) {
|
|
70
|
+
if (cell instanceof Date) return Number.isNaN(+cell) ? null : cell;
|
|
71
|
+
const parts = /^(\d{4})-(\d{1,2})(?:-\d{1,2})?$/.exec(String(cell ?? "").trim());
|
|
72
|
+
return parts && +parts[2] >= 1 && +parts[2] <= 12 ? new Date(+parts[1], +parts[2] - 1, 1) : null;
|
|
73
|
+
}
|
|
74
|
+
/** `high` rounded up to a tick, leaving the low end of the axis where the data puts it: a month of net inflow should not open a whole step under zero. */
|
|
75
|
+
static #rounded(high, ticks) {
|
|
76
|
+
return high > 0 ? scaleLinear().domain([0, high]).nice(ticks).domain()[1] : high;
|
|
77
|
+
}
|
|
78
|
+
#every = 1;
|
|
79
|
+
#ticks = 6;
|
|
80
|
+
defaults() {
|
|
81
|
+
return {
|
|
82
|
+
month: "month",
|
|
83
|
+
balance: "balance",
|
|
84
|
+
burn: "burn",
|
|
85
|
+
balanceMark: "area",
|
|
86
|
+
layout: "panels",
|
|
87
|
+
window: 3
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
yTicks() {
|
|
91
|
+
return this.options.yTicks ?? this.#ticks;
|
|
92
|
+
}
|
|
93
|
+
/** The pane under the balance has its own scale, so only the months can zoom. */
|
|
94
|
+
zoomAxes() {
|
|
95
|
+
return ZoomAxis.X;
|
|
96
|
+
}
|
|
97
|
+
/** The recorded months in order, each with its net burn where one is stated or can be worked out. */
|
|
98
|
+
get months() {
|
|
99
|
+
const { month, balance, burn } = this.options;
|
|
100
|
+
let before = null;
|
|
101
|
+
return StrategyRows.of(this.options.data).flatMap((row, i) => {
|
|
102
|
+
const cash = StrategyRows.number(row[balance]);
|
|
103
|
+
if (cash === null) return [];
|
|
104
|
+
const date = BurnRunwayChart.#date(row[month]), fall = before === null ? null : before - cash;
|
|
105
|
+
before = cash;
|
|
106
|
+
return [{
|
|
107
|
+
label: date ? MONTH.format(date) : FrameworkText.clean(row[month]) || `${i + 1}`,
|
|
108
|
+
date,
|
|
109
|
+
balance: cash,
|
|
110
|
+
burn: StrategyRows.number(row[burn]) ?? fall
|
|
111
|
+
}];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
get outlook() {
|
|
115
|
+
const recorded = this.months, last = recorded.at(-1), { window } = this.options;
|
|
116
|
+
const recent = recorded.slice(-(Number.isFinite(window) ? Math.max(1, Math.round(window)) : 3)).flatMap((month) => month.burn ?? []), rate = mean(recent) ?? null;
|
|
117
|
+
const runway = last ? BurnRunwayChart.runway(last.balance, rate) : null;
|
|
118
|
+
const reach = runway === null || rate === null ? 0 : Math.min(runway, Math.max(6, recorded.length * 2));
|
|
119
|
+
const ahead = reach > 0 ? [...range(0, Math.floor(reach) + 1), ...Number.isInteger(reach) ? [] : [reach]] : [];
|
|
120
|
+
return {
|
|
121
|
+
rate,
|
|
122
|
+
averaged: recent.length,
|
|
123
|
+
months: runway,
|
|
124
|
+
path: ahead.map((step) => ({
|
|
125
|
+
ahead: step,
|
|
126
|
+
balance: step === runway ? 0 : last.balance - rate * step
|
|
127
|
+
}))
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** A label per band: the recorded months, then the months of the projection. */
|
|
131
|
+
#labels(recorded, { path }) {
|
|
132
|
+
const from = recorded.at(-1)?.date;
|
|
133
|
+
return [...recorded.map((month) => month.label), ...range(1, Math.ceil(path.at(-1)?.ahead ?? 0) + 1).map((step) => from ? MONTH.format(new Date(from.getFullYear(), from.getMonth() + step, 1)) : `+${step}`)];
|
|
134
|
+
}
|
|
135
|
+
tickFormat(scale) {
|
|
136
|
+
if (!("bandwidth" in scale)) return super.tickFormat(scale);
|
|
137
|
+
const labels = this.#labels(this.months, this.outlook);
|
|
138
|
+
return (key) => +key % this.#every ? "" : FrameworkText.fit(labels[+key] ?? "", TICK * this.#every, 11);
|
|
139
|
+
}
|
|
140
|
+
#pane(height, recorded, rate) {
|
|
141
|
+
const burns = recorded.flatMap((month) => month.burn ?? []), low = Math.min(0, min(burns) ?? 0), high = Math.max(0, max(burns) ?? 0, rate ?? 0);
|
|
142
|
+
return scaleLinear([low, BurnRunwayChart.#rounded(high > low ? high : low + 1, 4)], [height, Math.round(height * SHARE) + PANE_GAP]);
|
|
143
|
+
}
|
|
144
|
+
scales(ctx) {
|
|
145
|
+
const recorded = this.months, outlook = this.outlook, overlay = this.options.layout === "overlay";
|
|
146
|
+
const x = scaleBand(this.#labels(recorded, outlook).map((_, i) => `${i}`), [0, ctx.plot.width]).paddingInner(.3).paddingOuter(.2);
|
|
147
|
+
this.#every = Math.max(1, Math.ceil(TICK / Math.max(1, x.step())));
|
|
148
|
+
const values = [...recorded.map((month) => month.balance), ...overlay ? recorded.flatMap((month) => month.burn ?? []) : []];
|
|
149
|
+
const low = Math.min(0, min(values) ?? 0), high = max(values) ?? 1, base = overlay ? ctx.plot.height : Math.round(ctx.plot.height * SHARE);
|
|
150
|
+
this.#ticks = Math.max(2, Math.min(6, Math.floor(base / 24)));
|
|
151
|
+
return {
|
|
152
|
+
x,
|
|
153
|
+
y: scaleLinear([low, BurnRunwayChart.#rounded(high > low ? high : low + 1, this.#ticks)], [base, 0])
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
plotMarks(ctx, { x, y }) {
|
|
157
|
+
const { balanceMark, layout } = this.options, { palette, theme, plot } = ctx;
|
|
158
|
+
const recorded = this.months, outlook = this.outlook, { rate, path } = outlook, labels = this.#labels(recorded, outlook);
|
|
159
|
+
const cash = palette.color(CASH), spend = palette.color(BURN), latest = recorded.length - 1;
|
|
160
|
+
const pane = layout === "overlay" ? y : this.#pane(plot.height, recorded, rate);
|
|
161
|
+
const centre = (index) => (x(`${Math.floor(index)}`) ?? 0) + x.bandwidth() / 2 + (index - Math.floor(index)) * x.step();
|
|
162
|
+
const told = (month) => ({
|
|
163
|
+
title: month.label,
|
|
164
|
+
rows: [[CASH, StrategyPane.compact(month.balance)], ...month.burn === null ? [] : [[BURN, StrategyPane.compact(month.burn)]]]
|
|
165
|
+
});
|
|
166
|
+
if (pane !== y) StrategyPane.axis(ctx, "burn", pane, 2, StrategyPane.compact);
|
|
167
|
+
const edge = path.length ? [centre(latest) + x.step() / 2] : [];
|
|
168
|
+
const divider = ctx.marks("axis-now", "line", edge).attr("stroke", theme.axis).attr("stroke-dasharray", "2 3");
|
|
169
|
+
ctx.set(divider, {
|
|
170
|
+
x1: (at) => at,
|
|
171
|
+
x2: (at) => at,
|
|
172
|
+
y1: 0,
|
|
173
|
+
y2: plot.height
|
|
174
|
+
});
|
|
175
|
+
const frame = new BarFrame(true, pane, x), thick = Math.min(x.bandwidth(), THICK);
|
|
176
|
+
const spent = recorded.flatMap((month, i) => month.burn === null ? [] : [{
|
|
177
|
+
key: `${i}`,
|
|
178
|
+
month,
|
|
179
|
+
burn: month.burn
|
|
180
|
+
}]);
|
|
181
|
+
const bars = ctx.marks("burn", "path", spent, (bar) => bar.key).attr("fill", spend);
|
|
182
|
+
ctx.set(bars, { d: (bar) => frame.bar(centre(+bar.key) - thick / 2, thick, 0, bar.burn, 3) }, { d: (bar) => frame.bar(centre(+bar.key) - thick / 2, thick, 0, 0, 0) }, 10);
|
|
183
|
+
ctx.hover(bars, (bar) => ({
|
|
184
|
+
...told(bar.month),
|
|
185
|
+
color: spend
|
|
186
|
+
}));
|
|
187
|
+
const averaged = rate === null ? [] : [{
|
|
188
|
+
from: centre(Math.max(0, recorded.length - outlook.averaged)) - x.step() / 2,
|
|
189
|
+
to: path.length ? centre(latest + path.at(-1).ahead) : centre(latest) + x.step() / 2
|
|
190
|
+
}];
|
|
191
|
+
const level = ctx.marks("burn-rate", "line", averaged).attr("stroke", spend).attr("stroke-width", 1.5).attr("stroke-dasharray", "5 4");
|
|
192
|
+
ctx.set(level, {
|
|
193
|
+
x1: (span) => span.from,
|
|
194
|
+
x2: (span) => span.to,
|
|
195
|
+
y1: pane(rate ?? 0),
|
|
196
|
+
y2: pane(rate ?? 0)
|
|
197
|
+
});
|
|
198
|
+
ctx.hover(level, () => ({
|
|
199
|
+
title: "Recent burn rate",
|
|
200
|
+
color: spend,
|
|
201
|
+
rows: [["Mean net burn", `${StrategyPane.compact(rate ?? 0)} a month`], ["Months averaged", outlook.averaged]]
|
|
202
|
+
}));
|
|
203
|
+
const past = recorded.map((month, i) => [centre(i), y(month.balance)]), ahead = path.map((step) => [centre(latest + step.ahead), y(step.balance)]);
|
|
204
|
+
const under = area().x((point) => point[0]).y0(y(0)).y1((point) => point[1]);
|
|
205
|
+
const filled = balanceMark === "area" ? [{
|
|
206
|
+
key: "recorded",
|
|
207
|
+
points: past,
|
|
208
|
+
opacity: .2
|
|
209
|
+
}, {
|
|
210
|
+
key: "projected",
|
|
211
|
+
points: ahead,
|
|
212
|
+
opacity: .08
|
|
213
|
+
}].filter((part) => part.points.length > 1) : [];
|
|
214
|
+
const fills = ctx.marks("balance-area", "path", filled, (part) => part.key).attr("fill", cash).attr("fill-opacity", (part) => part.opacity);
|
|
215
|
+
ctx.set(fills, { d: (part) => under(part.points) ?? "" });
|
|
216
|
+
const traced = ctx.marks("balance", "path", past.length > 1 ? [past] : []).attr("fill", "none").attr("stroke", cash).attr("stroke-width", 2).attr("stroke-linejoin", "round");
|
|
217
|
+
ctx.set(traced, { d: (points) => line()(points) ?? "" });
|
|
218
|
+
ctx.drawOn(traced);
|
|
219
|
+
ctx.hover(traced, () => ({
|
|
220
|
+
title: CASH,
|
|
221
|
+
color: cash,
|
|
222
|
+
rows: [["Latest", StrategyPane.compact(recorded[latest].balance)], ["Months recorded", recorded.length]]
|
|
223
|
+
}));
|
|
224
|
+
const runway = outlook.months === null ? "None at this rate" : lasting(outlook.months);
|
|
225
|
+
const dashed = ctx.marks("projection", "path", ahead.length > 1 ? [ahead] : []).attr("fill", "none").attr("stroke", cash).attr("stroke-width", 2).attr("stroke-dasharray", "6 4");
|
|
226
|
+
ctx.set(dashed, { d: (points) => line()(points) ?? "" });
|
|
227
|
+
ctx.hover(dashed, () => ({
|
|
228
|
+
title: "Balance at the recent burn rate",
|
|
229
|
+
color: cash,
|
|
230
|
+
rows: [["Burn rate", `${StrategyPane.compact(rate ?? 0)} a month`], ["Runway", runway]]
|
|
231
|
+
}));
|
|
232
|
+
const dotted = recorded.map((month, i) => ({
|
|
233
|
+
key: `${i}`,
|
|
234
|
+
month,
|
|
235
|
+
at: past[i]
|
|
236
|
+
})).filter((dot, i) => i === latest || balanceMark === "line" && x.step() >= 12);
|
|
237
|
+
const dots = ctx.marks("months", "circle", dotted, (dot) => dot.key).attr("fill", cash).attr("stroke", theme.surface).attr("stroke-width", 2);
|
|
238
|
+
ctx.set(dots, {
|
|
239
|
+
cx: (dot) => dot.at[0],
|
|
240
|
+
cy: (dot) => dot.at[1],
|
|
241
|
+
r: 4
|
|
242
|
+
}, { r: 0 }, 10);
|
|
243
|
+
ctx.hover(dots, (dot) => ({
|
|
244
|
+
...told(dot.month),
|
|
245
|
+
color: cash
|
|
246
|
+
}));
|
|
247
|
+
const spentOut = path.length && path.at(-1).ahead === outlook.months ? [ahead.at(-1)] : [];
|
|
248
|
+
const end = ctx.marks("cash-out", "circle", spentOut).attr("fill", theme.surface).attr("stroke", cash).attr("stroke-width", 2);
|
|
249
|
+
ctx.set(end, {
|
|
250
|
+
cx: (point) => point[0],
|
|
251
|
+
cy: (point) => point[1],
|
|
252
|
+
r: 4.5
|
|
253
|
+
}, { r: 0 });
|
|
254
|
+
ctx.hover(end, () => ({
|
|
255
|
+
title: `Reaches zero in ${labels.at(-1)}`,
|
|
256
|
+
color: cash,
|
|
257
|
+
rows: [["Runway", runway], ["Burn rate", `${StrategyPane.compact(rate ?? 0)} a month`]]
|
|
258
|
+
}));
|
|
259
|
+
StrategyWords.draw(ctx, "notes", this.#notes(recorded, outlook, plot.width));
|
|
260
|
+
}
|
|
261
|
+
/** The runway in words: at the right over the projection, or at the left when there is none to draw. */
|
|
262
|
+
#notes(recorded, { rate, averaged, months: runway, path }, width) {
|
|
263
|
+
if (!recorded.length) return [];
|
|
264
|
+
const span = averaged === 1 ? "month" : `${averaged} months`, room = Math.min(width - 8, width * .6);
|
|
265
|
+
const rated = (lead, burn) => [
|
|
266
|
+
`, the mean of the last ${span}`,
|
|
267
|
+
` (${averaged}-month mean)`,
|
|
268
|
+
""
|
|
269
|
+
].map((tail) => `${lead} ${StrategyPane.compact(burn)} a month${tail}`).filter((text) => FrameworkText.width(text, 10.5) <= room).slice(0, 1);
|
|
270
|
+
return (path.length ? [`Runway ${lasting(runway)}`, ...rated("at", rate)] : runway === 0 ? ["Balance is at or below zero"] : rate === null ? ["No net burn recorded to project from"] : ["Cash is not falling at the recent rate", ...rated("net burn", rate)]).map((text, row) => ({
|
|
271
|
+
key: `note/${row}`,
|
|
272
|
+
text: FrameworkText.fit(text, width - 8, 11),
|
|
273
|
+
full: text,
|
|
274
|
+
x: path.length ? width - 2 : 4,
|
|
275
|
+
y: 7 + row * 14,
|
|
276
|
+
anchor: path.length ? "end" : "start",
|
|
277
|
+
size: row ? 10.5 : 11.5,
|
|
278
|
+
weight: row ? 400 : 600,
|
|
279
|
+
tone: row ? "secondary" : "ink",
|
|
280
|
+
halo: true
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
export { BurnRunwayBalance, BurnRunwayChart, BurnRunwayLayout };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { CartesianChart } from "../../core/CartesianChart.js";
|
|
2
|
+
import { ChartFamily, DataShape, Dimension, Orientation } from "../../core/enums.js";
|
|
3
|
+
import { BarFrame } from "../bars/BarFrame.js";
|
|
4
|
+
import { FrameworkText } from "../frameworks/FrameworkText.js";
|
|
5
|
+
import { StrategyRows } from "./StrategyRows.js";
|
|
6
|
+
import { StrategySamples } from "./StrategySamples.js";
|
|
7
|
+
import { StrategyWords } from "./StrategyWords.js";
|
|
8
|
+
import { max, scaleBand, scaleLinear, sum } from "d3";
|
|
9
|
+
// src/charts/strategycharts/CapTableChart.ts
|
|
10
|
+
/** Every round as a stacked bar, one round as a single bar, or one holder's ownership stepping from round to round. */
|
|
11
|
+
var CapTableView = /* @__PURE__ */ function(CapTableView) {
|
|
12
|
+
CapTableView["Rounds"] = "rounds";
|
|
13
|
+
CapTableView["Single"] = "single-round";
|
|
14
|
+
CapTableView["Dilution"] = "dilution";
|
|
15
|
+
return CapTableView;
|
|
16
|
+
}({});
|
|
17
|
+
/** Bars as shares of the fully diluted total, or as share counts so the growing total shows. */
|
|
18
|
+
var CapTableBasis = /* @__PURE__ */ function(CapTableBasis) {
|
|
19
|
+
CapTableBasis["Percent"] = "percent";
|
|
20
|
+
CapTableBasis["Shares"] = "shares";
|
|
21
|
+
return CapTableBasis;
|
|
22
|
+
}({});
|
|
23
|
+
var LATEST = "Latest";
|
|
24
|
+
var SINGLE = 84;
|
|
25
|
+
var SIZE = 10.5;
|
|
26
|
+
var HEAD = 14;
|
|
27
|
+
var figure = (value, digits = 1) => value.toLocaleString("en", { maximumFractionDigits: digits });
|
|
28
|
+
var percent = (share, digits = 1) => `${figure(share * 100, digits)}%`;
|
|
29
|
+
var points = (change) => `${change > 0 ? "+" : change < 0 ? "−" : ""}${figure(Math.abs(change) * 100)} pts`;
|
|
30
|
+
var count = (shares) => Intl.NumberFormat("en", {
|
|
31
|
+
notation: "compact",
|
|
32
|
+
maximumFractionDigits: 1
|
|
33
|
+
}).format(shares);
|
|
34
|
+
/**
|
|
35
|
+
* A capitalisation table across funding rounds: who holds what share of a company after each round, stacked to the
|
|
36
|
+
* fully diluted total, with ribbons that narrow as new shares dilute the earlier holders. Use it to show how ownership
|
|
37
|
+
* moved from round to round, how one round split, or how far one holder was diluted.
|
|
38
|
+
*/
|
|
39
|
+
var CapTableChart = class CapTableChart extends CartesianChart {
|
|
40
|
+
static meta = {
|
|
41
|
+
id: "cap-table",
|
|
42
|
+
name: "Cap table chart",
|
|
43
|
+
family: ChartFamily.Composition,
|
|
44
|
+
dimension: Dimension.Two,
|
|
45
|
+
shape: DataShape.Table,
|
|
46
|
+
description: "Stacks each holder's share of the fully diluted total for every funding round, with ribbons that show dilution from one round to the next.",
|
|
47
|
+
tags: [
|
|
48
|
+
"capitalisation table",
|
|
49
|
+
"ownership",
|
|
50
|
+
"dilution",
|
|
51
|
+
"funding rounds",
|
|
52
|
+
"equity",
|
|
53
|
+
"start-up"
|
|
54
|
+
],
|
|
55
|
+
sample: (seed) => StrategySamples.capTable(seed),
|
|
56
|
+
variants: {
|
|
57
|
+
view: [
|
|
58
|
+
"rounds",
|
|
59
|
+
"single-round",
|
|
60
|
+
"dilution"
|
|
61
|
+
],
|
|
62
|
+
orientation: [Orientation.Vertical, Orientation.Horizontal],
|
|
63
|
+
basis: ["percent", "shares"],
|
|
64
|
+
ribbons: [true, false]
|
|
65
|
+
},
|
|
66
|
+
/** One bar has no neighbour to join and the same proportions on either basis, and the waterfall is in points of ownership. */
|
|
67
|
+
valid: (values) => values.view === "rounds" || values.basis === "percent" && values.ribbons === true
|
|
68
|
+
};
|
|
69
|
+
/** The waterfall of `holder`'s ownership: its share after the first round it holds in, the change at each later round, and where it stands. */
|
|
70
|
+
static dilution(rounds, holder) {
|
|
71
|
+
const shares = rounds.map((round) => round.stakes.find((stake) => stake.holder === holder)?.share ?? 0), first = shares.findIndex((share) => share > 0);
|
|
72
|
+
if (first < 0) return [];
|
|
73
|
+
const steps = shares.slice(first).map((share, i) => ({
|
|
74
|
+
label: rounds[first + i].round,
|
|
75
|
+
from: i ? shares[first + i - 1] : 0,
|
|
76
|
+
to: share,
|
|
77
|
+
total: !i
|
|
78
|
+
}));
|
|
79
|
+
return steps.length > 1 ? [...steps, {
|
|
80
|
+
label: LATEST,
|
|
81
|
+
from: 0,
|
|
82
|
+
to: shares.at(-1),
|
|
83
|
+
total: true
|
|
84
|
+
}] : steps;
|
|
85
|
+
}
|
|
86
|
+
#room = 84;
|
|
87
|
+
defaults() {
|
|
88
|
+
return {
|
|
89
|
+
round: "round",
|
|
90
|
+
holder: "holder",
|
|
91
|
+
shares: "shares",
|
|
92
|
+
view: "rounds",
|
|
93
|
+
orientation: Orientation.Vertical,
|
|
94
|
+
basis: "percent",
|
|
95
|
+
ribbons: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
get #vertical() {
|
|
99
|
+
return this.options.orientation !== Orientation.Horizontal;
|
|
100
|
+
}
|
|
101
|
+
#ledger() {
|
|
102
|
+
const { round, holder, shares } = this.options;
|
|
103
|
+
const held = /* @__PURE__ */ new Map(), holders = /* @__PURE__ */ new Set();
|
|
104
|
+
for (const row of StrategyRows.of(this.options.data)) {
|
|
105
|
+
const name = FrameworkText.clean(row[round]), who = FrameworkText.clean(row[holder]), amount = StrategyRows.number(row[shares]);
|
|
106
|
+
if (!name || !who || amount === null || amount < 0) continue;
|
|
107
|
+
holders.add(who);
|
|
108
|
+
const stakes = held.get(name) ?? /* @__PURE__ */ new Map();
|
|
109
|
+
held.set(name, stakes.set(who, (stakes.get(who) ?? 0) + amount));
|
|
110
|
+
}
|
|
111
|
+
let earlier = null;
|
|
112
|
+
return {
|
|
113
|
+
rounds: [...held].map(([name, stakes]) => {
|
|
114
|
+
const total = sum(stakes.values()), before = earlier;
|
|
115
|
+
const listed = [...holders].filter((who) => stakes.get(who)).map((who) => ({
|
|
116
|
+
round: name,
|
|
117
|
+
holder: who,
|
|
118
|
+
shares: stakes.get(who),
|
|
119
|
+
share: stakes.get(who) / total,
|
|
120
|
+
before: before ? before.get(who) ?? 0 : null
|
|
121
|
+
}));
|
|
122
|
+
earlier = new Map(listed.map((stake) => [stake.holder, stake.share]));
|
|
123
|
+
return {
|
|
124
|
+
round: name,
|
|
125
|
+
total,
|
|
126
|
+
stakes: listed
|
|
127
|
+
};
|
|
128
|
+
}),
|
|
129
|
+
holders: [...holders]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Each round in order, with every holder's stake in it. */
|
|
133
|
+
get rounds() {
|
|
134
|
+
return this.#ledger().rounds;
|
|
135
|
+
}
|
|
136
|
+
/** What the view draws: the stacked slices of the rounds shown, or the steps of the followed holder's waterfall. */
|
|
137
|
+
#plan() {
|
|
138
|
+
const { view, basis, focusRound, focusHolder } = this.options, { rounds, holders } = this.#ledger();
|
|
139
|
+
const holder = holders.includes(focusHolder ?? "") ? focusHolder : holders[0] ?? "";
|
|
140
|
+
if (view === "dilution") return {
|
|
141
|
+
rounds,
|
|
142
|
+
holders,
|
|
143
|
+
holder,
|
|
144
|
+
shown: [],
|
|
145
|
+
slices: [],
|
|
146
|
+
steps: CapTableChart.dilution(rounds, holder).map((step, i) => ({
|
|
147
|
+
...step,
|
|
148
|
+
key: `${i}`
|
|
149
|
+
}))
|
|
150
|
+
};
|
|
151
|
+
const shown = view === "single-round" ? rounds.filter((round, i) => rounds.some((other) => other.round === focusRound) ? round.round === focusRound : i === rounds.length - 1) : rounds;
|
|
152
|
+
return {
|
|
153
|
+
rounds,
|
|
154
|
+
holders,
|
|
155
|
+
holder,
|
|
156
|
+
shown,
|
|
157
|
+
slices: shown.flatMap((round) => {
|
|
158
|
+
let at = 0;
|
|
159
|
+
return round.stakes.map((stake, i) => {
|
|
160
|
+
const from = at;
|
|
161
|
+
at += basis === "shares" ? stake.shares : stake.share * 100;
|
|
162
|
+
return {
|
|
163
|
+
key: `${stake.round}/${stake.holder}`,
|
|
164
|
+
stake,
|
|
165
|
+
from,
|
|
166
|
+
to: at,
|
|
167
|
+
outer: i === round.stakes.length - 1
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}),
|
|
171
|
+
steps: []
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
get #stepped() {
|
|
175
|
+
return this.options.view === "dilution";
|
|
176
|
+
}
|
|
177
|
+
/** The followed holder is named over the waterfall, where a legend of one entry would not show. */
|
|
178
|
+
plotMargin() {
|
|
179
|
+
const base = super.plotMargin();
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
top: base.top + (this.#stepped ? HEAD : 0)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
legendItems() {
|
|
186
|
+
return this.#stepped ? [] : super.legendItems();
|
|
187
|
+
}
|
|
188
|
+
tickFormat(scale) {
|
|
189
|
+
if ("bandwidth" in scale) {
|
|
190
|
+
const steps = this.#stepped ? this.#plan().steps : null;
|
|
191
|
+
return (name) => FrameworkText.fit(steps ? steps[+name]?.label ?? "" : String(name), this.#room, 11);
|
|
192
|
+
}
|
|
193
|
+
return this.#stepped || this.options.basis !== "shares" ? (value) => `${figure(+value)}%` : super.tickFormat(scale);
|
|
194
|
+
}
|
|
195
|
+
scales(ctx) {
|
|
196
|
+
const { view, basis, ribbons } = this.options, vertical = this.#vertical, { width, height } = ctx.plot;
|
|
197
|
+
const { steps, shown } = this.#plan();
|
|
198
|
+
const names = this.#stepped ? steps.map((step) => step.key) : shown.map((round) => round.round);
|
|
199
|
+
const top = this.#stepped ? (max(steps, (step) => Math.max(step.from, step.to)) ?? 1) * 125 : basis === "shares" ? max(shown, (round) => round.total) || 1 : 100;
|
|
200
|
+
const band = scaleBand(names, [0, vertical ? width : height]).padding(view === "rounds" && ribbons ? .45 : .3);
|
|
201
|
+
const value = scaleLinear([0, top], vertical ? [height, 0] : [0, width]);
|
|
202
|
+
this.#room = vertical ? Math.max(12, band.step() - 6) : 84;
|
|
203
|
+
return vertical ? {
|
|
204
|
+
x: band,
|
|
205
|
+
y: value
|
|
206
|
+
} : {
|
|
207
|
+
x: value,
|
|
208
|
+
y: band
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
plotMarks(ctx, { x, y }) {
|
|
212
|
+
const { view, basis, ribbons } = this.options, { palette, theme, plot } = ctx;
|
|
213
|
+
const vertical = this.#vertical, frame = new BarFrame(vertical, vertical ? y : x, vertical ? x : y);
|
|
214
|
+
const { rounds, holders, holder, slices, steps } = this.#plan();
|
|
215
|
+
for (const name of holders) palette.color(name);
|
|
216
|
+
const thick = view === "single-round" ? Math.min(frame.thick, SINGLE) : frame.thick;
|
|
217
|
+
const at = (name) => (frame.band(name) ?? 0) + (frame.thick - thick) / 2;
|
|
218
|
+
const joins = view === "rounds" && ribbons ? slices.flatMap((left) => {
|
|
219
|
+
const next = rounds[rounds.findIndex((round) => round.round === left.stake.round) + 1];
|
|
220
|
+
const right = next && slices.find((slice) => slice.stake.round === next.round && slice.stake.holder === left.stake.holder);
|
|
221
|
+
return right ? [{
|
|
222
|
+
key: left.key,
|
|
223
|
+
holder: left.stake.holder,
|
|
224
|
+
left,
|
|
225
|
+
right
|
|
226
|
+
}] : [];
|
|
227
|
+
}) : [];
|
|
228
|
+
const ribbon = ({ left, right }) => {
|
|
229
|
+
const near = at(left.stake.round) + thick, far = at(right.stake.round);
|
|
230
|
+
return `M${frame.point(near, left.from)}L${frame.point(near, left.to)}L${frame.point(far, right.to)}L${frame.point(far, right.from)}Z`;
|
|
231
|
+
};
|
|
232
|
+
const bands = ctx.marks("ribbons", "path", joins, (join) => join.key).attr("fill", (join) => palette.color(join.holder)).attr("fill-opacity", .22);
|
|
233
|
+
ctx.set(bands, { d: ribbon });
|
|
234
|
+
ctx.hover(bands, ({ left, right }) => ({
|
|
235
|
+
title: left.stake.holder,
|
|
236
|
+
color: palette.color(left.stake.holder),
|
|
237
|
+
rows: [
|
|
238
|
+
[left.stake.round, percent(left.stake.share)],
|
|
239
|
+
[right.stake.round, percent(right.stake.share)],
|
|
240
|
+
["Change", points(right.stake.share - left.stake.share)]
|
|
241
|
+
]
|
|
242
|
+
}));
|
|
243
|
+
const bars = ctx.marks("stakes", "path", slices, (slice) => slice.key).attr("fill", (slice) => palette.color(slice.stake.holder));
|
|
244
|
+
const shape = (slice, to) => frame.bar(at(slice.stake.round), thick, slice.from, to, slice.outer ? 4 : 0, slice.from ? 2 : 0);
|
|
245
|
+
ctx.set(bars, { d: (slice) => shape(slice, slice.to) }, { d: (slice) => shape(slice, slice.from) }, 12);
|
|
246
|
+
ctx.hover(bars, ({ stake }) => ({
|
|
247
|
+
title: stake.holder,
|
|
248
|
+
color: palette.color(stake.holder),
|
|
249
|
+
rows: [
|
|
250
|
+
["Round", stake.round],
|
|
251
|
+
["Shares", stake.shares.toLocaleString("en")],
|
|
252
|
+
["Ownership", percent(stake.share, 2)],
|
|
253
|
+
...stake.before === null ? [] : [["Since last round", points(stake.share - stake.before)]]
|
|
254
|
+
]
|
|
255
|
+
}));
|
|
256
|
+
const words = slices.flatMap((slice) => {
|
|
257
|
+
const { stake } = slice, across = Math.abs(frame.value(slice.to) - frame.value(slice.from)), start = at(stake.round);
|
|
258
|
+
if (view === "single-round") return CapTableChart.#named(slice, frame, start, thick, across, plot);
|
|
259
|
+
const text = basis === "shares" ? count(stake.shares) : percent(stake.share, 0), wide = FrameworkText.width(text, SIZE) + 6;
|
|
260
|
+
if (vertical ? across < 14 || thick < wide : across < wide || thick < 14) return [];
|
|
261
|
+
const [px, py] = frame.point(start + thick / 2, (slice.from + slice.to) / 2);
|
|
262
|
+
return [{
|
|
263
|
+
key: slice.key,
|
|
264
|
+
text,
|
|
265
|
+
x: px,
|
|
266
|
+
y: py,
|
|
267
|
+
anchor: "middle",
|
|
268
|
+
size: SIZE,
|
|
269
|
+
weight: 600,
|
|
270
|
+
fill: palette.inkOn(palette.color(stake.holder))
|
|
271
|
+
}];
|
|
272
|
+
});
|
|
273
|
+
const fill = (step) => step.total ? palette.color(holder) : palette.diverging(step.to < step.from ? .9 : -.9);
|
|
274
|
+
const rules = steps.slice(0, -1).map((step, i) => ({
|
|
275
|
+
key: step.key,
|
|
276
|
+
...frame.rule(step.to * 100, at(step.key) + thick, at(steps[i + 1].key))
|
|
277
|
+
}));
|
|
278
|
+
const links = ctx.marks("connectors", "line", rules, (rule) => rule.key).attr("stroke", theme.axis).attr("stroke-width", 1);
|
|
279
|
+
ctx.set(links, {
|
|
280
|
+
x1: (rule) => rule.x1,
|
|
281
|
+
x2: (rule) => rule.x2,
|
|
282
|
+
y1: (rule) => rule.y1,
|
|
283
|
+
y2: (rule) => rule.y2
|
|
284
|
+
});
|
|
285
|
+
const falls = ctx.marks("steps", "path", steps, (step) => step.key).attr("fill", fill);
|
|
286
|
+
ctx.set(falls, { d: (step) => frame.bar(at(step.key), thick, step.from * 100, step.to * 100, 4) }, { d: (step) => frame.bar(at(step.key), thick, step.from * 100, step.from * 100, 0) }, 30);
|
|
287
|
+
ctx.hover(falls, (step) => ({
|
|
288
|
+
title: step.label,
|
|
289
|
+
color: fill(step),
|
|
290
|
+
rows: step.total ? [[holder, percent(step.to, 2)]] : [
|
|
291
|
+
["Before", percent(step.from, 2)],
|
|
292
|
+
["After", percent(step.to, 2)],
|
|
293
|
+
["Change", points(step.to - step.from)]
|
|
294
|
+
]
|
|
295
|
+
}));
|
|
296
|
+
words.push(...steps.flatMap((step) => {
|
|
297
|
+
const end = Math.max(step.from, step.to) * 100, room = vertical ? frame.band.step() - 4 : plot.width - frame.value(end) - 5;
|
|
298
|
+
const text = (step.total ? [percent(step.to), percent(step.to, 0)] : [points(step.to - step.from), points(step.to - step.from).replace(" pts", "")]).find((option) => FrameworkText.width(option, SIZE) <= room);
|
|
299
|
+
if (!text) return [];
|
|
300
|
+
const [px, py] = frame.point(at(step.key) + thick / 2, end, vertical ? -9 : 5);
|
|
301
|
+
return [{
|
|
302
|
+
key: `step/${step.key}`,
|
|
303
|
+
text,
|
|
304
|
+
x: px,
|
|
305
|
+
y: py,
|
|
306
|
+
anchor: vertical ? "middle" : "start",
|
|
307
|
+
size: SIZE,
|
|
308
|
+
weight: step.total ? 600 : 400,
|
|
309
|
+
tone: "ink"
|
|
310
|
+
}];
|
|
311
|
+
}));
|
|
312
|
+
if (steps.length) words.push({
|
|
313
|
+
key: "followed",
|
|
314
|
+
text: FrameworkText.fit(`${holder}: ownership after each round`, plot.width, 11),
|
|
315
|
+
x: 0,
|
|
316
|
+
y: -11,
|
|
317
|
+
weight: 600,
|
|
318
|
+
tone: "ink"
|
|
319
|
+
});
|
|
320
|
+
StrategyWords.draw(ctx, "labels", words);
|
|
321
|
+
}
|
|
322
|
+
/** The single bar names each slice outside it: its share before a column and its holder after, or both under a bar. Slices too thin for a line of text go unnamed. */
|
|
323
|
+
static #named(slice, frame, start, thick, across, plot) {
|
|
324
|
+
const { stake } = slice, [px, py] = frame.point(start + thick, (slice.from + slice.to) / 2), share = percent(stake.share);
|
|
325
|
+
if (frame.vertical) {
|
|
326
|
+
const name = FrameworkText.fit(stake.holder, plot.width - px - 8, SIZE);
|
|
327
|
+
if (across < 13) return [];
|
|
328
|
+
return [...start - 8 >= FrameworkText.width(share, SIZE) ? [{
|
|
329
|
+
key: slice.key,
|
|
330
|
+
text: share,
|
|
331
|
+
x: start - 8,
|
|
332
|
+
y: py,
|
|
333
|
+
anchor: "end",
|
|
334
|
+
size: SIZE
|
|
335
|
+
}] : [], ...name.length >= 4 ? [{
|
|
336
|
+
key: `${slice.key}/name`,
|
|
337
|
+
text: name,
|
|
338
|
+
full: stake.holder,
|
|
339
|
+
x: px + 8,
|
|
340
|
+
y: py,
|
|
341
|
+
size: SIZE,
|
|
342
|
+
tone: "ink"
|
|
343
|
+
}] : []];
|
|
344
|
+
}
|
|
345
|
+
const name = FrameworkText.fit(stake.holder, across - 4, SIZE), named = name.length >= 4;
|
|
346
|
+
if (py + 28 > plot.height || FrameworkText.width(share, SIZE) > across - 4) return [];
|
|
347
|
+
return [...named ? [{
|
|
348
|
+
key: `${slice.key}/name`,
|
|
349
|
+
text: name,
|
|
350
|
+
full: stake.holder,
|
|
351
|
+
x: px,
|
|
352
|
+
y: py + 11,
|
|
353
|
+
anchor: "middle",
|
|
354
|
+
size: SIZE,
|
|
355
|
+
tone: "ink"
|
|
356
|
+
}] : [], {
|
|
357
|
+
key: slice.key,
|
|
358
|
+
text: share,
|
|
359
|
+
x: px,
|
|
360
|
+
y: py + (named ? 24 : 11),
|
|
361
|
+
anchor: "middle",
|
|
362
|
+
size: SIZE
|
|
363
|
+
}];
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
export { CapTableBasis, CapTableChart, CapTableView };
|