Skip to content

Instantly share code, notes, and snippets.

@1Cr18Ni9
Last active March 16, 2018 09:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1Cr18Ni9/e915d9aea9c895b10f0c43be97db7488 to your computer and use it in GitHub Desktop.
Save 1Cr18Ni9/e915d9aea9c895b10f0c43be97db7488 to your computer and use it in GitHub Desktop.
Gene structure display
license: mit

Built with blockbuilder.org

features:

  1. A pie of genome will be divided by the variant opt.division into blocks. Every block containe one or more tiers, each tier will have some genome segements. If it detects segements collision with adjacent one, a new tier will be generated to accommodate the clinch one;

  2. Text will not collision with each other using collision detection;

  3. When Mouse hover on one segement that will prompt a tooltip with detail information.

{
"title":"Gene Graph",
"division":10000,
"range":[4345692,4419496],
"size":[1200,500],
"segments":[
{
"from":4345192,
"name":"-",
"color":"#6A5ACD",
"to":4346277,
"url":"http://www.bing.com/",
"id":"orf4009"
},
{"from":4346516,"name":"rsuA","color":"#800080","to":4347301,"url":"http://www.bing.com/","id":"orf4010"},{"from":4347606,"name":"","color":"#6A5ACD","to":4347944,"url":"http://www.bing.com/","id":"orf4011"},{"from":4348738,"name":"","color":"#696969","to":4347947,"url":"http://www.bing.com/","id":"orf4012"},{"from":4348994,"name":"","color":"#008000","to":4350388,"url":"http://www.bing.com/","id":"orf4013"},{"from":4351167,"name":"","color":"#6A5ACD","to":4352801,"url":"http://www.bing.com/","id":"orf4014"},{"from":4353393,"name":"atoD","color":"#808000","to":4354109,"url":"http://www.bing.com/","id":"orf4015"},{"from":4354118,"name":"scoB","color":"#808000","to":4354774,"url":"http://www.bing.com/","id":"orf4016"},{"from":4357190,"name":"-","color":"#6A5ACD","to":4354947,"url":"http://www.bing.com/","id":"orf4017"},{"from":4357888,"name":"-","color":"#008000","to":4357190,"url":"http://www.bing.com/","id":"orf4018"},{"from":4358408,"name":"","color":"#6A5ACD","to":4359124,"url":"http://www.bing.com/","id":"orf4019"},{"from":4359808,"name":"ilvH","color":"#FF6347","to":4359560,"url":"http://www.bing.com/","id":"orf4020"},{"from":4361540,"name":"ilvB","color":"#FF6347","to":4359858,"url":"http://www.bing.com/","id":"orf4021"},{"from":4362151,"name":"","color":"#FFFFFF","to":4362270,"url":"http://www.bing.com/","id":"orf4022"},{"from":4362435,"name":"uup","color":"#00BFFF","to":4364375,"url":"http://www.bing.com/","id":"orf4023"},{"from":4364444,"name":"","color":"#FFFFFF","to":4364653,"url":"http://www.bing.com/","id":"orf4024"},{"from":4364650,"name":"","color":"#696969","to":4365255,"url":"http://www.bing.com/","id":"orf4025"},{"from":4365300,"name":"","color":"#696969","to":4367885,"url":"http://www.bing.com/","id":"orf4026"},{"from":4367919,"name":"mgtC","color":"#6A5ACD","to":4368626,"url":"http://www.bing.com/","id":"orf4027"},{"from":4369053,"name":"","color":"#6A5ACD","to":4369499,"url":"http://www.bing.com/","id":"orf4028"},{"from":4369942,"name":"","color":"#8FBC8F","to":4370355,"url":"http://www.bing.com/","id":"orf4029"},{"from":4370455,"name":"bfd","color":"#6A5ACD","to":4370634,"url":"http://www.bing.com/","id":"orf4030"},{"from":4371027,"name":"","color":"#6A5ACD","to":4371995,"url":"http://www.bing.com/","id":"orf4031"},{"from":4371995,"name":"panB","color":"#8B4513","to":4372822,"url":"http://www.bing.com/","id":"orf4032"},{"from":4373215,"name":"panC","color":"#8B4513","to":4374057,"url":"http://www.bing.com/","id":"orf4033"},{"from":4374278,"name":"","color":"#FFFFFF","to":4374132,"url":"http://www.bing.com/","id":"orf4034"},{"from":4374426,"name":"panD","color":"#8B4513","to":4374800,"url":"http://www.bing.com/","id":"orf4035"},{"from":4375005,"name":"","color":"#FFFFFF","to":4375301,"url":"http://www.bing.com/","id":"orf4036"},{"from":4375529,"name":"","color":"#FFFFFF","to":4375816,"url":"http://www.bing.com/","id":"orf4037"},{"from":4375974,"name":"","color":"#6A5ACD","to":4377707,"url":"http://www.bing.com/","id":"orf4038"},{"from":4377720,"name":"","color":"#FFFFFF","to":4377884,"url":"http://www.bing.com/","id":"orf4039"},{"from":4377871,"name":"","color":"#FFFFFF","to":4378221,"url":"http://www.bing.com/","id":"orf4040"},{"from":4378226,"name":"","color":"#FFFFFF","to":4378546,"url":"http://www.bing.com/","id":"orf4041"},{"from":4378902,"name":"rfbB","color":"#FF00FF","to":4379888,"url":"http://www.bing.com/","id":"orf4042"},{"from":4380026,"name":"","color":"#00BFFF","to":4380664,"url":"http://www.bing.com/","id":"orf4043"},{"from":4380655,"name":"neuB","color":"#FF00FF","to":4381659,"url":"http://www.bing.com/","id":"orf4044"},{"from":4381675,"name":"","color":"#6A5ACD","to":4382622,"url":"http://www.bing.com/","id":"orf4045"},{"from":4382612,"name":"","color":"#FF00FF","to":4383787,"url":"http://www.bing.com/","id":"orf4046"},{"from":4383780,"name":"","color":"#6A5ACD","to":4384451,"url":"http://www.bing.com/","id":"orf4047"},{"from":4384732,"name":"","color":"#FF00FF","to":4385880,"url":"http://www.bing.com/","id":"orf4048"},{"from":4385901,"name":"","color":"#FF00FF","to":4386947,"url":"http://www.bing.com/","id":"orf4049"},{"from":4387053,"name":"legF","color":"#FF00FF","to":4387766,"url":"http://www.bing.com/","id":"orf4050"},{"from":4388030,"name":"","color":"#FFFFFF","to":4388974,"url":"http://www.bing.com/","id":"orf4051"},{"from":4389243,"name":"","color":"#00BFFF","to":4390673,"url":"http://www.bing.com/","id":"orf4052"},{"from":4393090,"name":"","color":"#696969","to":4391942,"url":"http://www.bing.com/","id":"orf4053"},{"from":4394783,"name":"xylB","color":"#FFFF00","to":4393281,"url":"http://www.bing.com/","id":"orf4054"},{"from":4396556,"name":"xylA","color":"#FFFF00","to":4395231,"url":"http://www.bing.com/","id":"orf4055"},{"from":4398275,"name":"xylH","color":"#FFFF00","to":4397106,"url":"http://www.bing.com/","id":"orf4056"},{"from":4399854,"name":"xylG","color":"#FFFF00","to":4398277,"url":"http://www.bing.com/","id":"orf4057"},{"from":4400998,"name":"xylF","color":"#FFFF00","to":4399919,"url":"http://www.bing.com/","id":"orf4058"},{"from":4402628,"name":"yesN","color":"#EE82EE","to":4401291,"url":"http://www.bing.com/","id":"orf4059"},{"from":4403970,"name":"","color":"#EE82EE","to":4402609,"url":"http://www.bing.com/","id":"orf4060"},{"from":4405075,"name":"rbsB","color":"#FFFF00","to":4404095,"url":"http://www.bing.com/","id":"orf4061"},{"from":4405770,"name":"","color":"#800000","to":4405555,"url":"http://www.bing.com/","id":"orf4062"},{"from":4406301,"name":"","color":"#696969","to":4405939,"url":"http://www.bing.com/","id":"orf4063"},{"from":4406381,"name":"pcaC","color":"#6A5ACD","to":4406746,"url":"http://www.bing.com/","id":"orf4064"},{"from":4407549,"name":"","color":"#6A5ACD","to":4407004,"url":"http://www.bing.com/","id":"orf4065"},{"from":4408308,"name":"","color":"#800080","to":4407799,"url":"http://www.bing.com/","id":"orf4066"},{"from":4409186,"name":"","color":"#00BFFF","to":4408329,"url":"http://www.bing.com/","id":"orf4067"},{"from":4409656,"name":"","color":"#6A5ACD","to":4409222,"url":"http://www.bing.com/","id":"orf4068"},{"from":4410398,"name":"rpiA","color":"#FFFF00","to":4409706,"url":"http://www.bing.com/","id":"orf4069"},{"from":4411700,"name":"","color":"#32CD32","to":4411098,"url":"http://www.bing.com/","id":"orf4070"},{"from":4412932,"name":"","color":"#FFFF00","to":4412141,"url":"http://www.bing.com/","id":"orf4071"},{"from":4414109,"name":"araM","color":"#8FBC8F","to":4412937,"url":"http://www.bing.com/","id":"orf4072"},{"from":4416173,"name":"-","color":"#6A5ACD","to":4414221,"url":"http://www.bing.com/","id":"orf4073"},{"from":4416363,"name":"","color":"#6A5ACD","to":4417259,"url":"http://www.bing.com/","id":"orf4074"},{"from":4417570,"name":"","color":"#696969","to":4418385,"url":"http://www.bing.com/","id":"orf4075"},{"from":4420013,"name":"abfA","color":"#FFFF00","to":4418496,"url":"http://www.bing.com/","id":"orf4076"}
]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<link rel="stylesheet" type="text/css" href="myCSS.css">
</head>
<body>
<div id="container"></div>
<script>
d3.svg.geneStructure = function (id, options) {
var opt = {
title : "",
size : [1200, 500],
division: 20000,
margin : { top: 60, right: 50, bottom: 20, left: 50 }
};
_.extend(opt, options);
// validation
var err = [];
(function(){
function inRange(e){
return (e.from < opt.range[0] && e.to < opt.range[0]) ||
(e.from > opt.range[1] && e.to > opt.range[1]);
}
if(!_.isArray(opt.segments)) {
opt.segments = [];
alert("\"opt.segments\" MUST be an Array!");
return;
}
opt.segments.forEach(function(d){
if(inRange(d)){ err.push(d); }
});
if(opt.size[0] < 650){ opt.size[0] = 650; }
})();
if(err.length){
var json = JSON.stringify(err.map(function(d){
return { id: d.id, from: d.from, to: d.to };
}), null, " ");
console.error("Follow segment(s) are out of range([" + opt.range + "]): \n" + json);
return;
}
var innerWidth = opt.size[0] - (opt.margin.left + opt.margin.right),
// Metric prefix
tickSI = d3.format("s"),
scale = d3.scale.linear().domain([0, 100]).range([0, innerWidth]);
// stripHeight + text-up-height + text-down-height = rowHeight
// axisHeight + rowHeight * n == blockHeight
var axisHeight = 30, rowHeight = 55, stripHeight = 15;
// data to be bind
var blocks = [];
// kind of scale for mapping domain and physic distance
var factor = innerWidth / Math.min(opt.division, opt.range[1] - opt.range[0]);
var tooltip = document.getElementById("extra-tip");
if(!tooltip){
tooltip = d3.select("body")
.append("div")
.attr({id: "extra-tip", class: "extra-tip"})
.html('<div class="extra-tip-content"></div><div class="extra-tip-arrow"><div class="extra-tip-wrapper"></div></div>')
.node();
}
// Anonymous function for data processing
(function(){
var span = opt.range[1] - opt.range[0];
var rowCount = Math.ceil(span / opt.division);
// srot all data before processing
var sortData = opt.segments.map(function(d){
var from;
if((d.from - d.to) < 0){
return d;
} else {
from = d.from;
d.from = d.to;
d.to = from;
// arrow point from Right To Left
d._rtl = true;
return d;
}
}).sort(function(a, b){
return a.from - b.from;
});
// rowCount greater than or equal to 2
function gte2(){
var ii = opt.range[0], rowRanges = [], acc = 0, rBlocks = [];
while(ii < opt.range[1]){
rowRanges.push([ii, Math.min(ii + opt.division, opt.range[1])]);
ii += opt.division;
}
rBlocks = _.groupBy(sortData, function(d){
return _.findIndex(rowRanges, function(j){
return d.from >= j[0] && d.from <= j[1];
});
});
// If the block is empty emplemented with empty array
ii = 0;
while(ii < rowRanges.length){
if(!rBlocks[ii + ""]){ rBlocks[ii + ""] = []; }
ii++;
}
// If the first segement is out of range,
// unshift it to the first tier.
if("-1" in rBlocks){
rBlocks["0"].unshift(rBlocks["-1"][0]);
delete rBlocks["-1"];
}
// Detecting if tier's last element is out of its domain.
// if it does, duplicate it and move the new one to the next tier
_.each(rBlocks, function(val, index){
var lastStrip = _.last(val);
if(lastStrip && (lastStrip.to > rowRanges[index][1]) && rBlocks[+index + 1]){
rBlocks[+index + 1].unshift(lastStrip);
}
});
return _.map(rBlocks, function(val, key){
var tiers = createTier(val),
y = axisHeight + tiers.length * rowHeight,
strip = {
tiers : tiers,
domain: rowRanges[key],
y : y,
_y0 : acc
};
acc = acc + y;
return strip;
});
} // gte2 END
// rowCount equals to 1
function eq1(){
var tiers = createTier(sortData);
return [{
tiers : tiers,
domain: opt.range,
y : axisHeight + tiers.length * rowHeight,
_y0 : 0
}];
}
if(rowCount > 1){
blocks = gte2();
}else if(rowCount === 1){
blocks = eq1();
}
})(); // Anonymous function END
// Drawding
var svg = d3.select("#" + id)
.html("")
.append("svg")
.attr({
width : opt.size[0],
height: _.last(blocks).y + _.last(blocks)._y0 +
opt.margin.top + opt.margin.bottom,
xmlns : "http://www.w3.org/2000/svg"
})
.style("font", "12px arial, sans-serif");
var clips;
(function(){
var domain = _.last(blocks).domain;
var width = factor * (domain[1] - domain[0]);
clips = [
{id: "L-" + (new Date()).getTime(), width:innerWidth},
{id: "S-" + (new Date()).getTime(), width:width},
];
svg.append("defs")
.selectAll("clipPath")
.data(clips)
.enter()
.append("clipPath")
.attr("id", function(d){ return d.id; })
.append("rect")
.attr({
x : 0,
y : 0,
width : function(d){ return d.width; },
height: rowHeight
});
})();
svg
.append("g")
.attr("class", "gMain")
.attr("transform", "translate(" + [opt.margin.left, opt.margin.top] + ")")
.selectAll("gBlock")
.data(blocks)
.enter()
.append("g")
.attr("class", "gBlock")
.attr("transform", function(d){ return "translate(0," + d._y0 + ")"; })
.each(function(d){
var self = d3.select(this),
range = factor * (d.domain[1] - d.domain[0]);
var xScale = scale.copy()
.domain(d.domain)
.range([0, range]),
xAxis = d3.svg.axis()
.orient("top")
.scale(xScale)
.ticks(Math.round(range / innerWidth * 10))
.tickFormat(tickSI);
self.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0," + axisHeight + ")")
.attr("shape-rendering", "crispEdges")
.call(xAxis)
.call(function(s){
s.select("path").attr({fill: "none", stroke: "black", "stroke-width": 1});
s.selectAll("line").attr({stroke: "black", "stroke-width": 1});
});
self.selectAll(".gTier")
.data(d.tiers)
.enter()
.append("g")
.attr("class", "gTier")
.attr("clip-path", "url(#" +
(range < innerWidth ? clips[1].id : clips[0].id) + ")")
.attr("transform", function(j, i){
return "translate(0," + (rowHeight * i + axisHeight) + ")";
})
.selectAll("gStrip")
.data(function(j){ return j; })
.enter()
.append("g")
.attr("class", "gStrip")
.attr("transform", function(j){
// if no segment available in this tier
if(!j){ return "translate(0,0)"; }
return "translate(" + xScale(j.from) + ",0)";
})
.each(function(j){
// if no segment available in this tier just return
if(!j){ return; }
var strip = d3.select(this);
strip.append("a")
.attr({ href: j.url, target: "_blank" })
.append("path")
.on("mouseenter", onMouseEnter)
.on("mouseleave", onMouseLeave)
.style("cursor", "pointer")
.attr({
d : dPath(factor, stripHeight),
fill : j.color,
transform : "translate(0," + 20 + ")",
"stroke-width": 1,
stroke: d3.rgb(j.color).toString() === "#ffffff" ? "black" : "none"
});
strip.append("text")
.text(j.id)
.attr({
transform: "translate(" + [0.5 * factor * (j.to - j.from), 15] + ")",
"text-anchor": "middle",
class: "up"
});
strip.append("text")
.text(j.name)
.attr({
transform: "translate(" +
[0.5 * factor * (j.to - j.from), 15 * 2 + stripHeight + 5] + ")",
class: "down",
"text-anchor": "middle"
});
}); // Inner EACH Loop
}); // Outer EACH Loop
// Main title
svg.append("text")
.text(("title" in opt) ? opt.title : "")
.style("font", "18px arial, sans-serif")
.attr({
x : opt.size[0] * 0.5,
y : 30,
"text-anchor" : "middle"
});
/*
Detecting & Clearing Text collisions.
Depending on the SVG canvas is rendered or not.
If it's not rendered collisions can not be cleared.
*/
try {
var svgBox = svg.node().getBoundingClientRect();
var axisBox = svg.select(".gBlock:last-child")
.select(".xAxis path")
.node()
.getBoundingClientRect();
svg.selectAll(".gTier").each(function(d){
var self = d3.select(this),
domain = d3.select(this.parentNode).datum().domain[1],
bounding;
if(domain < opt.range[1]){
bounding = [svgBox.x + opt.margin.left,
svgBox.x + opt.margin.left + innerWidth];
} else {
bounding = [axisBox.x, axisBox.x + axisBox.width];
}
clearCollision(self.selectAll("text.up")[0], bounding);
clearCollision(self.selectAll("text.down")[0], bounding);
});
} catch (e) {
//console.log("Oops, the SVG canvas is not rendered.");
console.log(e);
}
// Events
function onMouseEnter(d){
var x = d3.event.clientX,
y = d3.event.clientY,
rx = d3.mouse(this)[0],
ry = d3.mouse(this)[1],
self = d3.select(this),
html = "<table>" +
"<tr><td><strong>ID: </strong></td><td>" + d.id + "</td></tr>" +
((("name" in d) && d.name.length) ?
("<tr><td><strong>Name: </strong></td><td>" + d.name + "</td></tr>") : "") +
"<tr><td><strong>From: </strong></td><td>" + d.from + "</td></tr>" +
"<tr><td><strong>To: </strong></td><td>" + d.to + "</td></tr>" +
"</table>";
var outBox = svg.node().getBoundingClientRect(),
calculateX;
d3.select(tooltip).select("div:first-child").html(html);
self.attr({stroke: "black", fill: "red"});
// make sure the tooltip is always flooting on SVG's domain
calculateX = x - 0.5 * (tooltip.offsetWidth - (factor * (d.to - d.from))) - rx;
if(calculateX < outBox.x){ calculateX = outBox.x; }
if(calculateX > ((outBox.x + outBox.width) - tooltip.offsetWidth)){
calculateX = (outBox.x + outBox.width) - tooltip.offsetWidth;
}
tooltip.style.left = calculateX + "px";
tooltip.style.top = y - tooltip.offsetHeight - ry - 12 + "px";
}
function onMouseLeave(d){
d3.select(this).attr({
stroke: d3.rgb(d.color).toString() === "#ffffff" ? "black" : "transparent",
fill: d.color
});
tooltip.style.left = "-999999999999px";
}
/*
Text collision:
texts: An array of SVGTextElement
bounding: An array of two herizontal limitation values
*/
function clearCollision(texts, bounding){
var pText, nText, pBox, nBox, ii = 0;
if(texts.length < 1){ return; }
if(texts.length == 1){ clearTouchBounding(texts[0], bounding); return; }
pText = texts[0];
nText = texts[1];
while(ii < (texts.length - 1)){
pBox = pText.getBoundingClientRect();
nBox = nText.getBoundingClientRect();
// The text will be hide if it touch the inner bounding of gMain.
if((pBox.x < bounding[0]) || (pBox.x + pBox.width > bounding[1])){
d3.select(pText).style("display", "none");
}
if((nBox.x < bounding[0]) || (nBox.x + nBox.width > bounding[1])){
d3.select(nText).style("display", "none");
}
// if present text(pText) clinched with adjacent text(nText),
// the late one will be invisible({display: none})
if((pBox.x + pBox.width) > nBox.x){
d3.select(nText).style("display", "none");
}else{
pText = nText;
}
nText = texts[ii + 2];
ii++;
}
function clearTouchBounding(text, bounding){
var box = text.getBoundingClientRect();
if((box.x < bounding[0]) || (box.x + box.width > bounding[1])){
d3.select(text).style("display", "none");
}
}
}
// Path generator
function dPath(factor, height) {
if (!factor) { factor = 1; }
if (!height) { height = 20; }
return function (d) {
var width = factor * (d.to - d.from),
arrWidth = 10;
if ((width <= arrWidth) && d._rtl) {
return "M0" + "," + (0.5 * height) +
" L" + width + ",0" +
" V" + height + " Z";
} else if ((width > arrWidth) && d._rtl) {
return "M0" + "," + (0.5 * height) +
" L" + arrWidth + ",0" +
" H" + width +
" V" + height +
" H" + arrWidth + " Z";
} else if ((width <= arrWidth) && !d._rtl) {
return "M0,0" +
" L" + width + "," + (height * 0.5) +
" L0," + height + " Z";
} else if ((width > arrWidth) && !d._rtl) {
return "M0,0" +
" H" + (width - arrWidth) +
" L" + width + "," + (0.5 * height) +
" L" + (width - arrWidth) + "," + height +
" H0" + " Z";
}
};
} // gPath End
function createTier(segments) {
function recursive(segments) {
var currentTier = [],
nextTier = [],
ii = 0,
aa, bb;
// segments.length == 0
if (segments.length === 0) {
arr.push([null]);
return;
}
// segments.length == 1
if (segments.length === 1) {
arr.push(segments);
return;
}
// segments.length >= 2
aa = segments[0];
bb = segments[1];
currentTier.push(aa);
while (ii < (segments.length - 1)) {
if (bb.from < aa.to) {
nextTier.push(bb);
} else {
currentTier.push(bb);
aa = bb;
}
bb = segments[ii + 2];
ii++;
} // WHILE End
arr.push(currentTier);
if (nextTier.length >= 1) { recursive(nextTier); }
} // recursive End
var arr = [];
recursive(segments);
return arr;
} // createTier End
}; // d3.svg.geneStructure END
d3.json("gene3.json", function(error, data){
if(error){ return console.log(error); }
d3.svg.geneStructure("container", data);
});
d3.select(self.frameElement).style({width: 1200 + "px",height: 750 + "px"});
</script>
</body>
*{margin: 0; padding: 0;}
.extra-tip{
position: fixed;
max-width: 200px;
left: -9999px;
background-color: white;
border: 1px solid gray;
border-radius: 5px;
font: 12px;
overflow-wrap: break-word;
word-wrap: break-word;
}
.extra-tip > .extra-tip-content{
padding: 5px;
}
.extra-tip .extra-tip-content a{
text-decoration: none;
}
.extra-tip > .extra-tip-arrow{
position: absolute;
height: 10px;
width: 100%;
top: 100%;
/* indicate area when needed */
background-color: rgba(200,100,0,0);
}
.extra-tip .extra-tip-wrapper{
position: absolute;
left: 50%;
}
.extra-tip .extra-tip-wrapper:before,
.extra-tip .extra-tip-wrapper:after{
content: " ";
width: 0;
height: 0;
top: 100%;
position: absolute;
border: solid transparent;
pointer-events: none;
}
.extra-tip .extra-tip-wrapper:before{
border-width: 10px;
border-top-color: gray;
left: -8px;
}
.extra-tip .extra-tip-wrapper:after{
border-width: 8px;
border-top-color: white;
left: -6px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment