Skip to content

Commit

Permalink
Add links graph
Browse files Browse the repository at this point in the history
  • Loading branch information
ggodlewski committed Feb 8, 2024
1 parent d211ccb commit 5f2c8b4
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 11 deletions.
203 changes: 201 additions & 2 deletions apps/ui/src/components/BackLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<slot name="header">
<h5>Back Links</h5>
</slot>

<table class="table table-hover table-clickable table-bordered table-layout-fixed" v-if="backlinks && backlinks.length > 0">
<tbody>
<tr v-for="(item, idx) of backlinks" :key="idx" @click="selectFile(item.path)">
Expand All @@ -16,10 +17,38 @@
<div v-else>
No BackLinks
</div>

<h5>Links</h5>

<table class="table table-hover table-clickable table-bordered table-layout-fixed" v-if="links && links.length > 0">
<tbody>
<tr v-for="(item, idx) of links" :key="idx" @click="selectFile(item.path)">
<td class="text-overflow" data-bs-toggle="tooltip" data-bs-placement="top" :title="item.path">
<button class="btn btn-sm float-end" @click.prevent.stop="goToGDocs(item.fileId)"><i class="fa-brands fa-google-drive"></i></button>
<span>{{item.path}}</span>
</td>
</tr>
</tbody>
</table>
<div v-else>
No Links
</div>

<svg ref="graph" width="830" height="300" viewBox="0 0 830 300" style="max-width: 100%; height: auto;">
<defs>
<marker id="arrow" viewBox="0 -10 20 20" refX="100" refY="0" markerWidth="8" markerHeight="8" orient="auto">
<path class="cool arrowHead" d="M0,-10L20,0L0,10" style="fill: teal; stroke: teal;"></path>
</marker>
</defs>
<!--
<line class="cool" x1="100" y1="150" x2="130" y2="150" stroke="teal" stroke-width="1" marker-end="url(#arrow)"></line>
-->
</svg>
</div>
</template>
<script>
import {UtilsMixin} from './UtilsMixin.ts';
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
export default {
mixins: [UtilsMixin],
Expand All @@ -29,7 +58,12 @@ export default {
},
data() {
return {
backlinks: []
backlinks: [],
links: [],
graphData: {
edges: [],
nodes: []
}
};
},
async created() {
Expand All @@ -45,10 +79,14 @@ export default {
methods: {
async fetch() {
if (this.selectedFile.id) {
this.backlinks = await this.FileClientService.getBacklinks(this.driveId, this.selectedFile.id);
const { backlinks, links } = await this.FileClientService.getBacklinks(this.driveId, this.selectedFile.id);
this.backlinks = backlinks;
this.links = links;
} else {
this.backlinks = [];
this.links = [];
}
this.updateGraph();
},
selectFile(path) {
if (this.isAddon) {
Expand All @@ -57,6 +95,167 @@ export default {
} else {
this.$router.push('/drive/' + this.driveId + this.contentDir + path);
}
},
updateGraph() {
const data = {
edges: [],
nodes: []
};
{
const nodes = {
[this.selectedFile.id]: {
id: this.selectedFile.id,
title: this.selectedFile.fileName,
path: this.selectedFile.path,
group: 0
}
};
for (const row of this.links) {
nodes[row.fileId] = {
id: row.fileId,
title: row.name,
path: row.path,
group: 1
};
data.edges.push({
source: this.selectedFile.id,
target: row.fileId,
value: 1
});
}
for (const row of this.backlinks) {
nodes[row.fileId] = {
id: row.fileId,
title: row.name,
path: row.path,
group: 2
};
data.edges.push({
source: row.fileId,
target: this.selectedFile.id,
value: 1
});
}
for (const nodeId of Object.keys(nodes)) {
data.nodes.push(nodes[nodeId]);
}
}
console.log('data', data);
const svg = d3.select(this.$refs.graph);
this.graphData.edges = data.edges.map(d => ({...d}));
this.graphData.nodes = data.nodes.map(d => ({...d}));
const width = 830;
const height = 300;
// Specify the color scale.
const color = d3.scaleOrdinal(d3.schemeCategory10);
console.log('1',this.graphData.edges);
console.log('11',this.graphData.nodes);
// Add a line for each link, and a circle for each node.
const link = svg.append('g')
.attr('stroke', '#999')
.attr('stroke-width', 4)
.attr('stroke-opacity', 0.6)
.selectAll()
.data(this.graphData.edges)
.join('line')
.attr('marker-end', 'url(#arrow)')
.attr('stroke-width', d => Math.sqrt(d.value));
const node = svg.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(this.graphData.nodes)
.join('circle')
.style('cursor', d => d.group !== 0 ? 'pointer' : undefined)
.attr('r', 30)
.attr('fill', d => color(d.group));
const texts = svg.selectAll('text.label')
.data(this.graphData.nodes)
.enter().append('text')
.attr('class', 'label')
.attr('fill', '#999')
.style('cursor', d => d.group !== 0 ? 'pointer' : undefined)
.style('text-anchor', 'middle')
.text(function(d) { return d.title; });
// Set the position attributes of edges and nodes each time the simulation ticks.
function ticked() {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
texts.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
// Create a simulation with several forces.
const simulation = d3.forceSimulation(this.graphData.nodes)
.force('link', d3.forceLink(this.graphData.edges).id(d => d.id).distance(200))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked);
node.append('title')
.text(d => d.title);
// Add a drag behavior.
node.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
node.on('click', (element, data) => {
this.$router.push('/drive/' + this.driveId + this.contentDir + data.path + '#drive_backlinks');
});
texts.on('click', (element, data) => {
this.$router.push('/drive/' + this.driveId + this.contentDir + data.path + '#drive_backlinks');
});
// Reheat the simulation when drag starts, and fix the subject position.
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
// Update the subject (dragged node) position during drag.
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
// Restore the target alpha so the simulation cools after dragging ends.
// Unfix the subject position now that it’s no longer being dragged.
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
// When this cell is re-run, stop the previous simulation. (This doesn’t
// really matter since the target alpha is zero and the simulation will
// stop naturally, but it’s a good practice.)
// invalidation.then(() => simulation.stop());
console.log('simulation', simulation);
}
}
};
Expand Down
29 changes: 21 additions & 8 deletions src/containers/server/routes/BackLinksController.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Controller, RouteGet, RouteParamPath} from './Controller';
import {FileContentService} from '../../../utils/FileContentService';
import {LocalLinks} from '../../transform/LocalLinks';
import {UserConfigService} from '../../google_folder/UserConfigService';
import {MarkdownTreeProcessor} from '../../transform/MarkdownTreeProcessor';
import {getContentFileService} from '../../transform/utils';
import {Controller, RouteGet, RouteParamPath} from './Controller.ts';
import {FileContentService} from '../../../utils/FileContentService.ts';
import {LocalLinks} from '../../transform/LocalLinks.ts';
import {UserConfigService} from '../../google_folder/UserConfigService.ts';
import {MarkdownTreeProcessor} from '../../transform/MarkdownTreeProcessor.ts';
import {getContentFileService} from '../../transform/utils.ts';

export class BackLinksController extends Controller {

Expand All @@ -25,10 +25,23 @@ export class BackLinksController extends Controller {
const localLinks = new LocalLinks(contentFileService);
await localLinks.load();

const linkFileIds = localLinks.getLinks(fileId);
const links = [];
for (const linkFileId of linkFileIds) {
const [file] = await markdownTreeProcessor.findById(linkFileId);
if (file) {
links.push({
folderId: file.parentId,
fileId: linkFileId,
path: file.path,
name: file.fileName
});
}
}

const backLinkFileIds = localLinks.getBackLinks(fileId);
const backlinks = [];
for (const backLinkFileId of backLinkFileIds) {

const [file] = await markdownTreeProcessor.findById(backLinkFileId);
if (file) {
backlinks.push({
Expand All @@ -40,7 +53,7 @@ export class BackLinksController extends Controller {
}
}

return backlinks;
return { backlinks, links };
}

}
15 changes: 14 additions & 1 deletion src/containers/transform/LocalLinks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {FileContentService} from '../../utils/FileContentService';
import {FileContentService} from '../../utils/FileContentService.ts';

interface Link {
fileId: string;
Expand Down Expand Up @@ -60,6 +60,19 @@ export class LocalLinks {
return Array.from(retVal);
}

getLinks(fileId) {
for (const link of this.links) {
if (link.fileId === fileId) {
const links = link.links
.filter(link => link.startsWith('gdoc:'))
.map(link => link.substring('gdoc:'.length));

return links;
}
}
return [];
}

async save() {
const content = 'source;name;dest';

Expand Down

0 comments on commit 5f2c8b4

Please sign in to comment.