Relationship Chart

This commit is contained in:
Nick Beckley 2026-06-04 19:53:37 +01:00
parent 1410666a39
commit dd2917693a
23 changed files with 411 additions and 231 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUXW/lJhD9KxavNQ5gwMZvaZJVr9J8aO9VW/VtgHFC4+ArjJNdRfnvlX2TzceqUl8sa4A5Z86cmScS4R5JR473+7MHjHkiJclhDQkmNGWasnoneMdNx1TVGN0Kpv4mJQnn+J10pJHOcWeQ1qiRSs8UtXXvqPdoOOOeCSWXnHAzke6JQKg8PgSH1Tj9gWkKYyQd+TNEPz4u2BAqN4yzr9I44CZOGaJbyFxuTs7p78fXu6vrw60Jp+VxFTzpCOuV4FIxKnrRU8kaQ61qkVqjnG0VMtDm8CzEjCnCUE3+7g3fjzlidp2oRF0xKgx5LomHDAtlCxPuvu8XFqtEp0u8XMPrb/dEHjCRTpSvYh7SHbkhHN1Pdg6DP1q/pCT7NO4x5YCrGlfb4o0EZxWrhBaMkZJcgLsNEYvNaXE1LCWCa5lEX1ODvafScKBG1jX1tYJG6L5pRE9Kco4p4vAu60VwaZzGPhcvIhcfcLbHJ2cR7IALxhcYJiQl2UzbDNHDMEY8+4ZuzodcuzQvxydjzCHO4zwVm5jxJsHL+ev7kzkljLm4hnxb/AbTLemIgdZxMGAAtHM9KoY98ww0azxqr9FL4RsvjfRaGua0BMulB8Vtv0RISX5dNNzOzuE0vbFZTTIMK4fipU8nQ8CYP8i4uISj8HWLtBdcU9l6oKBqQ7WudY/cmUbwV5gdpBvMpCN9a61lzlkhDXcgDNS+Z7XXvRGiaUD2tWqk0FqxBpxvGsGlrxt0UPc1awUpyXUa/ezy51bXawOu0/gPurxIRTqimVKtA2uFlrwF06LkWrbaYGNkD0zWzgJXLVjLGSjmTCsccG1AWq/6HyqdxZsQ8TRM+wG+v8NtK13VvziAlvcgP13/kuAeH8d0d3mwcXV5tiMl+TrHZSMUm8UijyHSb3qZ6It5yGF3mxA8+ovR488+wmVcinVCaxRQO2yp1FxQ2WigtteK+rZ1tgEvlG1ISU5Hd4epWBwGIS5T9SPZ1bY4Tu42ZHR5Tgu/v1YeV9vieoDcj+n+wyZ5V9hnASpRc7Z0xuPDYRutHLX3LXrNqZBcUWkMp61RDbVNa43VLSq3KHY15/2ci6/oQ0KX14pfvLhintyiu/tZjFcXvFiUs8Vr28MWW6X9X0vsuST3CNOc8H7d1t0T2cSIaUU+nQ+juIkXYRjChG6MfiKdMVpUtVAvBP/rGmdMqUpJ2Tw/Pz//CwAA//8DAIu0/5cbBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZbom7GeBcZOFkPEiNZ7I1itzJEZGpAUU4CY/59Qc0EiXPLjSiyuqurmi8kuickPbk9nf56xpgXUpMcNkgwYSgzlMlHwXtue6abjrFWm/Y/UpNwj99JT1rlPfcWqUSDVAHTdJCjpwBoOePAhFalpvu8kP6FuNAAPgePzbx8xLSEOZKefAoR5q+ltwuNn+YVmjRPuI9LdtEXMf/sd/f07e3h8eFwebXgUshNANITNmrBlWZUjGKkirWWDrpDOljth04jc8ZeaCFmTNFNzQJffvaHOUfMvheNkA2jwpJzTcBlVyQPbsHH76eiYrPoruD1Bm/H/oU8YyK9qH+YeSl346dw4+enJxfhZgwxLEdSk1OaT5hywM2ObcJpcjnMsbo22U0BYyY1efhQHSaXxzk9vfLow2X0/R9M/s75Y4hY7e8Kh6MA2SEdBTdUdeCo09JSY6QZkXvbCk5qsptjDnGd16Xax4yf06aS9ORvNy140Xeb/DFk9HlNRfq/pmSN30LezVAARmpySDOsPlc/7easYY1k5fJhzac1V+8RQkKfscz0mNZr+d8owoiNtFtTwpirg8vH6o1bjqQn1nWeO+usc8b7ETXDkQFzhrWABgyCEtCCsgqMsswb5QauwGk+jAUpuku61bZQoN0oPCiKrpNUDdzRzrSGcimUkGi1kfKVrdXDVHjOd0whSGpxBKosd9QqKSlI7VphxrYV4y+WXBPnrPgN+Hz5GpsCA9AhGE6F4poqazntrG7p0HaDHUyH2hfN95giTr8Y9S74NC/zmKvrwlSvrHu/xvK9q215voZIv22R3c3+C6aqRO5CLNt8Tfl8Pp//BwAA//8DAMbyj+ElBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2q3SNxbb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFflayU6ITthFBBGnDX6xm4yN9Yx3zJkYZA3FNjrhBYXmvh8gRKUghUShrSk/4vLLulcHYIJ3HSM2y/k5pHZeZdeyPccblazkbxiZOy4ZNWiZ6mNcMcyxmfnu4e+S/3h6ePx4uf620FnEzIutYaD2FARTXPiA3AnoOg+h5T7aVUmrdGnuRjXOmNMPUrPjl+/m45Jly7FSjdCO4CuytZggZiuUeVnr+dioudkT3pV7v5f2ze2VnSqxT9b8wL+1u4jTerFs/0ZmmE6SV0k1cjkeYkdXslJYTpTzSjuVMqV/WMReiMXpoWymxBW+sx1ZbpT1qQdgKbzFgAGfcoKK1A/Zt8GYwqKRBR8b0ntVsBzdNkMdlrq7e76aR5sxq9vGpOkyQhyUd36F/uhB9+B9Ai23WMS2sdt6T0VpDcL0ShN5F51QfQfihVyQjCRuMD9ILqYMSQVvnW48gQmQ1+wDxZZyperhnHROSFOqW+KCk46ZF4GB14M5pN5CMwSvJana3zHmct2Vbq4c50+e035d17GeYVrrc9DbFlzFTzFsqEP50JYyHtOAWc/U9AFI0otFCFNGWT1uuPhGOiWKmguM5bdd+/5Eop3bR3ZYSzbk6QH6pfoH1pTCENkoIEABcjANZQYNAAU54JIeO0Cj0aIJBZ4KIzkAvDYKV/VAqrGZU8lbtEY+9i1b2yBVZww2R56GA0Tq6aJUhB+85Vh+nooPYCkOoeaABuQkSeDBac9QWvHKD92r4Ack1LFIUwEjny7LuDhxiS+gkV0ZabkKQvA3W8963fehdSzYWz4+UZpp+APVhjGlZlyFX16xV79B92uby4FR77r6OM/97n9H9Er9QqsqMYZzLfl3H+lazI8G6JTru71X3yg5luarn/dlSoXFWqJo9ZUh5O13Lum2UMPLt7e3tHwAAAP//AwDz0opy9AQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsygPP0SKuhl2i7pu6kW8SIveRuTIFqKVFhS1aWDsfw+o3SBxb70JA70zw2femTc24YFYy26Px19ONKWFlSwNW0iCNBwMB7WXopXQQl1BrYQA/Q8r2fBIX1nLrPZeeEdckSGuA9S8U73nIZATIALIWuec+LKw9o3hUAU6DZ6qeflEcRnmibXsr2EK85dcG4fKj/MaqjiP9DAtCSefm/nz4e6R/3G72z/tLn8ttGRxNQTWMtdYcj1KrqwLXAN2HHvoeEd1I4RQqtH1RTZMieKEY7WEzz/qhzlNlHwrK6kq4NKxc8kCJswtd7jQ/usxd7Ehus/xcgtvn+0bO1FkrSy/w7yku/HjcJPm40gnGo8YF4o3fj4ccAqsZMc4HymmgTYs20vHEdMwT8W12N040JRYyZ6ei92IqZ/j4R2r5wuCh/9B4ESxYy1TUCtjLWmlFDrTSaBgjTdGdh7B9p0k4Qlqp60TFoRyEpyqjW1sQHCelexlnDscf182fk5C1wSw0kjS5KQ2xnkg1VBfW4KAxnSm8S6oxkrdhEahQxSuI+ud8dkgH9C/DhMVD/esZSBIBtUQ76UwXDcBOdbKcWOU6Ul4Z6VgJbubpzRM67wuxcOU6CVu/FjLfsVxoQu52+hfh0Q+rTFD/dvkYrs4h9Wn4ocDBFRQKYAsWtNxTcVHCkMknyjj3cf1mu8/EmnkJrpbY6QpFTtMr8VvuLxmKth4gS4/1XjfUw3UQwA0YAOZYChoGWzQTgejHXijsRM6YC26PkdYySgbrtg8Do6sc0FzKWrHtfLAO+uBKxDBGiUNWvOOY/E0Zh36BjQFxR31gWsnkDutFA+qRitNb63sf0JyNZ+ADDjQ6bKtWwcmhIaCEVxqUXPtnOCNqy3vbNO5zjRU+9zzI8WJxp9AfRh8nJe5T8XVu8U7dB/XKV+cYvPxl2Hi/24zup/9Z4pFnjEOU16w61jPJTsQLmukw3aw2je2y9tV7Le7JV1lapAle04Y03q8hlVTSdDifD6fvwEAAP//AwCWgzK09QQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUy27jNhSGX0XgWlR4EyVqlzod1MhkYkzcadEdRR45RGTSoKhMg8DvPqDsIM4MuuhOOOJ/Lt/5yVfk9R5Qh64Ph9+fwacJlSi5JcQIk5hITPiW0Y6RjtQVZZyxRv2DSuRu4QV1qBHGUKMAc5CAhSU17vlgsLWgKKGWsFrknHo3oe4VaVdZeHYGqjB9gzi54FGH/nLehu+5tnaVGcNsqxhGWPspaW9yM1/Wq1v8+Xqzvd+cTk0wZXHlLOqQahtQg2aYN8piQXSP9UB63EPdUko5b0V9kjmfIHo9VpN9eq9vQ/KQTMcqxiuCmULHElmddG651xNsXw65iwXRTY6XS3j57F7RM0TUsfIN5indlRnd1X7qZzfaqzHsds7vVsEPbjdHnXLhEh1iOEBMDhY4vzmv48vnsNvlfOiTHidAJdpC3Duvx9OPGxj0PKaHMMeFzGSf/usM6pCeU0AlWkiO41K2OA+zGh349Iv2ovD9Q7EZdRpC3H9Y0sOJ/fp/oL/T5tF5KNY3qEOEArO8BTwwKrForca65gpLyeUA1KiGUVSiVfDJ+TnMU7H2CXZnah/6u47m0SUwaY55pr9lNtsmBjubVLwvmJKKVJyQLJrTYU7FV7AugkmQh9jG+ZzvJwmTbBF9ciP8gmc1xwg+FRudHos/9PSYcejWUK200loaM0BNYCCWaEkaC9JKsILZxgolrBSKGCl0T4XVNe2HHEElgmyyYvE1N40mQ8+xNUJiATXF7cAs1pTUDeNWkkF8gFvcj1mnTUsEWI4VDBYLRTVWgnNsea0bJoemYcMFp7MhKMnUL0345wT27KUvJ2u/zW7h+XSPlz6ltS1YSTETtMZCKYpbVTe4b9pe9bKF2uTJVsFP4Q3kN4h9mFzKj8id826vx5+PvG/mFqKH8WI7d87EMIUhFWdbFh/29XX2+RUrFot+dx7/uxjjJpgniEU2lnb+YpnH4/H4AwAA//8DAIOrApAMBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJKFCUa2IPhtFgjTWMk7u5ibxQ5jInIpEFRToMi/31BWa6dooe9CSPO43vM/EBO7gEt0PJw+HQEFweUo2inUElKjgnHpNqWdFGSBamLmnAi6vZflCN7B29ogRqmFFUCcAUcMNOkxl1lFNYaBCVUk7JmqaZ8HtDiB5K20HC0Cgo//AVhsN6hBfrbOu1fU29pC9X7URfB97B2Q5ROpWG+rld3+Mtys33YnF4NMKTkwmq0QKJtQBhZ4qoRGjMiOywN6XAHdUspraqW1ac06yIEJ/ti0C+X/tpHB1EtyqKsCoJLgd5zpGWUaeRODrB9O6QpJopuUzyfwtPn4gc6QkCLMj+TeSp3o3p7sx+60fb6JsrwDNEEuYdXH17gKHuUo0PwBwjRwsTNw1N2GYmSghQlLwlBObqXamcdZOvb7KFPgKVqCQNdYQFGYyaoxIJVFdZVLZuSm6YpDcrRHQQH/VXVe6uCH7yJ2Ux59qHPyrto3ejHIVu7CM9BxlPeZ9kPgHK0GbveDrtHkPpt6x/H9M+NfcKyGkMAF7ONjLvsTznski6yVVQKKaTkShmoCRiiieSk0cA1B81K3WgmmOZMEMWZ7CjTsqadSRGUo4cxHsZ45v/71RDbYPd70JcJthPHm15G48N+rcFFa2yS5vxiMlTfT6iyueaqt+DiB5LRAhEKpa5awKakHLNWSyzrSmDOK26AKtGU9ESYsc/jT56IMZpVpm5b3omOEihN0zLSmI5oZSoldN1BUzakaaEVipCuZrTthCIaaM10Qhe8HlX81QrVJNDj6NJ2ZusE+9U6/J2zCyOb4KNXvk+C2R6e3oYI+9+ptvL7gx9sTPjNLO23Ae69S3/n/Z85m5OXPl5ikPYgm1aPgeKmIhxL2jLMaNPiFuoKN3o6FUoZrVCObr16gZAlg0nrJk3Onnp4ypZB7WwEFceQOv8zgTrJ+fm8Mhc+iq+ftisfYHk45HP0j2Mi6VTsbIAPd+XbkJ7v/BCvIM/QnqA358Gu7KTheDpVNgW51i1oTnHJaI2ZEBS3om5w17Sd6HgLtbrYNXsEbQOoONXbhjF1m5n9nS+XIVojVRzS8nzxarLT7M/5ya9DXsn2M/vU/Eq62UpzJUqSZZ9Oh3Ny0P+6m+/v7+//AQAA//8DAKXy/XEoBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/UMBD9K5HPceqvOHFu1RbEqkBXZQWIm+OZUKups3Kcfqja/46cXVTgxsWyRn4z7715fiXBPiDpyOXh8O4RQ5pJSZJfS4IJTZmmTO4F7wTrmK44Z7JuxQ9SEn+NL6QjjXKOO4NUokaqgNW0l4OjAGg448BErXJP+3Mm3SuxvgJ89A6raf6KcfZTIB355gNMT3m29ZUbpwWqOI24DXOywWUyn7eba/rxcre/2Z1ezThncOWBdMS0DZrBCiobA1Qx21M7sJ72WLeccylbVZ9gPiSMwY7VDPdv82FKAZPrRCVkxagw5FgSsMlmyr2dcf9yyCxWi65yvVzL67V7JY8YSSfK32ae2l240V88zP3iR7g4ncnO91nlIU4HjMnj6skqcxxt8lMozpM2o8eQSEluvhS70aZhig9/GfXlpH/7H/L3efq7Z3RLQthMS0ikI1y2pCSfrLvzAYvtFekI4yhAtkgHwTVVLVhqa2mo1lIPyJ1pBCcl2Uwh+bBMy1xsQ8KfcRVAOvLejjOeqF9Gd+cTurTErOq7zlnYxQkWl4o3/zmrWCUZy6AlHZZU3CL4iC5h1rePy7nfPxChxQraLDFiSMXOprvig53vsim2ddwaa6zVzg1YMxwYMKtZA6hBIygBDSijQCvDnFa25wpszfshV0hJMK+7WBPGsHdON0AbxoAqaQZqmByo7pu2bxuhB5ND8eZjcTNmnHUtUwiSGhyAKsMtNUpKCrK2GdU0YvjDkvP2OcsGAz6e/srKQAO0CJpToXhNlTGctqZuaCZget1i7TLna4wBxz+M+uRdnOZpSMU5PMVf1t0uIf/3Yg3Skw/0ed3R1eTuMRZ5x9aHHO/zWo/H4/EXAAAA//8DAFmhW4E2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2JEe2aplaUJSTwPC/F9TauaJAXwRhSM45c+bMPJEID0h6cnY8Xj5izAupSR63kGBCU6YpkwfBe8F6phvOhTKi/ZvUZHyHn0lPOuU99xapRI1UBdZSJwdPQ0DLGQ9MtKrkhNuF9E8Exibg4+ixmZc/MC3jHElP/hxjmD8WbBgbP81raNI84S4uGaIvZN7vzt/R389uDtc3p1sLLuVxMwbSE2s6tAMIKjsbqGLgKAzMUYet4ZxLaVR7ejbGjCnC1Czh/it+mHPE7HvRCNkwKix5rkmADIWygwUPn4+FxSbRRYnXW3j77Z/IIybSi/pVzFO6N34a3zwsbh2n8Gb7kpoc03zElEfc1LjeV19JcNawRmjBGKnJFfi7MWK1u6iup1IieMMUBkktDoEqy4FaJSUNsoVO6KHrxEBq8g5TxOmbrFejT/MyD7l6Ebn6Dmd/dn4ZwU1YMN7CtCCpyW7ZZ4gBpjni5Sf0az7lOqS1HJ/PMY9xndel2sWMtwlezl/fn68pYczVDeS76jdY7kqLwHgOFiyA9n7AluHAAgPNuoA6aAxKhC4oq4JWlnmtwHEVoOVuKBFSk1+LhvvVe1yWr2w2k0zTxqF66dP5NGLM38lIesI4iiAN0kFwTZUJQKGVlmot9YDc207wV5gDpFvMpCeDcc4x751QlnsQFmQYmAx6sEJ0HahBtp0SWresAx+6TnAVZIce5CCZEaQmN2kOq88/tlpuDbhJ8z/oc5GK9ESztjUenBNacQPWoOJaGW2xs2oApqR3wFsDznEGLfPWCA9cW1AutMMXlS7j7RjxYlyOE3z+Btc0upG/eADDB1A/XH+b4AE/zun+/cnGzfvLA6nJhzWWjVDtikU+jpF+0mWir9Ypj4e7hBAwXM0Bf/YRlnGptgl1VvrWcKQOOkeVFS0tzKnyQoJ2ofO8SHUx+3tMVXEYjLFM1Zdk1/vqLPm7MaPPayr8/tp4XO+rmwnyMKeH7zbJN4X9KEAjJGcFLuDjaRttHHUIBoPmVCjeUmUtp8a2HXWdcdZpg60vil2v+bjm6gOGMaHPW8UvXtwwz+/Q3/8sxqsLXizKWfHa/rTFNmn/1xJ7rskDwrImfNi2df9EdjFi2pAv1tMo7uLVOE3jgn6OYSG9tbYx3JoXgv91jbOWNabt2ufn5+d/AQAA//8DAAb8XS4aBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aIveaInuCHWkgSynLYL57wt5pki7t70JlEi+fF7qhQR4QjKQ29Ppr2cMeSE1yX4LCSY0ZZoy+Sj4INjAdMO5aLtWfyE18ff4kwykU9Zya5BK1EiVYy0d5WSpc2g4446JVpWa8HUhwwsB3zh89habuHzEtPgYyEA++eDi99IbfGPnuLomxRn3YckQbBHzz353T9/eHh4fDpdXCy4lufGODMT0HZoJBJWdcVQxGClMbKQjtj3nXMpetZc0HzKmAHOzuG+v/V3MAbMdRCNkw6gw5FwTBxmK5BEWfPx5Kio2RHclXm/h7Ti8kGdMZBD1L5iXcjd29jc2Pj1BcDeTD345kpqcUjxhyh43HNuE8wzZx1Bdm+xmjyGTmjx8qA4z5Cmmpz8YfbiMvv8fk78De/QBq/0dGQjjKJzskU6Ca6p6BxRaaajWUk/IrekEJzXZxZB9WOO6VPuQ8WvaVJKB/A3zghd9t8kefUab11Skf9bFa/zh8y66EmCkJocU3Wpz9Yqbs4Y1kpXLhzWf1ly9R+cT2oxlpse0Xsv/J0VosSXt1pQw5OoA+Vi9geVYQEBvORgwANraCVuGE3MMNOscaqfRKeE6p4xyWhlmtYKRKwctH6cSKbqLu9W2UJ0w48gQqHFmpMpopCNnnBrGZNd1RqIuiF6xVg9zyQPbM4VOUoOTo8pwoEZJSZ1soRN66jox/Ybk6jhnpZjD58vX2BRo53p0mlOheEuVMZz2pu3o2PWjGXWPrS2a7zEFnH8D9c7bFJc45eq6MNUf6N6voXzvalue7z7QH5tld9F+w1QVy8GHss1Xl8/n8/lfAAAA//8DANrGmTolBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/cNhT8KwLPoswvUaJuxjpBF27qRbJIi94ovqeYsEwtSMqJYex/L6jdIvGp6IUgnjiPM/OGeiPBPiMZyO3p9OEFQ06kJtlvJcGEpkxTJo+CD4INTDec9Vwa8Tepib/HVzKQTjnHnUEqUSNVwFo6yslRADSccWCiVaWn/ZbI8EasbwBfvMNmSV8xJr8EMpA/fYDle7nb+sbNywpNXGbch5RtcIXMH/vdPf399nB8OFxOJUwF3HggAzF9h2aygsrOAFXMjtRObKQjtj3nXMpetReYDxljsHOT4Onn/bDkgNkNohGyYVQYcq4J2GwL5dEmPL6eCovNortSr7fyth3eyAtGMoj6XzMv7W7c7G+e07j6GW4ua7bpKW3rZF1e4iupySkuJ4zZ4+bPbUr4PM6vR5uePl7OlG368APdmhF2yxoyGQiXRdBm0Dzb7JdQXTnuZo8hbx9z9CF591+9JKnJw5fqMNs8LfH53Ty+XGze/w+XP1n36ANW+zsyEMZRgOyRToJrqnqw1LbSUK2lnpA70wlOarJbQvZhXdZU7UPGb3GTRAby0c4JL/xuo3v0GV1eY9H5ly65OsQFVpern7PkrGGNZKyA1nxac/UZwUd0GYuIY1yv/b6+hwgtNtBujRFDrg42P1a/2fRYlNvecWussVY7N2HLcGLArGYdoAaNoAR0oIwCrQxzWtmRK7AtH6dSITXBEp1qS+ukJi7aDqhk0lClmaCmBUNBQNejYEbo8Z2P1cNccNb1TCFIanACqgy31CgpKcjWdkJPXSemXyy55oGzYjDgy+XdbQw0QI+gORWKt1QZw2lv2o6OXT+aUffYusL5HmPA+RejPnkXl7RMubompHpn3ec1lH9HtaXluw/0xzaju8U9YazKjK0P5alcx3o+n8//AAAA//8DAGO5D/uCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/jKBSG/4rFtXEBY2x81013tFGn02ianV3tHYZDiupAhHFnqyr/fYSTqumM9mLvrGPe8/GcF16RV3tAPbo+HH5/Bp8mVKLklhAjTGAiMOFb2vVU9k1T1awhksl/UIncLbygHrVca6ol4BoEYG5Ig4faamwMSEqoIazhOafaTah/RcpVBp6dhipM3yBOLnjUo7+cN+F7rq1cpccwmyqGEdZ+Ssrr3MyX9eoWf77ebO83p1MTTFlcOYN6JJVVmmuDhw46zC0o3NXSYgOm1YzUumnqk8z5BNGrsZrM03t9E5KHpHtWsboimEl0LJFRSeWWBzXB9uWQu1gQ3eR4uYSXz/4VPUNEPSvfYJ7SXenRXe2nYXajuRrDbuf8bhW8dbs5qpQLl+gQwwFicrDA+c15FV8+h90u50Of1DgBKtEW4t55NZ5+3IBV85gewhwXMpN5+q8zqEdqTgGVaCE5jkvZ4jzManTg0y/ai8L3D8VmVMmGuP+wpIcT+/X/QH+n9KPzUKxvUI8IBWbqDrBlVGDeGYVVU0ssRC0sUC1bRlGJVsEn5+cwT8XaJ9idqX3o7zrqR5dApznmmf4W2WybGMysU/G+YEoqUtWEZNGcDnMqvoJxEXSCPMQ2zud8P0mYYIvokxvhFzyrOUbwqdio9Fj8oabHBUenqZJKKiW0ttAQsMQQJUhrQBgBhjPTGi65EVwSLbgaKDeqoYPNEVQiyCYrFl+LRrJaGoLtQAnmDenwoEWNOz5YKjtqO9V+gFvcj1mndEc4mBpLsAZzSRWWvK6xqRvVMmHbltkLTmdDUJKpX5rwzwnM2UtfTtZ+m93A8+ken/o0pgMjKGacNphLSXEnmxYPbTfIQXTQ6DzZKvgpvIH8BnEIk0v5Eblz3u3V+POR983cQvQwXmznzukYpmBTcbZl8WFfX2efX7Fiseh35/G/izFugn6CWGRjKecvlnk8Ho8/AAAA//8DAEJm1vgMBQAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1Hm/aI3w27RhZt6ES/aoG8UZxQL0UoLito0MPa/F9RukLpveRMGOsPDb87wjUzhiKQl96fTL2ec8kJqkoetJJgwlBnK1IG7lvtW60Zo7a11f5OaDE/4jbTEqhh59EglGqQKmKad7CMFQM8ZBya0Kj3D54W0byQMDeB5iNjMy5+YlmGeSEv+GiaYv5azw9DEcV6hSfOIu2nJYYrFzB+7hyf6+/3+8Ly//rXgUsTNAKQlPvQhqgi0c+io6jFQJ31PAcFGwWTUWl5lw5QxTWFsFvjy43yY84Q5tqIRsmFUeHKpCYQciuUuLHj4diouNkSPpV5v5e2zfSNnTKQV9XeY13Z3cRzulrUb8YzjKaQF012cj8cwAanJKc0nTHnADcsZUzcvQy5EY7TBOc7BBau0BSe1kBYkQ3DMavDgg1GmF1HrHjrnreoVCK7AoFKdJTXZwI1jyMM8VTfvD+OAUyY1eX6p9mPI/ZyO79C/XInufgJosU1aIpmWxlpUUsrgTScYgjXRGNHFwGzfCeQRmfbKem4Zl14wL7WxzkJgPpKafAjxdZiw2j2SljCOAqRD2gtuqHIQaNDSU2Ok6ZFHbwUnNXmYpzxM67wu1W7K+Dlt9yUt+TWMC15vep/i65Ax5jUVCJ9MCeM+zbDGXP0IAGcNayRjRbTm05qrjwhDwpix4Dik9dbvfxJhxCZ6WFPCKVf7kF+r38LyujF0kQcffAgmxh41w54BC4ZZQAMGQQmwoLwCozyLRoWOKwiad32pkJpgyVu1RdwEpYzskArBLVUGDHVBO6qwQyV7Jhj07zhWz2PRheiYQpDUYw9UeR6oV1JSkDpYYXprRdF9R3ILC2cFMOD5uqxXBwAOwXAqFNdUec+p89rSzrrOd8ahjsXzE6YJx/+A+jDENC9zn6tb1qp36D6uU3lwqi13X4eJ/rPN6HGOXzBVZcZhmMp+3cZ6qckRw7ImPG7vVftG9mW5qsP2bBnTMO5lTV5ySHk93crWNd4xcblcLv8CAAD//wMAL93JHfQEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUwW7cIBT8FYuzcQBjbHyLNo26Stus2lVb9YZ5zwmKg1eA00bR/nuFd6s0p6oXhJ6Zx7yZwS/Em0ckPbk8HN49oU+RlCS5tSSYUJQpyuSed71gPZOVZqxjrfpBSuJu8Jn0pJXWcquR1qiQSmANHerRUgDUnHFgopG5p7mLpH8hxlWAT85iNcevGKKbPenJN+dh/pnvNq6y07xAFeYJtz4m420m82m7uaEfLnf7293pVMSYwZUD0hNtRmOlBTp02FE5oqFdrUcKCK0VrLZNU59gzicM3kxVhIfX+2FOHpPtRSXqilGhybEkYJLJlAcTcf98yCxWia5yvVzL67Z/IU8YSC/KP2Ke2l3YyV08xmFxE1yc1mTiQ1zX0dg0h2dSkkOYDxiSw1WfyxjxcZie9yY+XJ/O5G189wvtkhA28+IT6QmvO1KSVaBpMsnNvjhz3EwOfVo/puB8dPZfvbI0t1+K3WTSOIfHN358Ocm8/Q+VPxp77zwW2yvSE8ZRQN0hHQVXVHZgqGlqTZWq1Yjc6lZwUpLN7JPzy7zEYusT3oV1JNKTazNFPPG7DPbeJbRpCXnO7yrnahdmWGwqXr3krGJVzVgGLemwpOIzggtoE+Yh9mE59/v6FiKUWEGbJQT0qdiZdF+8N/F+nbyz3GijjVHWjtgwHBkwo1gLqEAhSAEtSC1BSc2skmbgEkzDhzFXSEkwR6dY0yrVoKQdWqp1K6jsFKdDMwramLqRo5YDDtncVx2L2ynjjO2YRKipxhGo1NxQLeuaQt2YVqixbcX4lyTnPHCWBQZ8Or27lYEC6BAUp0LyhkqtOe1009Kh7QY9qA4bmznfYPA4/SXUR2fDHOcxFeeEFG+k+7z4/O8o1rT8dJ7+Wj26mu0DhiJ7bJzPT+Vs6/F4PP4GAAD//wMAZSWeyoIEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2Ioc2a5laUJSTwPC/F9LauaJAXwRhSM45c+bMPJEED0h6cnY8Xj5iKjOpSYlbSDChKdOUqQM3vWA9U41lWigj/yY1ie/wM+lJp5zjziKVqJEqz1o6yOCo92g5456JVq054XYm/ROB2Hh8jA6baf4D8xynRHryZ0x++rhiQ2zcOC2+ydOIuzQXSG4l8353/o7+fnZzuL453ZpxXh830ZOeWAjglPN0MGioCgjUSBuoR985waRrW3l6FlPBnGBsZn//Fd9PJWFxvWiEbBgVljzXxEOBlfIAMx4+H1cWm0QXa7zewttv/0QeMZNe1K9intK9cWN88zAPSxz9m+1LanLM0xFzibipcb2vvpLgrGGN0IIxUpMrcHcxYbW7qK7HtURwhin0kloMnirLgVolJfWyhU7o0HUikJq8w5xw/CbrVXR5mqdQqheRq+9w9mfnlwmGEVeMtzDOSGqym/cFkodxSnj5Cd1STrkOeVmPz6dUYlqmZa52qeBthpfz1/fnS86YSnUD5a76Dea7rUXGcbBgAbRzAVuGgXkGmnUetdfolfCdV1Z5rSxzWsHAlYeWD2GNkJr8umq4X5zDef7KZjPJOG4cqpc+nY8RU/lORtITxlF4aZAGwTVVxgOFVlqqtdQBubOd4K8wB8i3WEhPghmGgTk3CGW5A2FB+sCk18EK0XWggmw7JbRuWQfOd53gyssOHcggmRGkJjd58osrP7Zabg24ydM/6MoqFemJZm1rHAyD0IobsAYV18poi51VAZiSbgDeGhgGzqBlzhrhgGsLavBt+KLSZbqNCS/ifBzh8ze4ptGN/MUBGB5A/XD9bYYH/Djl+/cnGzfvLw+kJh+WtG6Earda5GNM9JNeJ/pqGUs83GUEj/5q8vizj3Adl2qbUOOw7bQJlAfDqWKar0bmVA+MgYUOguxITS4md4+5Wh0GMa1T9SXZ9b46y+4uFnRlySu/vzYe1/vqZoQSpvzw3Sb5prAfBWiE5GztjMfH0zbaOGrvDXrNqVC8pcpaTo1tOzp0ZrCDNti6VbHrpRyXUn1AHzO6slX84sUN8/wO3f3PYry64MWinK1e25+22Cbt/1pizzV5QJiXjA/btu6fyC4lzBvyxXIaxV26iuMYZ3RT8jPpbWtFwxjrXhj+572Oi4YJpZ+fn5//BQAA//8DAJsBfCEbBgAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTwW7cOAyGX8XQ2XIkWZYt34LJLjpIuxm0QbvYGy3SHaGOPJDltEUw776QZ4o2vfUiCLRJ/vx+6oUFeCLWs9vT6a9nCmlhJUt+CymhDBeGC/0ou16JXujKilao1v7HSubv6TvrWaudk84Sr8kQ1ygaPtSj44hkpZAoVKNzTfi8sP6Fga+Qnr2jal4+Ulz8HFjPPvmA89fcG3zlpnnFKs4T7cOSILgs5p/97p6/vT08Phwufy205OTKI+uZhRGcdsiHjjquRwLe1XbkSNg6JWrXNPUlzYdEMcBULfjlZ3+cU6DkelWpuhJcWXYuGUKCLHmAhR6/n7KKDdFdjpdbeLv2L+yZIutV+QPmpdyNm/yNm5+eIODN6INfjqxkpzifKCZPG45twmmC5OdQXJvsJk8hsZI9fCgOE6Rxjk+vGH24jL7/g8nfgTv6QMX+jvVMSFJYd8RHJQ3XHQKHprbcmNqMJJ1tlWQl280h+bDO61LsQ6LPcVPJevY3TAtd9N1Gd/SJXFpjlv6vyV7TN592M+aAYCU7xBlXl4qfuKWoRFWL/PFhTac1Fe8JfSSXKM/0GNdr+d9SlFFb0m6NkUIqDpCOxRtYjhuIzkmwYAGMcyM1gkaBAoxokQwaQq2wRW01Gm2FMxoGqREaOYw5knVnd4vLQllXDyCQNwQt12CAD2I72g7G1kLXuFdYi4cp54HrhCasuaURubYSuNV1zbFuoFVmbFs1/oLk6rgUmTfS8+VpbAoMYkdoJFdaNlxbK3lnm5YPbTfYwXTUuKz5nmKg6RdQ77yL8zKPqbguTPEK3fs15OddbMvz1Qf+bbPsbnZfKBbZcvAhb/PV5fP5fP4fAAD//wMAIOQppCUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTy27bMBBFf0Xg2lT4EiVqFzgtaqRtjMRoi+4ozqgmolAGReWBwP9eUHaRtLtuBGHAy7n3zPCVBPuApCWXh8OHRwxpIiuS/FISTGjKNGVqx5tWsJap0jApeMN+khXx1/hCWlIr57gzSCVqpApYRTvZOwqAhjMOTFQq32l/TaR9JdaXgI/eYTlO3zBOfgykJd99gPEp97a+dMM4QxnHATdhSja4bObrZn1NP19udzfb06kJpywuPZCWGNtbpxzQrsGGqh4tbaTpKSDUTjDpqkqeZD4kjMEO5QT3b/1hTAGTa0UpZMmoMOS4ImCTzZY7O+Hu5ZBdLIiucn21lJff9pU8YiStWP2Bebruwg3+4mHqZj/Axemb7HSfUx7ieMCYPC5MlpjDYJMfQ3HutB48hkRW5Oau2A429WN8+AvU3Sn/5j/i73L3D8/o5oSwHueQSEu44mRFvli39wGLzRVpCeMoQDZIe8E1VQ1YaitpqNZS98idqUXWrMeQfJjHeSo2IeGvuAQgLflohwlP1i+j2/uELs0xp/qh8y5s4wizS8Ubf85KVkrGsmhOhzkVtwg+okuY8+3ifL7vH4nQYhGt5xgxpGJr0774ZKf9AqVx3BprrNXO9Vgx7Bkwq1kNqEEjKAE1KKNAK8OcVrbjCmzFuz5XyIpgHnexbFgHoI2sKtqJilFVy54aXnVUKF2j1KbuMCveOBY3Q9ZZ1zCFIKnBHqgy3FKjpKQgK1sL3de16N8hOU+fswwY8PH0VhYHGqBB0JwKxSuqjOG0MVVNu7rpTKcbrFx2cI0x4PAO1Bfv4jiNfSrOy1P8he52Dvm9F8siPflAn5cZXY3uHmORZ2x9yOt9HuvxeDz+BgAA//8DAO0UJnI2BAAA

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2223,6 +2223,7 @@ public sealed class RelationshipMapService(IProjectRepository projects, ICharact
{
CharacterID = x.CharacterID,
CharacterName = x.CharacterName,
CharacterImportance = x.CharacterImportance,
IsFocus = x.CharacterID == focusCharacterId
})
.ToList();
@ -2252,7 +2253,7 @@ public sealed class RelationshipMapService(IProjectRepository projects, ICharact
CharacterRelationshipID = x.CharacterRelationshipID,
SourceCharacterID = x.CharacterAID,
TargetCharacterID = x.CharacterBID,
RelationshipLabel = x.CurrentStateName,
RelationshipLabel = x.RelationshipTypeName,
CategoryName = x.RelationshipCategoryName,
CssClass = x.CssClass,
Intensity = x.CurrentIntensity

View File

@ -990,6 +990,7 @@ public sealed class RelationshipMapNodeViewModel
{
public int CharacterID { get; set; }
public string CharacterName { get; set; } = string.Empty;
public int? CharacterImportance { get; set; }
public bool IsFocus { get; set; }
}

View File

@ -70,11 +70,20 @@
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Project Start</a>
</form>
<section class="relationship-map-legend" aria-label="Relationship categories">
<section class="relationship-map-legend plain-card" aria-label="Relationship categories">
<div>
<p class="eyebrow">Relationship key</p>
<h2>Category colours</h2>
</div>
<div class="relationship-map-legend-grid">
@foreach (var category in Model.Categories)
{
<span class="relationship-map-legend-item @category.CssClass">@category.CategoryName</span>
<span class="relationship-map-legend-item @category.CssClass">
<span class="relationship-map-legend-line" aria-hidden="true"></span>
<span>@category.CategoryName</span>
</span>
}
</div>
</section>
<section class="relationship-map-workspace">
@ -83,12 +92,17 @@
{
<div class="empty-state">
<h2>No visible relationships at this point</h2>
<p class="muted">Try choosing a later book, chapter, or scene, or remove the character focus.</p>
<p class="muted">Add initial relationships or relationship events, choose a later story point, or remove the character focus to build the map.</p>
</div>
}
else
{
<svg id="relationshipMapSvg" class="relationship-map-svg" role="img" aria-label="Relationship network"></svg>
<div class="relationship-map-toolbar" aria-label="Map controls">
<button class="btn btn-outline-secondary btn-sm" type="button" data-map-fit>Fit</button>
<button class="btn btn-outline-secondary btn-sm" type="button" data-map-reset>Reset view</button>
</div>
<div id="relationshipMapGraph" class="relationship-map-graph" role="img" aria-label="Relationship network"></div>
<p id="relationshipMapError" class="alert alert-warning d-none mt-3">The relationship map renderer could not load. The relationship list below is still available.</p>
}
</div>
@ -140,108 +154,158 @@
</section>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/cytoscape/dist/cytoscape.min.js"></script>
<script>
(() => {
const nodes = @Html.Raw(JsonSerializer.Serialize(Model.Nodes, jsonOptions));
const links = @Html.Raw(JsonSerializer.Serialize(Model.Links, jsonOptions));
const relationships = @Html.Raw(JsonSerializer.Serialize(Model.Relationships, jsonOptions));
const svg = document.getElementById("relationshipMapSvg");
if (!svg || !nodes.length) return;
const width = 960;
const height = 620;
const centreX = width / 2;
const centreY = height / 2;
const radius = Math.max(180, Math.min(width, height) / 2 - 90);
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
const positions = new Map();
nodes.forEach((node, index) => {
const angle = nodes.length === 1 ? -Math.PI / 2 : (-Math.PI / 2) + (index * 2 * Math.PI / nodes.length);
const nodeRadius = node.isFocus ? radius * 0.15 : radius;
positions.set(node.characterID, {
x: centreX + Math.cos(angle) * nodeRadius,
y: centreY + Math.sin(angle) * nodeRadius
});
});
const relationshipById = new Map(relationships.map(item => [item.characterRelationshipID, item]));
const makeSvg = name => document.createElementNS("http://www.w3.org/2000/svg", name);
const linkLayer = makeSvg("g");
const nodeLayer = makeSvg("g");
svg.append(linkLayer, nodeLayer);
const graph = document.getElementById("relationshipMapGraph");
const error = document.getElementById("relationshipMapError");
if (!graph || !nodes.length) return;
if (!window.cytoscape) {
error?.classList.remove("d-none");
return;
}
const focusCharacterId = @Html.Raw(JsonSerializer.Serialize(Model.FocusCharacterID));
const fullNetwork = @Html.Raw(JsonSerializer.Serialize(Model.ShowFullNetwork));
const connectedToFocus = new Set();
if (focusCharacterId && fullNetwork) {
links.forEach(link => {
const from = positions.get(link.sourceCharacterID);
const to = positions.get(link.targetCharacterID);
if (!from || !to) return;
if (link.sourceCharacterID === focusCharacterId) connectedToFocus.add(link.targetCharacterID);
if (link.targetCharacterID === focusCharacterId) connectedToFocus.add(link.sourceCharacterID);
});
}
const group = makeSvg("g");
group.classList.add("relationship-map-link-group", link.cssClass);
group.dataset.relationshipId = link.characterRelationshipID;
group.setAttribute("tabindex", "0");
group.setAttribute("role", "button");
group.setAttribute("aria-label", `${link.relationshipLabel} relationship`);
const elements = [
...nodes.map(node => ({
group: "nodes",
data: {
id: `c-${node.characterID}`,
characterID: node.characterID,
label: node.characterName,
importance: node.characterImportance || 0,
width: node.isFocus ? 172 : node.characterImportance >= 8 ? 158 : node.characterImportance >= 4 ? 144 : 128,
height: node.isFocus ? 58 : node.characterImportance >= 8 ? 52 : 46,
isFocus: node.isFocus,
isNeighbour: connectedToFocus.has(node.characterID)
},
classes: [
node.isFocus ? "is-focus" : "",
node.characterImportance >= 8 ? "is-major-character" : "",
node.characterImportance >= 4 && node.characterImportance < 8 ? "is-supporting-character" : "",
focusCharacterId && fullNetwork && !node.isFocus && !connectedToFocus.has(node.characterID) ? "is-dimmed" : ""
].filter(Boolean).join(" ")
})),
...links.map(link => ({
group: "edges",
data: {
id: `r-${link.characterRelationshipID}`,
relationshipID: link.characterRelationshipID,
source: `c-${link.sourceCharacterID}`,
target: `c-${link.targetCharacterID}`,
label: link.relationshipLabel,
category: link.categoryName,
intensity: link.intensity || 4,
tooltip: buildRelationshipTooltip(relationshipById.get(link.characterRelationshipID))
},
classes: [
link.cssClass,
focusCharacterId && fullNetwork && link.sourceCharacterID !== focusCharacterId && link.targetCharacterID !== focusCharacterId ? "is-dimmed" : ""
].filter(Boolean).join(" ")
}))
];
const line = makeSvg("line");
line.setAttribute("x1", from.x);
line.setAttribute("y1", from.y);
line.setAttribute("x2", to.x);
line.setAttribute("y2", to.y);
line.setAttribute("stroke-width", Math.max(3, Math.min(9, link.intensity || 4)));
const label = makeSvg("text");
label.setAttribute("x", (from.x + to.x) / 2);
label.setAttribute("y", (from.y + to.y) / 2 - 8);
label.setAttribute("text-anchor", "middle");
label.textContent = link.relationshipLabel;
group.append(line, label);
group.addEventListener("click", () => selectRelationship(link.characterRelationshipID));
group.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectRelationship(link.characterRelationshipID);
const cy = cytoscape({
container: graph,
elements,
minZoom: 0.25,
maxZoom: 2.5,
wheelSensitivity: 0.18,
style: relationshipMapStyle(),
layout: {
name: nodes.length <= 8 ? "circle" : "cose",
animate: false,
fit: true,
padding: 48,
nodeRepulsion: 9000,
idealEdgeLength: 170,
edgeElasticity: 80,
numIter: 1200,
randomize: false
}
});
linkLayer.append(group);
cy.ready(() => {
cy.fit(undefined, 48);
const first = relationships[0];
if (first) selectRelationship(first.characterRelationshipID);
});
nodes.forEach(node => {
const position = positions.get(node.characterID);
const group = makeSvg("g");
group.classList.add("relationship-map-node");
if (node.isFocus) group.classList.add("is-focus");
cy.on("tap", "edge", event => {
selectRelationship(event.target.data("relationshipID"));
});
const circle = makeSvg("circle");
circle.setAttribute("cx", position.x);
circle.setAttribute("cy", position.y);
circle.setAttribute("r", node.isFocus ? 34 : 27);
cy.on("tap", "node", event => {
const characterId = event.target.data("characterID");
const characterName = event.target.data("label");
document.getElementById("relationshipMapEmptyDetail")?.classList.add("d-none");
document.getElementById("relationshipMapDetail")?.classList.remove("d-none");
document.querySelector("[data-map-detail-title]").textContent = characterName;
document.querySelector("[data-map-detail-category]").textContent = "Character";
document.querySelector("[data-map-detail-reciprocal]").textContent = characterId === focusCharacterId ? "Focused" : "Node";
document.querySelector("[data-map-detail-type]").textContent = "Character";
document.querySelector("[data-map-detail-initial]").textContent = "Select a relationship line to see relationship state.";
document.querySelector("[data-map-detail-current]").textContent = "Use Character focus to centre this character.";
const history = document.querySelector("[data-map-detail-history]");
history.innerHTML = "";
const li = document.createElement("li");
li.innerHTML = `<a href="/Characters/Details/${characterId}">Open character details</a> or <a href="/RelationshipMap?projectId=@Model.Project.ProjectID&focusCharacterId=${characterId}">focus this character</a>.`;
history.append(li);
});
const label = makeSvg("text");
label.setAttribute("x", position.x);
label.setAttribute("y", position.y + 50);
label.setAttribute("text-anchor", "middle");
label.textContent = node.characterName;
cy.on("mouseover", "node, edge", event => {
const label = event.target.isNode() ? event.target.data("label") : event.target.data("tooltip");
graph.setAttribute("title", label || "");
});
group.append(circle, label);
nodeLayer.append(group);
document.querySelector("[data-map-fit]")?.addEventListener("click", () => {
cy.resize();
cy.fit(undefined, 48);
});
document.querySelector("[data-map-reset]")?.addEventListener("click", () => {
cy.layout({
name: nodes.length <= 8 ? "circle" : "cose",
animate: false,
fit: true,
padding: 48,
randomize: false
}).run();
});
window.addEventListener("resize", () => {
window.requestAnimationFrame(() => {
cy.resize();
cy.fit(undefined, 48);
});
});
document.querySelectorAll("[data-map-select]").forEach(button => {
button.addEventListener("click", () => selectRelationship(Number(button.dataset.mapSelect)));
});
if (relationships.length) {
selectRelationship(relationships[0].characterRelationshipID);
}
function selectRelationship(id) {
const relationship = relationshipById.get(id);
if (!relationship) return;
document.querySelectorAll(".relationship-map-link-group, .relationship-map-list-item").forEach(item => item.classList.remove("is-selected"));
document.querySelectorAll(`[data-relationship-id="${id}"], [data-map-select="${id}"]`).forEach(item => item.classList.add("is-selected"));
cy.elements().removeClass("is-selected");
cy.$id(`r-${id}`).addClass("is-selected");
document.querySelectorAll(".relationship-map-list-item").forEach(item => item.classList.remove("is-selected"));
document.querySelectorAll(`[data-map-select="${id}"]`).forEach(item => item.classList.add("is-selected"));
document.getElementById("relationshipMapEmptyDetail")?.classList.add("d-none");
document.getElementById("relationshipMapDetail")?.classList.remove("d-none");
@ -265,6 +329,128 @@
function formatState(name, intensity) {
return intensity ? `${name} / Intensity ${intensity}` : name;
}
function buildRelationshipTooltip(relationship) {
if (!relationship) return "";
return `${relationship.characterAName} - ${relationship.characterBName}: ${relationship.relationshipTypeName}; current ${formatState(relationship.currentStateName, relationship.currentIntensity)}`;
}
function relationshipMapStyle() {
const isDark = document.documentElement.dataset.theme === "dark";
const nodeFill = isDark ? "#222126" : "#fffdf8";
const nodeText = isDark ? "#f7efe4" : "#1f2937";
const focusFill = isDark ? "#3b2a16" : "#fff3d8";
const majorFill = isDark ? "#332417" : "#fff0d5";
const supportingFill = isDark ? "#29252c" : "#fbf6ef";
const labelFill = isDark ? "#f7efe4" : "#2f3742";
const labelBackground = isDark ? "#17171c" : "#fffdf8";
return [
{
selector: "node",
style: {
"label": "data(label)",
"shape": "round-rectangle",
"width": "data(width)",
"height": "data(height)",
"background-color": nodeFill,
"border-color": "#6b7280",
"border-width": 2,
"color": nodeText,
"font-size": 12,
"font-weight": 650,
"text-wrap": "wrap",
"text-max-width": 118,
"text-valign": "center",
"text-halign": "center",
"text-margin-y": 0,
"padding": "10px",
"shadow-blur": 10,
"shadow-color": isDark ? "rgba(0, 0, 0, 0.45)" : "rgba(31, 41, 55, 0.18)",
"shadow-opacity": 0.32,
"shadow-offset-y": 3,
"overlay-opacity": 0
}
},
{
selector: "node.is-supporting-character",
style: {
"background-color": supportingFill,
"border-color": isDark ? "#b9a891" : "#b99970",
"border-width": 2.5,
"font-weight": 700
}
},
{
selector: "node.is-major-character",
style: {
"background-color": majorFill,
"border-color": "#c47c2c",
"border-width": 3,
"font-size": 13,
"font-weight": 800,
"shadow-opacity": 0.42
}
},
{
selector: "node.is-focus",
style: {
"width": "data(width)",
"height": "data(height)",
"background-color": focusFill,
"border-color": "#c47c2c",
"border-width": 4,
"font-size": 13,
"font-weight": 850,
"shadow-opacity": 0.5
}
},
{
selector: "edge",
style: {
"label": "data(label)",
"curve-style": "bezier",
"width": "mapData(intensity, 1, 10, 2, 8)",
"line-color": "#7a8796",
"target-arrow-shape": "none",
"font-size": 11,
"font-weight": 700,
"color": labelFill,
"text-background-color": labelBackground,
"text-background-opacity": 0.88,
"text-background-padding": 3,
"text-rotation": "none",
"overlay-opacity": 0
}
},
{ selector: "edge.relationship-category-family", style: { "line-color": "#b86522", "color": "#7c3f12" } },
{ selector: "edge.relationship-category-friendship", style: { "line-color": "#2f9e5f", "color": "#14532d" } },
{ selector: "edge.relationship-category-romantic", style: { "line-color": "#d9467f", "color": "#831843" } },
{ selector: "edge.relationship-category-hostile", style: { "line-color": "#dc3f3f", "color": "#7f1d1d" } },
{ selector: "edge.relationship-category-professional", style: { "line-color": "#4f7fd8", "color": "#1e3a8a" } },
{ selector: "edge.relationship-category-other", style: { "line-color": "#7a8796", "color": "#374151" } },
{
selector: ".is-dimmed",
style: {
"opacity": 0.25
}
},
{
selector: "edge.is-selected",
style: {
"width": 9,
"z-index": 20,
"text-background-opacity": 1
}
},
{
selector: "node:selected",
style: {
"border-color": "#c47c2c",
"border-width": 4
}
}
];
}
})();
</script>
}

View File

@ -1471,13 +1471,46 @@ a:focus-visible {
}
.relationship-map-legend {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
display: grid;
grid-template-columns: minmax(160px, max-content) 1fr;
gap: 0.9rem 1.2rem;
align-items: center;
margin: 0 0 1rem;
}
.relationship-map-legend-item,
.relationship-map-legend h2 {
margin: 0;
font-size: 1rem;
}
.relationship-map-legend-grid {
display: flex;
flex-wrap: wrap;
gap: 0.45rem 0.8rem;
align-items: center;
}
.relationship-map-legend-item {
display: inline-flex;
gap: 0.45rem;
align-items: center;
padding: 0.28rem 0.45rem;
color: inherit;
background: transparent;
border: 1px solid transparent;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 650;
}
.relationship-map-legend-line {
display: inline-block;
width: 2.15rem;
height: 0;
border-top: 4px solid currentColor;
border-radius: 999px;
}
.relationship-map-detail-badges .status-pill,
.relationship-map-list-item .status-pill {
border: 1px solid transparent;
@ -1495,60 +1528,23 @@ a:focus-visible {
overflow: hidden;
}
.relationship-map-svg {
display: block;
.relationship-map-toolbar {
display: flex;
justify-content: flex-end;
gap: 0.4rem;
margin-bottom: 0.6rem;
padding-bottom: 0.45rem;
border-bottom: 1px solid rgba(107, 114, 128, 0.12);
}
.relationship-map-graph {
width: 100%;
min-height: 620px;
}
.relationship-map-link-group {
cursor: pointer;
}
.relationship-map-link-group line {
stroke: #7a8796;
stroke-linecap: round;
opacity: 0.72;
transition: opacity 0.16s ease, stroke-width 0.16s ease;
}
.relationship-map-link-group text {
fill: #2f3742;
font-size: 0.78rem;
font-weight: 700;
paint-order: stroke;
stroke: rgba(255, 255, 255, 0.86);
stroke-width: 4px;
stroke-linejoin: round;
}
.relationship-map-link-group:hover line,
.relationship-map-link-group:focus line,
.relationship-map-link-group.is-selected line {
opacity: 1;
}
.relationship-map-link-group.is-selected line {
stroke-width: 8;
}
.relationship-map-node circle {
fill: #fffdf8;
stroke: #6b7280;
stroke-width: 2;
filter: drop-shadow(0 6px 12px rgba(31, 41, 55, 0.14));
}
.relationship-map-node.is-focus circle {
fill: #fff3d8;
stroke: #c47c2c;
stroke-width: 3;
}
.relationship-map-node text {
fill: #1f2937;
font-size: 0.82rem;
font-weight: 700;
border: 1px solid rgba(107, 114, 128, 0.16);
border-radius: 0.75rem;
background:
radial-gradient(circle at 20% 20%, rgba(196, 124, 44, 0.08), transparent 28rem),
linear-gradient(180deg, rgba(255, 253, 248, 0.95), rgba(248, 250, 252, 0.86));
}
.relationship-map-details {
@ -1613,71 +1609,40 @@ a:focus-visible {
outline: none;
}
.relationship-category-family,
.relationship-category-family line {
.relationship-category-family {
color: #7c3f12;
border-color: #d89a5e;
background: #fff1df;
stroke: #b86522;
}
.relationship-category-friendship,
.relationship-category-friendship line {
.relationship-category-friendship {
color: #14532d;
border-color: #7cc69a;
background: #e7f8ed;
stroke: #2f9e5f;
}
.relationship-category-romantic,
.relationship-category-romantic line {
.relationship-category-romantic {
color: #831843;
border-color: #efa3c1;
background: #fde7f0;
stroke: #d9467f;
}
.relationship-category-hostile,
.relationship-category-hostile line {
.relationship-category-hostile {
color: #7f1d1d;
border-color: #ee8f8f;
background: #fee6e6;
stroke: #dc3f3f;
}
.relationship-category-professional,
.relationship-category-professional line {
.relationship-category-professional {
color: #1e3a8a;
border-color: #93b4ef;
background: #e8f0ff;
stroke: #4f7fd8;
}
.relationship-category-other,
.relationship-category-other line {
.relationship-category-other {
color: #374151;
border-color: #bac2cf;
background: #f2f4f7;
stroke: #7a8796;
}
[data-theme="dark"] .relationship-map-link-group text {
fill: #f7efe4;
stroke: rgba(20, 20, 22, 0.92);
}
[data-theme="dark"] .relationship-map-node circle {
fill: #222126;
stroke: #9ca3af;
}
[data-theme="dark"] .relationship-map-node.is-focus circle {
fill: #3b2a16;
stroke: #f1c38f;
}
[data-theme="dark"] .relationship-map-node text {
fill: #f7efe4;
}
[data-theme="dark"] .relationship-map-list-item {
@ -1685,6 +1650,53 @@ a:focus-visible {
border-color: rgba(255, 255, 255, 0.12);
}
[data-theme="dark"] .relationship-map-graph {
border-color: rgba(255, 255, 255, 0.12);
background:
radial-gradient(circle at 20% 20%, rgba(241, 195, 143, 0.08), transparent 28rem),
linear-gradient(180deg, rgba(33, 32, 38, 0.96), rgba(22, 22, 27, 0.92));
}
[data-theme="dark"] .relationship-map-toolbar {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .relationship-category-family {
color: #ffd2a5;
background: rgba(184, 101, 34, 0.17);
border-color: rgba(255, 210, 165, 0.3);
}
[data-theme="dark"] .relationship-category-friendship {
color: #a8e8bf;
background: rgba(47, 158, 95, 0.16);
border-color: rgba(168, 232, 191, 0.3);
}
[data-theme="dark"] .relationship-category-romantic {
color: #f8b5cf;
background: rgba(217, 70, 127, 0.16);
border-color: rgba(248, 181, 207, 0.3);
}
[data-theme="dark"] .relationship-category-hostile {
color: #ffb4b4;
background: rgba(220, 63, 63, 0.16);
border-color: rgba(255, 180, 180, 0.3);
}
[data-theme="dark"] .relationship-category-professional {
color: #b7cdf7;
background: rgba(79, 127, 216, 0.16);
border-color: rgba(183, 205, 247, 0.3);
}
[data-theme="dark"] .relationship-category-other {
color: #d0d6df;
background: rgba(122, 135, 150, 0.15);
border-color: rgba(208, 214, 223, 0.28);
}
@media (max-width: 1200px) {
.timeline-filter-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));