@@ -1,8 +1,139 @@
11import Graph from 'graphology';
2−import { clan, sein, patp2dec, patp } from 'urbit-ob';
32import * as colors from './colors';
43
4+const example = '[0[3216291:t;9219219;4608:s[156546;1612196;219621:~;321965149:4608;]1280[4289594624;4278191360;426672256;4260168960;4260824320;4246668544;4246144256;4253746432;4235396352;4230415616;4225697024;4227007744;4219667712;4222616832;4224648448;4214490368;4215997696;4219143424;4237296896;]]]'
5+
6+export interface ChartPoint {
7+ patp: string,
8+ dominion: ('l1' | 'l2' | 'spawn'),
9+ sponsor?: string,
10+ parent?: string,
11+ children?: ChartPoint[],
12+ sponsees?: ChartPoint[],
13+}
14+
15+function parseMetaString(metaString: string): ChartPoint {
16+ let idString, subMetaString = '';
17+ const colIndex = metaString.indexOf(':');
18+ if (colIndex === -1) {
19+ idString = metaString;
20+ } else {
21+ idString = metaString.slice(0, colIndex);
22+ subMetaString = metaString.slice(colIndex + 1);
23+ }
24+ const meta: ChartPoint = {
25+ patp: '~' + idString,
26+ dominion: 'l1',
27+ }
28+ if (subMetaString) {
29+ const firstChar = subMetaString[0];
30+ if (firstChar === 't') {
31+ meta.dominion = 'l2';
32+ subMetaString = subMetaString.slice(1);
33+ } else if (firstChar === 's') {
34+ meta.dominion = 'spawn';
35+ subMetaString = subMetaString.slice(1);
36+ }
37+ if (subMetaString) {
38+ if (subMetaString[0] === '~') {
39+ meta.sponsor = null;
40+ } else {
41+ let [sponsor, parent] = subMetaString.slice(1).split('!');
42+ meta.sponsor = '~' + sponsor;
43+ meta.parent = '~' + parent;
44+ }
45+ }
46+ }
47+ return meta;
48+}
49+
50+function extractMeta(data: string): [ChartPoint, string] {
51+ const index = data.search(/[[;]/);
52+ if (index === -1) {
53+ throw new Error(`No point meta present in ${data}`);
54+ }
55+ const metaString = data.slice(0, index);
56+ data = data.slice(index);
57+ return [parseMetaString(metaString), data];
58+}
59+
60+function extractChildren(data: string, sup: string = undefined): [ChartPoint[], ChartPoint[], string] {
61+ if (data[0] === ';') return [[], [], data.slice(1)];
62+ if (data[0] !== '[') throw new Error('Invalid children in data: ' + data.slice(0, 20));
63+
64+ let endFound = false;
65+ let counter = 0;
66+ data = data.slice(1);
67+ const children = [];
68+ const sponsees = [];
69+ while (!endFound && data.length && counter < 20000) {
70+ if (data[0] === ']') {
71+ endFound = true;
72+ data = data.slice(1);
73+ } else {
74+ let sub;
75+ [sub, data] = extractChartPoint(data);
76+ if (!sub.parent || sub.parent === sup)
77+ children.push(sub);
78+ if (sub.sponsor === undefined || sub.sponsor === sup)
79+ sponsees.push(sub);
80+ }
81+ counter++;
82+ }
83+ return [children, sponsees, data];
84+}
85+
86+function extractChartPoint(data: string): [ChartPoint, string] {
87+ let point, children, sponsees;
88+ [point, data] = extractMeta(data);
89+ [children, sponsees, data] = extractChildren(data, point.patp);
90+ point.children = children;
91+ point.sponsees = sponsees;
92+ return [point, data];
93+}
94+
95+function linkChartData(points: ChartPoint[], store: any): ChartPoint[] {
96+ if (!points) return points;
97+ return points.map(point => {
98+ if (!store[point.patp]) return point;
99+ if (!point.children?.length) {
100+ point.children = linkChartData(store[point.patp].children, store);
101+ }
102+ if (!point.sponsees?.length) {
103+ point.sponsees = linkChartData(store[point.patp].sponsees, store);
104+ }
105+ return point;
106+ });
107+}
108+
109+function _parseChartData(str: string): ChartPoint[] {
110+ if (!str) return [];
111+ let [baseChildren, _] = extractChildren(str);
112+ let store = {};
113+ baseChildren.forEach(child => {
114+ store[child.patp] = {
115+ children: child.children,
116+ sponsees: child.sponsees,
117+ };
118+ });
119+
120+ return linkChartData(store['~base'].children, store);
121+}
122+
123+export function parseChartData(data: string): ChartPoint[] {
124+ let start = Date.now();
125+ let parsed = _parseChartData(data);
126+ // console.log(`finished parsing chart data to js object in ${Date.now() - start} ms`);
127+ return parsed;
128+}
129+
5130const circle = 2 * Math.PI;
131+function chordLength(radius, radians) {
132+ return 2 * radius * Math.sin(radians / 2);
133+}
134+function distancePolar(r1, th1, r2, th2) {
135+ return Math.sqrt((r1 * r1) + (r2 * r2) - (2 * r1 * r2 * Math.cos(th2 - th1)));
136+}
6137export function arrangePointsInSpirals(
7138 numPoints,
8139 {
@@ -10,18 +141,22 @@ export function arrangePointsInSpirals(
10141 startX = 0, startY = 0,
11142 maxArms = 6, maxPerArm = 45,
12143 makeSingleArmCircle = false,
13− }): { coords: any[], numArms: number } {
144+ }): { coords: any[], numArms: number, minDistanceBetween: number } {
14145 const numSmallEnoughArms = Math.ceil(numPoints / maxPerArm);
15146 let eRootNumArms = Math.max(1, Math.floor(Math.pow(numPoints, 1 / Math.E)));
16147 const numArms = Math.max(numSmallEnoughArms, eRootNumArms);
17148 const singleArmCircle = makeSingleArmCircle && numArms === 1;
18149 const numPerArm = numPoints / numArms;
19− // todo refine theta range, and rename maxPerArm (misnomer)
20150 let armThetaRange = numPerArm * Math.min(Math.PI / 12, 0.25 * circle / numArms);
21151 if (singleArmCircle) {
22152 minDistance = maxDistance;
23− armThetaRange = 2 * Math.PI;
153+ armThetaRange = circle;
24154 }
155+ const distanceRange = maxDistance - minDistance;
156+ const pointWeight = 1 / numPerArm;
157+ let minDistanceBetween = distancePolar(minDistance, 0,
158+ minDistance + distanceRange * pointWeight, armThetaRange * pointWeight);
159+ if (numPoints <= 3) minDistanceBetween = maxDistance * 0.75;
25160 const coords = [];
26161 let lastArm = null;
27162 let indexInArm = null;
@@ -34,15 +169,11 @@ export function arrangePointsInSpirals(
34169 indexInArm = 0;
35170 }
36171 const interArmProgress = arm / numArms;
37− // (i - (numarms * i/numpoints) * (numpoints/numarms)) / numperarm
38− // const intraArmProgress = (i - arm * numPerArm) / numPerArm;
39172 const beginningOfNextArm = Math.floor(numPerArm * (arm + 1));
40173 const numInThisArm = beginningOfNextArm - (i - indexInArm);
41174 const intraArmProgress = indexInArm / numInThisArm;
42− // const intraArmProgress = (numArms * overallProgress) % 1;
43175 const theta = circle * interArmProgress + armThetaRange * intraArmProgress;
44− let baseLine = maxDistance - minDistance;
45− let line = (minDistance + baseLine * intraArmProgress);
176+ let line = (minDistance + distanceRange * intraArmProgress);
46177 let x = 0 + startX + line * Math.cos(theta);
47178 let y = 0 + startY + line * Math.sin(theta);
48179 coords.push({
@@ -54,17 +185,19 @@ export function arrangePointsInSpirals(
54185 return {
55186 coords,
56187 numArms,
188+ minDistanceBetween,
57189 };
58190}
59191
60−export
61−
62−function populateGraph(g: Graph, rawChartData: any[]) {
63− if (!(rawChartData instanceof Array)) return;
192+export function populateGraph(g: Graph, chartData: ChartPoint[], bySponsor: boolean, callback?): Graph {
193+ if (!(chartData instanceof Array)) return;
64194 if (!g) {
65195 g = new Graph();
196+ } else {
197+ g.clear();
66198 }
67199
200+ let subsKey = bySponsor ? 'sponsees' : 'children';
68201 let sponsorEdges = [];
69202
70203 let offsetX = 0;
@@ -74,34 +207,35 @@ function populateGraph(g: Graph, rawChartData: any[]) {
74207 let b = 9000000;
75208 let theta = 0;
76209
77− rawChartData.forEach((galaxy, gi) => {
210+ chartData.forEach((galaxy, gi) => {
78211 offsetX = 0 + (a + b * theta) * Math.cos(theta);
79212 offsetY = 0 + (a + b * theta) * Math.sin(theta);
80213
81214 theta = 0.15 * (gi+1) * (1 - .15 * (gi / 256));
82− const gPatp = '~' + galaxy.p;
215+ const gPatp = galaxy.patp;
216+ const itsYou = gPatp === '~' + window.ship;
83217 g.addNode(gPatp, {
84− // id: gPatp,
85218 label: gPatp,
86219 size: 3,
87− color: colors.WHITE,
88− // color: colors.'#fcd34d',
220+ color: itsYou ? colors.YOU : colors.WHITE,
89221 x: offsetX,
90222 y: offsetY,
91223 zIndex: 10,
92224 });
93225
94− const stars = galaxy.c || [];
226+ const stars = galaxy[subsKey] || [];
95227 const numStars = stars.length;
96228 const maxStarDistance = 60000 * 128;
229+ const minStarDistance = maxStarDistance / 4;
97230 const starSize = 1.2 / Math.pow(numStars, 1 / 7);
98231 const {
99232 coords: starCoords,
100233 numArms: numStarArms,
234+ minDistanceBetween: minDistanceBetweenStars,
101235 } = arrangePointsInSpirals(numStars,
102236 {
103237 maxDistance: maxStarDistance,
104− minDistance: maxStarDistance / 4,
238+ minDistance: minStarDistance,
105239 startX: offsetX,
106240 startY: offsetY,
107241 makeSingleArmCircle: true,
@@ -109,15 +243,19 @@ function populateGraph(g: Graph, rawChartData: any[]) {
109243 let lastStarArm = null;
110244 let lastStarPatp = null;
111245 stars.forEach((star, si) => {
112− const sPatp = '~' + star.p;
246+ const sPatp = star.patp;
113247
114248 const [starX, starY] = starCoords[si].coords;
115249 const arm = starCoords[si].arm;
250+
251+ const itsYou = sPatp === '~' + window.ship;
252+ if (itsYou) {
253+ g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
254+ }
116255 g.addNode(sPatp, {
117256 label: sPatp,
118257 size: starSize,
119− // color: colors.arm % 2 == 0 ? LGREY : DGREY,
120− color: colors.GOLDT2,
258+ color: itsYou ? colors.YOU : colors.GOLDT2,
121259 x: starX,
122260 y: starY,
123261 zIndex: 8,
@@ -137,24 +275,27 @@ function populateGraph(g: Graph, rawChartData: any[]) {
137275 }
138276 lastStarArm = arm;
139277 lastStarPatp = sPatp;
140− if (star.s) {
141− sponsorEdges.push([sPatp, '~' + star.s, {
278+ if (star.sponsor) {
279+ sponsorEdges.push([sPatp, star.sponsor, {
142280 color: colors.GREEN,
143281 size: 0.2,
144282 zIndex: 11,
145283 }]);
146284 }
147285
148− const planets = star.c || [];
286+ const planets = star[subsKey] || [];
149287 const numPlanets = planets.length;
150− const maxPlanetDistance = starSize * 1.6 * 4000 * 64;
288+ // const maxPlanetDistance = starSize * 1.6 * 4000 * 64;
289+ const maxPlanetDistance = minDistanceBetweenStars * 0.45;
290+ const minPlanetDistance = maxPlanetDistance / 4;
151291 const {
152292 coords: planetCoords,
153293 numArms: numPlanetArms,
294+ minDistanceBetween: minDistanceBetweenPlanets,
154295 } = arrangePointsInSpirals(numPlanets,
155296 {
156297 maxDistance: maxPlanetDistance,
157− minDistance: maxPlanetDistance / 4,
298+ minDistance: minPlanetDistance,
158299 startX: starX,
159300 startY: starY,
160301 maxArms: 30,
@@ -163,18 +304,20 @@ function populateGraph(g: Graph, rawChartData: any[]) {
163304 let lastPlanetArm = null;
164305 let lastPlanetPatp = null;
165306 planets.forEach((planet, pi) => {
166− const pPatp = '~' + planet.p;
167− // const circle = 2 * Math.PI;
168− // let line = 16000 * 64;
169− // let planetX = 0 + starX + Math.cos(circle * pi / numPlanets) * line;
170− // let planetY = 0 + starY + Math.sin(circle * pi / numPlanets) * line;
171−
307+ const pPatp = planet.patp;
172308 const [planetX, planetY] = planetCoords[pi].coords;
173309 const arm = planetCoords[pi].arm;
310+ const planetSize = Math.sqrt(Math.min(minPlanetDistance, minDistanceBetweenPlanets)) / 1300;
311+ const planetEdgeSize = planetSize / 3;
312+ const itsYou = pPatp === '~' + window.ship;
313+ if (itsYou) {
314+ g.setNodeAttribute(sPatp, 'color', colors.YOUR_STAR);
315+ g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
316+ }
174317 g.addNode(pPatp, {
175318 label: pPatp,
176− size: .15,
177− color: planet.d == 'l2' ? colors.GREEN : colors.BLUE,
319+ size: planetSize,
320+ color: itsYou ? colors.YOU : (planet.dominion == 'l2' ? colors.GREEN : colors.BLUE),
178321 x: planetX,
179322 y: planetY,
180323 zIndex: 6,
@@ -182,22 +325,22 @@ function populateGraph(g: Graph, rawChartData: any[]) {
182325 if (lastPlanetArm == arm) {
183326 g.addEdge(pPatp, lastPlanetPatp, {
184327 color: colors.DGREY,
185− size: 0.05,
328+ size: planetEdgeSize,
186329 zIndex: 5,
187330 });
188331 } else {
189332 if (numPlanetArms == 1) {
190333 g.addEdge(pPatp, sPatp, {
191334 color: colors.DGREY,
192− size: 0.05,
335+ size: planetEdgeSize,
193336 zIndex: 5,
194337 });
195338 }
196339 }
197− if (planet.s) {
198− sponsorEdges.push([pPatp, '~' + planet.s, {
340+ if (planet.sponsor) {
341+ sponsorEdges.push([pPatp, planet.sponsor, {
199342 color: colors.GREEN,
200− size: 0.05,
343+ size: planetEdgeSize,
201344 zIndex: 11,
202345 }]);
203346 }
@@ -209,5 +352,8 @@ function populateGraph(g: Graph, rawChartData: any[]) {
209352 sponsorEdges.forEach(([point, sponsor, attrs]) => {
210353 // g.addEdge(point, sponsor, attrs);
211354 });
355+ if (callback) {
356+ setTimeout(callback, 0);
357+ }
212358 return g;
213359}