1
0
mirror of https://github.com/lxsang/antd-web-apps synced 2024-11-19 18:08:21 +01:00

add explore mode to blog

This commit is contained in:
lxsang 2021-02-17 12:59:42 +01:00
parent bf52f93e27
commit d416ae36c8
7 changed files with 387 additions and 5 deletions

218
blog/assets/graph.js Normal file
View File

@ -0,0 +1,218 @@
$(document).ready(function () {
const colors = [
"#3957ff", "#d3fe14", "#c9080a", "#fec7f8", "#0b7b3e", "#0bf0e9", "#c203c8", "#fd9b39",
"#888593", "#906407", "#98ba7f", "#fe6794", "#10b0ff", "#ac7bff", "#fee7c0", "#964c63",
"#1da49c", "#0ad811", "#bbd9fd", "#fe6cfe", "#297192", "#d1a09c", "#78579e", "#81ffad",
"#739400", "#ca6949", "#d9bf01", "#646a58", "#d5097e", "#bb73a9", "#ccf6e9", "#9cb4b6",
"#b6a7d4", "#9e8c62", "#6e83c8", "#01af64", "#a71afd", "#cfe589", "#d4ccd1", "#fd4109",
"#bf8f0e", "#2f786e", "#4ed1a5", "#d8bb7d", "#a54509", "#6a9276", "#a4777a", "#fc12c9",
"#606f15", "#3cc4d9", "#f31c4e", "#73616f", "#f097c6", "#fc8772", "#92a6fe", "#875b44",
"#699ab3", "#94bc19", "#7d5bf0", "#d24dfe", "#c85b74", "#68ff57", "#b62347", "#994b91",
"#646b8c", "#977ab4", "#d694fd", "#c4d5b5", "#fdc4bd", "#1cae05", "#7bd972", "#e9700a",
"#d08f5d", "#8bb9e1", "#fde945", "#a29d98", "#1682fb", "#9ad9e0", "#d6cafe", "#8d8328",
"#b091a7", "#647579", "#1f8d11", "#e7eafd", "#b9660b", "#a4a644", "#fec24c", "#b1168c",
"#188cc1", "#7ab297", "#4468ae", "#c949a6", "#d48295", "#eb6dc2", "#d5b0cb", "#ff9ffb",
"#fdb082", "#af4d44", "#a759c4", "#a9e03a", "#0d906b", "#9ee3bd", "#5b8846", "#0d8995",
"#f25c58", "#70ae4f", "#847f74", "#9094bb", "#ffe2f1", "#a67149", "#936c8e", "#d04907",
"#c3b8a6", "#cef8c4", "#7a9293", "#fda2ab", "#2ef6c5", "#807242", "#cb94cc", "#b6bdd0",
"#b5c75d", "#fde189", "#b7ff80", "#fa2d8e", "#839a5f", "#28c2b5", "#e5e9e1", "#bc79d8",
"#7ed8fe", "#9f20c3", "#4f7a5b", "#f511fd", "#09c959", "#bcd0ce", "#8685fd", "#98fcff",
"#afbff9", "#6d69b4", "#5f99fd", "#aaa87e", "#b59dfb", "#5d809d", "#d9a742", "#ac5c86",
"#9468d5", "#a4a2b2", "#b1376e", "#d43f3d", "#05a9d1", "#c38375", "#24b58e", "#6eabaf"];
d3.json("/post/graph_json")
.then(
function (json) {
if (json.result) {
const tooltip_div = d3.select("#desktop")
.append("div")
.attr("class", "d3tooltip")
.style("display", "none");
const links = json.result.links;
const nodes = json.result.nodes;
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
tooltip_div.style("display", "none");
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
};
const simulation = d3.forceSimulation(nodes)
.force("link",
d3.forceLink(links)
.id(d => d.id)
.distance(d => 2.0 / d.score)
)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter());
const svg = d3.create("svg")
.attr("preserveAspectRatio", "xMidYMid meet");
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.8)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => d.score * 7.0); //d.score
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", (d) => {
conn = links.filter((l) => {
//console.log(d.id, l.target.id, l.source.id)
return l.target.id == d.id || l.source.id == d.id;
}).map(c=>c.score);
//return conn.reduce((a, b) => a + b, 0) * 10;
return conn.length;
})
.attr("fill", (d) => {
conn = links.filter((l) => {
//console.log(d.id, l.target.id, l.source.id)
return l.target.id == d.id || l.source.id == d.id;
});
return colors[conn.length % colors.length - 1];
})
.on("click", (d) => {
const index = $(d.target).index();
const data = nodes[index];
d3.json("/post/json/" + data.id)
.then( (json) => {
if(json.result)
{
$("#floating_content").html(json.result.description);
$("#floating_container").show();
$("#floating_btn_read_more").attr("href", "/post/id/" + json.result.id);
}
})
.catch ((e)=>{
console.log(e);
});
})
.call(drag(simulation))
.on('mouseover', function (d) {
const index = $(d.target).index();
const data = nodes[index];
link.style('stroke', function (l) {
if (data.id == l.source.id || data.id == l.target.id)
return "#9a031e";
else
return "#999";
});
const off = $("#desktop").offset();
tooltip_div.transition()
.duration(200)
tooltip_div.style("display", "block")
.style("opacity", .8);
tooltip_div.html(data.title)
.style("left", (d.clientX - off.left + 10) + "px")
.style("top", (d.clientY - off.top + 10) + "px");
})
.on('mouseout', function () {
link.style('stroke', "#999");
tooltip_div.style("display", "none");
});
//node.append("title")
// .text(d => d.title);
/*const label = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.2)
.selectAll("text")
.data(nodes)
.join("text")
.text(d=>d.id)
.style("user-select", "none")
.style("font-size", (d) =>{
conn = links.filter((l) => {
//console.log(d.id, l.target.id, l.source.id)
return l.target.id == d.id || l.source.id == d.id;
});
return conn.length + "px";
})
.style('fill', '#000');*/
simulation.on("tick", () => {
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);
const nodes_x = nodes.map(d => d.x);
const nodes_y = nodes.map(d => d.y);
const min_x = Math.min(...nodes_x) - 10;
const min_y = Math.min(...nodes_y) - 10;
const w = Math.max(...nodes_x) - min_x + 10;
const h = Math.max(...nodes_y) - min_y + 10;
svg.attr("viewBox",
[min_x, min_y, w, h]);
/*label
.attr("x", d => {
conn = links.filter((l) => {
//console.log(d.id, l.target.id, l.source.id)
return l.target.id == d.id || l.source.id == d.id;
});
return d.x - conn.length / 2;
})
.attr("y", d => {
conn = links.filter((l) => {
//console.log(d.id, l.target.id, l.source.id)
return l.target.id == d.id || l.source.id == d.id;
});
return d.y + conn.length / 2;
});*/
});
// invalidation.then(() => simulation.stop());
$("#floating_btn_close").click((e)=>{
$("#floating_container").hide();
});
$("#desktop")
.css("position", "relative");
$("#container")
.css("height", "100%")
.css("position", "relative")
.append($(svg.node())
.css("height", "calc(100% - 10px)")
.css("margin", "0 auto")
.css("display", "block"));
}
}
)
.catch((e) => {
console.log(e);
});
});

View File

@ -18,7 +18,6 @@ function subscribe(prefix) {
scheme = undefined;
});
obs.on("rendered", function (d) {
console.log("rednered");
$(".afx-window-title", scheme).html("Subscribe");
$("[data-id='send']", scheme).click(function () {
var status = $("[data-id='status']", scheme);

View File

@ -73,6 +73,51 @@ body {
justify-content: flex-end;
flex-direction: row;
}
div.d3tooltip {
position: absolute;
padding: 5px;
background-color: #2c2c2c;
color: white;
border: 0px;
border-radius: 5px;
max-width: 300px;
overflow: auto;
}
div.d3tooltip a{
color: white;
text-decoration: underline;
}
#floating_container {
position: absolute;
max-width: 50%;
height: calc(100% - 1px);
background-color: #2c2c2c;
opacity: 0.9;
right: 0;
overflow:hidden;
}
#floating_content
{
overflow-y:auto;
height: 100%;
padding-left: 15px;
padding-right: 15px;
}
#floating_btn_container
{
display: block;
height: 24px;
padding: 5px;
font-weight: bold;
border-bottom: 1px solid #cccccc;
}
#floating_btn_container a {
text-decoration: none;
margin-right: 10px;
}
#floating_container, #floating_container a{
color: white;
}
#navbar.navmobile {
margin: 0 auto;
max-width: 960px;

View File

@ -88,6 +88,43 @@ function PostController:bytag(b64tag, limit, action, id)
return true
end
function PostController:json(id)
local obj = {
error = false,
result = false
}
local data, order = self.blog:fetch({["="] = {id = id}})
if not data or #order == 0 then
obj.error = "No data found"
else
data = data[1]
obj.result = {
id = data.id,
title = data.title,
description = nil,
tags = data.tags,
ctime = data.ctimestr,
utime = data.utimestr
}
local c, d = data.content:find("%-%-%-%-%-")
if c then
obj.description = data.content:sub(0, c - 1)
else
obj.description = data.content
end
-- convert description to html
local content = ""
local md = require("md")
local callback = function(s) content = content .. s end
md.to_html(obj.description, callback)
obj.result.description = content
end
std.json()
std.t(JSON.encode(obj));
return false;
end
function PostController:id(pid)
local data, order = self.blog:fetch({["="] = {id = pid}})
if not data or #order == 0 then
@ -130,6 +167,52 @@ function PostController:actionnotfound(...)
return self:notfound("Action [" .. args[1] .. "] not found")
end
function PostController:graph_json(...)
local nodes = self.blog:find({exp= { ["="] = { publish = 1}}, fields = {"id", "title"}})
local output = { error = false, result = false }
local lut = {}
std.json()
if not nodes then
output.error = "No nodes found"
else
output.result = {
nodes = {},
links = {}
}
for k,v in ipairs(nodes) do
local title = v.title
output.result.nodes[k] = { id = tonumber(v.id), title = title }
end
-- get statistic links
local links = self.analytical:find({fields = {"pid", "sid", "score"}})
if links then
local i = 1
for k,v in ipairs(links) do
local link = { source = tonumber(v.pid), target = tonumber(v.sid), score = tonumber(v.score)}
local key = ""
if link.source < link.target then
key = v.pid..v.sid
else
key = v.sid..v.pid
end
key = std.sha1(key)
if not lut[key] then
output.result.links[i] = link
i = i + 1
lut[key] = true
end
end
end
end
std.t(JSON.encode(output))
return false
end
function PostController:graph(...)
self.template:set("title", "Posts connection graph")
self.template:set("d3", true)
return true
end
function PostController:analyse(n)
if not n then
n = 5

View File

@ -27,7 +27,7 @@ function BlogModel:fetch(cnd, limit, order)
exp = {["and"] = exp },
order = { ctime = "DESC" },
fields = {
"id", "title", "utime", "ctime", "utimestr", "ctimestr", "rendered", "tags"
"id", "title", "utime", "ctime", "utimestr", "content", "ctimestr", "rendered", "tags"
}
}
if limit then

View File

@ -5,6 +5,7 @@
local render = __main__:get("render")
local url = __main__:get("url")
local tags = __main__:get("tags")
local d3 = __main__:get("d3")
local cls = ""
if HEADER.mobile then
cls = "navmobile"
@ -29,6 +30,14 @@
<script src="<?=HTTP_ROOT?>/rst/afx.js"> </script>
<script src="<?=HTTP_ROOT?>/rst/gscripts/jquery-3.2.1.min.js"> </script>
<script src="<?=HTTP_ROOT?>/assets/main.js"></script>
<?lua if d3 then ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js" ></script>
<script src="https://d3js.org/d3-dispatch.v2.min.js"></script>
<script src="https://d3js.org/d3-quadtree.v2.min.js"></script>
<script src="https://d3js.org/d3-timer.v2.min.js"></script>
<script src="https://d3js.org/d3-force.v2.min.js"></script>
<script src="<?=HTTP_ROOT?>/assets/graph.js"></script>
<?lua end ?>
<meta property="og:image" content="" />
<?lua if render then ?>
<meta name="twitter:card" content="summary" />
@ -116,14 +125,14 @@
<div class = "logo"><a href = "https://lxsang.me"></a></div>
<ul>
<li><i class = "fa fa-home"></i><a href="<?=HTTP_ROOT?>">Home</a></li>
<li ><i class = "fa fa-address-card"></i><a href="https://info.lxsang.me" >Portfolio</a></li>
<li><i class = "fa fa-envelope"></i><a href="#" onclick="mailtoMe('<?=HTTP_ROOT?>')" >Contact</a></li>
<?lua
if not HEADER.mobile then
?>
<li > <i class = "fa fa-globe"></i><a href = "/post/graph">Explore</a></li>
<li> <i class = "fa fa-paper-plane"></i><a href="#" onclick="subscribe('<?=HTTP_ROOT?>')">Subscribe</a></li>
<li > <i class = "fa fa-globe"></i><a href = "https://os.lxsang.me" target="_blank">AntOS</a></li>
<?lua end ?>
<li ><i class = "fa fa-address-card"></i><a href="https://info.lxsang.me" >Portfolio</a></li>
<li><i class = "fa fa-envelope"></i><a href="#" onclick="mailtoMe('<?=HTTP_ROOT?>')" >Contact</a></li>
</ul>
<?lua
if not HEADER.mobile then

View File

@ -0,0 +1,28 @@
<div id="floating_container">
<div id="floating_btn_container">
<i class="fa fa-close"></i>
<a id="floating_btn_close" href="#" >Close</a>
<i class="fa fa-chain"></i>
<a id="floating_btn_read_more" href="#">Read more</a>
</div>
<div id="floating_content">
<p>
The graph shows this blog posts relationship in term of similarity.
Each node in the graph is a post, two nodes are connected by an edge if
they share some degree of similarity (weighted by edge thickness and edge distance).
A large edge thickness and/or short edge distance shows a strong similarity between
the two connected nodes.
</p>
<p>
Nodes are arranged by force which is modelled by content similarity.
The more similar is the nodes content, the stronger is the force between them.
Therefore, nodes that share similar topic will tend to group themself together in a cluster.
</p>
<p>
Navigate the blog by hovering the mouse on a node and following the node relationship
(edges) to find your interesting topic.
</p>
</div>
</div>