@@ -1,8 +1,139 @@
1import Graph from 'graphology';
1import Graph from 'graphology';
2−import { clan, sein, patp2dec, patp } from 'urbit-ob';
3import * as colors from './colors';
2import * as colors from './colors';
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;]]]'
6+export interface ChartPoint {
8+ dominion: ('l1' | 'l2' | 'spawn'),
11+ children?: ChartPoint[],
12+ sponsees?: ChartPoint[],
15+function parseMetaString(metaString: string): ChartPoint {
16+ let idString, subMetaString = '';
17+ const colIndex = metaString.indexOf(':');
18+ if (colIndex === -1) {
19+ idString = metaString;
21+ idString = metaString.slice(0, colIndex);
22+ subMetaString = metaString.slice(colIndex + 1);
24+ const meta: ChartPoint = {
25+ patp: '~' + idString,
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);
38+ if (subMetaString[0] === '~') {
41+ let [sponsor, parent] = subMetaString.slice(1).split('!');
42+ meta.sponsor = '~' + sponsor;
43+ meta.parent = '~' + parent;
50+function extractMeta(data: string): [ChartPoint, string] {
51+ const index = data.search(/[[;]/);
53+ throw new Error(`No point meta present in ${data}`);
55+ const metaString = data.slice(0, index);
56+ data = data.slice(index);
57+ return [parseMetaString(metaString), data];
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));
64+ let endFound = false;
66+ data = data.slice(1);
69+ while (!endFound && data.length && counter < 20000) {
70+ if (data[0] === ']') {
72+ data = data.slice(1);
75+ [sub, data] = extractChartPoint(data);
76+ if (!sub.parent || sub.parent === sup)
78+ if (sub.sponsor === undefined || sub.sponsor === sup)
83+ return [children, sponsees, data];
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];
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);
102+ if (!point.sponsees?.length) {
103+ point.sponsees = linkChartData(store[point.patp].sponsees, store);
109+function _parseChartData(str: string): ChartPoint[] {
110+ if (!str) return [];
111+ let [baseChildren, _] = extractChildren(str);
113+ baseChildren.forEach(child => {
114+ store[child.patp] = {
115+ children: child.children,
116+ sponsees: child.sponsees,
120+ return linkChartData(store['~base'].children, store);
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`);
5const circle = 2 * Math.PI;
130const circle = 2 * Math.PI;
131+function chordLength(radius, radians) {
132+ return 2 * radius * Math.sin(radians / 2);
134+function distancePolar(r1, th1, r2, th2) {
135+ return Math.sqrt((r1 * r1) + (r2 * r2) - (2 * r1 * r2 * Math.cos(th2 - th1)));
6export function arrangePointsInSpirals(
137export function arrangePointsInSpirals(
7 numPoints,
138 numPoints,
@@ -10,18 +141,22 @@ export function arrangePointsInSpirals(
10 startX = 0, startY = 0,
141 startX = 0, startY = 0,
11 maxArms = 6, maxPerArm = 45,
142 maxArms = 6, maxPerArm = 45,
12 makeSingleArmCircle = false,
143 makeSingleArmCircle = false,
13− }): { coords: any[], numArms: number } {
144+ }): { coords: any[], numArms: number, minDistanceBetween: number } {
14 const numSmallEnoughArms = Math.ceil(numPoints / maxPerArm);
145 const numSmallEnoughArms = Math.ceil(numPoints / maxPerArm);
15 let eRootNumArms = Math.max(1, Math.floor(Math.pow(numPoints, 1 / Math.E)));
146 let eRootNumArms = Math.max(1, Math.floor(Math.pow(numPoints, 1 / Math.E)));
16 const numArms = Math.max(numSmallEnoughArms, eRootNumArms);
147 const numArms = Math.max(numSmallEnoughArms, eRootNumArms);
17 const singleArmCircle = makeSingleArmCircle && numArms === 1;
148 const singleArmCircle = makeSingleArmCircle && numArms === 1;
18 const numPerArm = numPoints / numArms;
149 const numPerArm = numPoints / numArms;
19− // todo refine theta range, and rename maxPerArm (misnomer)
20 let armThetaRange = numPerArm * Math.min(Math.PI / 12, 0.25 * circle / numArms);
150 let armThetaRange = numPerArm * Math.min(Math.PI / 12, 0.25 * circle / numArms);
21 if (singleArmCircle) {
151 if (singleArmCircle) {
22 minDistance = maxDistance;
152 minDistance = maxDistance;
23− armThetaRange = 2 * Math.PI;
153+ armThetaRange = circle;
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;
25 const coords = [];
160 const coords = [];
26 let lastArm = null;
161 let lastArm = null;
27 let indexInArm = null;
162 let indexInArm = null;
@@ -34,15 +169,11 @@ export function arrangePointsInSpirals(
34 indexInArm = 0;
169 indexInArm = 0;
36 const interArmProgress = arm / numArms;
171 const interArmProgress = arm / numArms;
37− // (i - (numarms * i/numpoints) * (numpoints/numarms)) / numperarm
38− // const intraArmProgress = (i - arm * numPerArm) / numPerArm;
39 const beginningOfNextArm = Math.floor(numPerArm * (arm + 1));
172 const beginningOfNextArm = Math.floor(numPerArm * (arm + 1));
40 const numInThisArm = beginningOfNextArm - (i - indexInArm);
173 const numInThisArm = beginningOfNextArm - (i - indexInArm);
41 const intraArmProgress = indexInArm / numInThisArm;
174 const intraArmProgress = indexInArm / numInThisArm;
42− // const intraArmProgress = (numArms * overallProgress) % 1;
43 const theta = circle * interArmProgress + armThetaRange * intraArmProgress;
175 const theta = circle * interArmProgress + armThetaRange * intraArmProgress;
44− let baseLine = maxDistance - minDistance;
45− let line = (minDistance + baseLine * intraArmProgress);
176+ let line = (minDistance + distanceRange * intraArmProgress);
46 let x = 0 + startX + line * Math.cos(theta);
177 let x = 0 + startX + line * Math.cos(theta);
47 let y = 0 + startY + line * Math.sin(theta);
178 let y = 0 + startY + line * Math.sin(theta);
48 coords.push({
179 coords.push({
@@ -54,17 +185,19 @@ export function arrangePointsInSpirals(
62−function populateGraph(g: Graph, rawChartData: any[]) {
192+export function populateGraph(g: Graph, chartData: ChartPoint[], bySponsor: boolean, callback?): Graph {
63− if (!(rawChartData instanceof Array)) return;
193+ if (!(chartData instanceof Array)) return;
64 if (!g) {
194 if (!g) {
65 g = new Graph();
195 g = new Graph();
200+ let subsKey = bySponsor ? 'sponsees' : 'children';
68 let sponsorEdges = [];
201 let sponsorEdges = [];
70 let offsetX = 0;
203 let offsetX = 0;
@@ -74,34 +207,35 @@ function populateGraph(g: Graph, rawChartData: any[]) {
74 let b = 9000000;
207 let b = 9000000;
75 let theta = 0;
208 let theta = 0;
77− rawChartData.forEach((galaxy, gi) => {
210+ chartData.forEach((galaxy, gi) => {
78 offsetX = 0 + (a + b * theta) * Math.cos(theta);
211 offsetX = 0 + (a + b * theta) * Math.cos(theta);
79 offsetY = 0 + (a + b * theta) * Math.sin(theta);
212 offsetY = 0 + (a + b * theta) * Math.sin(theta);
81 theta = 0.15 * (gi+1) * (1 - .15 * (gi / 256));
214 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;
83 g.addNode(gPatp, {
217 g.addNode(gPatp, {
85 label: gPatp,
218 label: gPatp,
87− color: colors.WHITE,
220+ color: itsYou ? colors.YOU : colors.WHITE,
88− // color: colors.'#fcd34d',
89 x: offsetX,
221 x: offsetX,
90 y: offsetY,
222 y: offsetY,
91 zIndex: 10,
223 zIndex: 10,
94− const stars = galaxy.c || [];
226+ const stars = galaxy[subsKey] || [];
95 const numStars = stars.length;
227 const numStars = stars.length;
96 const maxStarDistance = 60000 * 128;
228 const maxStarDistance = 60000 * 128;
229+ const minStarDistance = maxStarDistance / 4;
97 const starSize = 1.2 / Math.pow(numStars, 1 / 7);
230 const starSize = 1.2 / Math.pow(numStars, 1 / 7);
99 coords: starCoords,
232 coords: starCoords,
100 numArms: numStarArms,
233 numArms: numStarArms,
234+ minDistanceBetween: minDistanceBetweenStars,
101 } = arrangePointsInSpirals(numStars,
235 } = arrangePointsInSpirals(numStars,
103 maxDistance: maxStarDistance,
237 maxDistance: maxStarDistance,
104− minDistance: maxStarDistance / 4,
238+ minDistance: minStarDistance,
105 startX: offsetX,
239 startX: offsetX,
106 startY: offsetY,
240 startY: offsetY,
107 makeSingleArmCircle: true,
241 makeSingleArmCircle: true,
@@ -109,15 +243,19 @@ function populateGraph(g: Graph, rawChartData: any[]) {
109 let lastStarArm = null;
243 let lastStarArm = null;
110 let lastStarPatp = null;
244 let lastStarPatp = null;
111 stars.forEach((star, si) => {
245 stars.forEach((star, si) => {
112− const sPatp = '~' + star.p;
246+ const sPatp = star.patp;
114 const [starX, starY] = starCoords[si].coords;
248 const [starX, starY] = starCoords[si].coords;
115 const arm = starCoords[si].arm;
249 const arm = starCoords[si].arm;
251+ const itsYou = sPatp === '~' + window.ship;
253+ g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
116 g.addNode(sPatp, {
255 g.addNode(sPatp, {
117 label: sPatp,
256 label: sPatp,
118 size: starSize,
257 size: starSize,
119− // color: colors.arm % 2 == 0 ? LGREY : DGREY,
120− color: colors.GOLDT2,
258+ color: itsYou ? colors.YOU : colors.GOLDT2,
121 x: starX,
259 x: starX,
122 y: starY,
260 y: starY,
123 zIndex: 8,
261 zIndex: 8,
@@ -137,24 +275,27 @@ function populateGraph(g: Graph, rawChartData: any[]) {
138 lastStarArm = arm;
276 lastStarArm = arm;
139 lastStarPatp = sPatp;
277 lastStarPatp = sPatp;
140− if (star.s) {
278+ if (star.sponsor) {
141− sponsorEdges.push([sPatp, '~' + star.s, {
279+ sponsorEdges.push([sPatp, star.sponsor, {
142 color: colors.GREEN,
280 color: colors.GREEN,
143 size: 0.2,
281 size: 0.2,
144 zIndex: 11,
282 zIndex: 11,
148− const planets = star.c || [];
286+ const planets = star[subsKey] || [];
149 const numPlanets = planets.length;
287 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;
152 coords: planetCoords,
292 coords: planetCoords,
153 numArms: numPlanetArms,
293 numArms: numPlanetArms,
294+ minDistanceBetween: minDistanceBetweenPlanets,
154 } = arrangePointsInSpirals(numPlanets,
295 } = arrangePointsInSpirals(numPlanets,
156 maxDistance: maxPlanetDistance,
297 maxDistance: maxPlanetDistance,
157− minDistance: maxPlanetDistance / 4,
298+ minDistance: minPlanetDistance,
158 startX: starX,
299 startX: starX,
159 startY: starY,
300 startY: starY,
160 maxArms: 30,
301 maxArms: 30,
@@ -163,18 +304,20 @@ function populateGraph(g: Graph, rawChartData: any[]) {
163 let lastPlanetArm = null;
304 let lastPlanetArm = null;
164 let lastPlanetPatp = null;
305 let lastPlanetPatp = null;
165 planets.forEach((planet, pi) => {
306 planets.forEach((planet, pi) => {
166− const pPatp = '~' + planet.p;
307+ const pPatp = planet.patp;
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;
172 const [planetX, planetY] = planetCoords[pi].coords;
308 const [planetX, planetY] = planetCoords[pi].coords;
173 const arm = planetCoords[pi].arm;
309 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;
314+ g.setNodeAttribute(sPatp, 'color', colors.YOUR_STAR);
315+ g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
174 g.addNode(pPatp, {
317 g.addNode(pPatp, {
175 label: pPatp,
318 label: pPatp,
176− size: .15,
319+ size: planetSize,
177− color: planet.d == 'l2' ? colors.GREEN : colors.BLUE,
320+ color: itsYou ? colors.YOU : (planet.dominion == 'l2' ? colors.GREEN : colors.BLUE),
178 x: planetX,
321 x: planetX,
179 y: planetY,
322 y: planetY,
180 zIndex: 6,
323 zIndex: 6,
@@ -182,22 +325,22 @@ function populateGraph(g: Graph, rawChartData: any[]) {
182 if (lastPlanetArm == arm) {
325 if (lastPlanetArm == arm) {
183 g.addEdge(pPatp, lastPlanetPatp, {
326 g.addEdge(pPatp, lastPlanetPatp, {
184 color: colors.DGREY,
327 color: colors.DGREY,
185− size: 0.05,
328+ size: planetEdgeSize,
186 zIndex: 5,
329 zIndex: 5,
189 if (numPlanetArms == 1) {
332 if (numPlanetArms == 1) {
190 g.addEdge(pPatp, sPatp, {
333 g.addEdge(pPatp, sPatp, {
191 color: colors.DGREY,
334 color: colors.DGREY,
192− size: 0.05,
335+ size: planetEdgeSize,
193 zIndex: 5,
336 zIndex: 5,
197− if (planet.s) {
340+ if (planet.sponsor) {
198− sponsorEdges.push([pPatp, '~' + planet.s, {
341+ sponsorEdges.push([pPatp, planet.sponsor, {
199 color: colors.GREEN,
342 color: colors.GREEN,
200− size: 0.05,
343+ size: planetEdgeSize,
201 zIndex: 11,
344 zIndex: 11,
@@ -209,5 +352,8 @@ function populateGraph(g: Graph, rawChartData: any[]) {
209 sponsorEdges.forEach(([point, sponsor, attrs]) => {
352 sponsorEdges.forEach(([point, sponsor, attrs]) => {
210 // g.addEdge(point, sponsor, attrs);
353 // g.addEdge(point, sponsor, attrs);
356+ setTimeout(callback, 0);
212 return g;
358 return g;