mirror of
https://github.com/lxsang/antd-web-apps
synced 2024-12-25 17:08:22 +01:00
add explore mode to blog
This commit is contained in:
parent
bf52f93e27
commit
d416ae36c8
218
blog/assets/graph.js
Normal file
218
blog/assets/graph.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
28
blog/views/default/post/graph.ls
Normal file
28
blog/views/default/post/graph.ls
Normal 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>
|
Loading…
Reference in New Issue
Block a user