Last active
January 25, 2018 17:38
-
-
Save timproDev/39ad3846b38069cd2bc8e1b488cb3cdd to your computer and use it in GitHub Desktop.
Bar Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var d3Config = d3Config || {}; | |
d3Config.init = function(settings) { | |
console.log("Configing!!"); | |
d3Config.linkStyle(settings); | |
if (typeof settings.win.d3 === "undefined") { | |
console.log("lib undefined"); | |
d3Config.loadLib(settings); | |
} | |
if (typeof settings.win.d3 !== "undefined") { | |
console.log("lib defined"); | |
d3Config.loadComp(settings); | |
} | |
} | |
d3Config.linkStyle = function(params){ | |
var linkElem = document.createElement('link'); | |
document.getElementsByTagName('head')[0].appendChild(linkElem); | |
linkElem.rel = 'stylesheet'; | |
linkElem.type = 'text/css'; | |
linkElem.href = (params.path + params.css); | |
} | |
d3Config.loadLib = function(params){ | |
$.when( | |
$.getScript((params.path + params.lib)), | |
$.Deferred(function( deferred ){ | |
$( deferred.resolve ); | |
}) | |
).done(function(){ | |
console.log("lib defined"); | |
d3Config.loadComp(params); | |
}); | |
} | |
d3Config.loadComp = function(params){ | |
$.when( | |
$.getScript((params.path + params.comp)), | |
$.Deferred(function( deferred ){ | |
$( deferred.resolve ); | |
}) | |
).done(function(){ | |
params.win[params.func](params.path, params.data, params.elem, params.options); | |
d3Config.chartRefire(params); | |
}); | |
} | |
d3Config.chartRefire = function(params){ | |
var resizeTimer; | |
$(params.win).on('resize', function(e) { | |
clearTimeout(resizeTimer); | |
resizeTimer = setTimeout(function() { | |
params.win[params.func](params.path, params.data, params.elem, params.options); | |
}, 300); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html class="no-js" lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta http-equiv="x-ua-compatible" content="ie=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Flood Data Barchart</title> | |
<link href="https://fonts.googleapis.com/css?family=Quicksand" rel="stylesheet"> | |
<link rel="stylesheet" type="text/css" href="m-flood-barc.css"> | |
<script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script> | |
</head> | |
<body> | |
<section style="width:80%; margin: 0 auto;"> | |
<div class="mx-dv-container" id="cBarChart"> | |
<h3 class="chart-title">U.S. Natural Catastrophe Losses 2019</h3> | |
<div class="chart-premise"></div> | |
<div class="plot-wrap w-svg"></div> | |
<div class="plot-wrap no-svg"> | |
<img src="barchart.png"> | |
</div> | |
</div> | |
</section> | |
<script type="text/javascript"> | |
document.addEventListener("DOMContentLoaded", function(){ | |
var d3settings = { | |
win: window, | |
path:"", | |
css: "m-flood-barc.css", | |
lib: "d3v4-473-jetpack.js", | |
comp: "m-flood-barc.js", | |
data: "nat-cat.csv", | |
elem: "cBarChart", | |
func: "mfBarChart", | |
options:{ | |
axisCategory:"Catastrophe", | |
axisValue:"Total losses", | |
rangeMax:false, | |
chartHeight:250, | |
showDataOpt:false | |
} | |
}; | |
d3Config.init(d3settings); | |
}); | |
</script> | |
<script src="d3-config.js"></script> | |
<script src="https://rawgit.com/timproDev/d3-first-timer/master/js/d3v4-473-jetpack.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Global Styles and resets */ | |
@media only screen and (min-width: 641px) { | |
body, html { | |
overflow-x: hidden; } } | |
@media only screen and (max-width: 40em) { | |
html { | |
-webkit-text-size-adjust: 100%; } } | |
body { | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
font-family: 'Quicksand', Arial, sans-serif; | |
height: auto; | |
display: block; | |
background-color: #fcfcfc; } | |
article, | |
aside, | |
details, | |
figcaption, | |
figure, | |
footer, | |
header, | |
hgroup, | |
main, | |
nav, | |
section, | |
summary { | |
display: block; } | |
img { | |
border: none; } | |
button, | |
input, | |
optgroup, | |
select, | |
textarea { | |
color: inherit; | |
/* 1 */ | |
font: inherit; | |
/* 2 */ | |
margin: 0; | |
/* 3 */ | |
-webkit-font-smoothing: antialiased; } | |
input, button, select { | |
outline: none; } | |
*, | |
*:before, | |
*:after { | |
box-sizing: border-box; } | |
.prm-box, .tooltip { | |
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.5); | |
border-radius: 2px; | |
background-color: #fff; } | |
.chart-container-linedot-table, .chart-container-table { | |
margin-top: 1rem; } | |
.chart-container-linedot-table table, .chart-container-table table { | |
width: 100%; | |
background-color: #fff; } | |
.chart-table-container { | |
margin-top: 1rem; } | |
.chart-table-container table { | |
width: 100%; | |
background-color: #fff; } | |
.mx-dv-container { | |
text-align: center; | |
margin: 0 0 2rem 0; | |
background-color: transparent; | |
border-top: 6px solid #f5f5f5; | |
border-bottom: 6px solid #f5f5f5; | |
font-weight: normal; | |
font-size: 16px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
/* IE HACK CANVAS */ } | |
.mx-dv-container .plot-wrap.no-svg { | |
display: none; | |
padding-bottom: 1rem; } | |
.mx-dv-container .plot-wrap.no-svg img { | |
width: 100%; | |
max-width: 706px; } | |
.mx-dv-container .svg-container { | |
position: relative; } | |
.mx-dv-container .svg-container canvas { | |
display: block; | |
height: 100%; | |
visibility: hidden; } | |
.mx-dv-container .svg-container svg { | |
height: 100%; | |
left: 0; | |
position: absolute; | |
top: 0; | |
width: 100%; } | |
.mx-dv-container .chart-title { | |
font-weight: normal; | |
font-size: 25px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
padding: 1.15rem 0 0 0; | |
margin: 0; | |
text-align: left; } | |
.mx-dv-container .chart-premise, .mx-dv-container .chart-citation { | |
font-weight: normal; | |
font-size: 16px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
color: #808080; | |
padding: .25rem 0 1rem 0; | |
margin: 0; | |
text-align: left; } | |
.mx-dv-container .chart-premise p, .mx-dv-container .chart-premise input, .mx-dv-container .chart-citation p, .mx-dv-container .chart-citation input { | |
margin: 0; | |
text-align: left; | |
display: inline-block; | |
vertical-align: middle; | |
padding: 0 .75rem .25rem 0; } | |
.mx-dv-container .chart-citation { | |
font-weight: normal; | |
font-size: 13px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
color: #bdbdbd; } | |
.mx-dv-container .raw-wrap { | |
text-align: right; | |
padding: 0 1.5rem 0 0; | |
margin: 0; } | |
.mx-dv-container .raw-wrap .raw-btn { | |
font-weight: normal; | |
font-size: 14px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
cursor: pointer; | |
margin: 0 0 .75rem 0; | |
padding: 0; } | |
.tooltip { | |
top: -1000px; | |
position: fixed; | |
padding: .65rem; | |
pointer-events: none; | |
max-width: 25%; | |
z-index: 500; } | |
.tooltip-info { | |
color: #45555f; | |
font-weight: normal; | |
font-size: 16px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
padding: 0; | |
margin: 0; | |
display: block; | |
width: 100%; } | |
.tooltip-info span { | |
font-weight: normal; | |
font-size: 16px; | |
font-family: 'Quicksand', Arial, sans-serif; } | |
.tooltip-hidden { | |
opacity: 0; | |
transition: all .3s; | |
transition-delay: .1s; } | |
.dnld-btn { | |
display: block; | |
width: 100%; | |
margin-top: -1.25rem; | |
padding: .75rem 1.25rem; | |
text-align: right; | |
font-weight: normal; | |
font-size: 14px; | |
font-family: 'Quicksand', Arial, sans-serif; } | |
.dnld-btn:hover { | |
text-decoration: underline; } | |
@media only screen and (max-width: 40em) { | |
.mx-dv-container .plot-wrap.no-svg { | |
display: block; } | |
.mx-dv-container .plot-wrap.w-svg { | |
display: none; } } | |
#cBarChart .domain { | |
display: none; } | |
#cBarChart .comp-rect { | |
transition: opacity 250ms; } | |
#cBarChart .comp-rect.is-hover, #cBarChart .comp-rect.tooltipped { | |
opacity: .65; } | |
#cBarChart .bg-bar { | |
fill: #ececec; } | |
#cBarChart .bar-label { | |
font-weight: bold; | |
font-size: 14px; | |
font-family: 'Quicksand', Arial, sans-serif; } | |
#cBarChart .axis-category-text { | |
font-weight: bold; | |
font-size: 13px; | |
font-family: 'Quicksand', Arial, sans-serif; | |
fill: #404040; | |
font-size: 1rem; | |
text-anchor: end; } | |
#cBarChart .x-axis .tick line { | |
stroke-width: 1; | |
stroke: #45555f; } | |
#cBarChart .chart-premise .btn-wrap { | |
display: inline-block; | |
vertical-align: middle; | |
padding-bottom: .25rem; } | |
#cBarChart .chart-premise .btn-wrap .input-group { | |
display: inline-block; | |
vertical-align: middle; | |
display: inline-block; | |
padding: .3rem .75rem .25rem 0; | |
margin: 0 0.25rem 0 0; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-label { | |
padding: 0 0 0 .5rem; | |
color: #404040; | |
transition: color 250ms; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-radio { | |
cursor: pointer; | |
margin-bottom: 3px; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-radio:checked + .ui-label { | |
color: #006D9E; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-radio[value="Total losses"]:checked + .ui-label { | |
color: #006D9E; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-radio[value="Insured losses"]:checked + .ui-label { | |
color: #D06F1A; } | |
#cBarChart .chart-premise .btn-wrap .input-group .ui-radio[value="Uninsured losses"]:checked + .ui-label { | |
color: #625BC4; } | |
@media only screen and (max-width: 40em) { | |
#cBarChart .chart-premise .btn-wrap { | |
display: none; } } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function mfBarChart(setPath, setData, elem, opts) { | |
var settings = { | |
premise: opts.premise, | |
title: opts.title, | |
axisCategory: opts.axisCategory, | |
axisValue: opts.axisValue, | |
rangeMax: opts.rangeMax, | |
chartHeight: opts.chartHeight, | |
transSpeed: 500, | |
dataPath: setPath + setData, | |
chartContainerID: "#" + elem, | |
labelSplit:3, | |
showDataOpt:opts.showDataOpt | |
}; | |
// horizontal bar margins | |
var margin = { | |
top:20, | |
left:0, | |
right:0, | |
bottom:30 | |
}; | |
var chartContainer = d3.select(settings.chartContainerID); | |
var plotContainer = chartContainer.select(".plot-wrap.w-svg").html(''); | |
var w = plotContainer.node().clientWidth - margin.left - margin.right, | |
h = settings.chartHeight - margin.top - margin.bottom; | |
// h = .7 * width; | |
var premiseDiv = chartContainer.select("div.chart-premise"); | |
// create inner div | |
var div = plotContainer | |
.append("div.svg-container") | |
.style("width", (w + margin.left + margin.right) + "px") | |
.style("height", (h + margin.top + margin.bottom) + "px"); | |
// insert canvas inner div | |
div.insert("canvas"); | |
// append svg in inner div | |
var svg = div | |
.append("svg") | |
.style("width", (w + margin.left + margin.right) + "px") | |
.style("height", (h + margin.top + margin.bottom) + "px") | |
.append("g.wrap") | |
.translate([margin.left,margin.top]); | |
d3.queue() | |
.defer(d3.csv, settings.dataPath) | |
.await(ready); | |
function ready(error, data) { | |
if (error) throw "error: not loading data, bro"; | |
data.forEach(function(d){ | |
d[settings.axisValue] = +d[settings.axisValue]; | |
d["Uninsured losses"] = +d["Total losses"] - +d["Insured losses"]; | |
}); | |
// manipulate data | |
var valArray = data.map(function(d){ | |
return d[settings.axisValue]; | |
}); | |
// if range | |
if (settings.rangeMax == true) { | |
var fullRange = d3.max(valArray); | |
} else { | |
var fullRange = 20; | |
} | |
// ui interaction | |
var optionsArray = d3.keys(data[0]); | |
// Remove Catastrophe, Events and Fatalities keys from UI options | |
optionsArray = optionsArray.filter(function(d){ | |
if (d !== "Catastrophe" && d !== "Events" && d !== "Fatalities") { | |
return d; | |
} | |
}); | |
// var optWrap = premiseDiv.append("div.btn-wrap"); | |
chartContainer.select("div.btn-wrap").remove(); | |
var uiWrap = premiseDiv.append("div.btn-wrap"); | |
var radios = uiWrap | |
.selectAll('div.input-group') | |
.data(optionsArray).enter() | |
.append('div.input-group'); | |
radios | |
.append("input") | |
.attr("type","radio") | |
.classed("ui-radio",true) | |
.attr("value", function(d) {return d;}) | |
.attr("name", "natCat") | |
.property("checked", function(d, i) {return i===0;}); | |
radios | |
.append('label.ui-label') | |
.text(function (d) { return d; }); | |
uiWrap.selectAll('input').on('click', onchange); | |
// sort | |
data.sort(function(a,b){ | |
return d3.descending(+a[settings.axisValue], +b[settings.axisValue]) | |
}); | |
// bind data | |
var gRects = svg.selectAll(".g-comp-rect") | |
.data(data) | |
.enter() | |
.append("g.g-comp-rect"); | |
// fire charts | |
// pass range AND axisValue | |
var axisValue = settings.axisValue; | |
createHorizScales(fullRange); | |
fireScales(); | |
createHorizElements(fullRange, axisValue); | |
function createHorizScales(rangeOpt) { | |
yscale = d3.scaleBand() | |
.rangeRound([0,h]) | |
.domain(data.map(function(d) { return d[settings.axisCategory]; })) | |
.paddingInner(0.65); | |
xscale = d3.scaleLinear() | |
.domain([0, rangeOpt]) | |
.range([0,w]); | |
} | |
function createHorizElements(rangeOpt, axisValue) { | |
gRects | |
// append grey background | |
.append("rect.bg-bar") | |
.attr("y",function(d){ | |
return yscale(d[settings.axisCategory]); | |
}) | |
.attr("x",0) | |
.attr("height",yscale.bandwidth()) | |
.attr("width", function(d){ | |
return xscale(rangeOpt); | |
}); | |
gRects | |
// append bars | |
.append("rect.comp-rect") | |
.attr("y",function(d){ | |
return yscale(d[settings.axisCategory]); | |
}) | |
.attr("x",0) | |
.attr("height",yscale.bandwidth()) | |
.attr("width",0) | |
.transition().delay(function(d, i) { return i * 120; }) | |
.attr("width", function(d){ | |
return xscale(d[axisValue]); | |
}) | |
.attr("fill","#0080b4"); | |
svg.selectAll("rect.comp-rect") | |
.on("mouseenter", function(d){ | |
d3.select(this).classed("is-hover", true); | |
d3.select('body').selectAppend('div.tooltip'); | |
d3.select(this).call(d3.attachTooltip); | |
d3.select('.tooltip') | |
.html( | |
"<p class=\'tooltip-info\'>" + d["Events"] + " events" + | |
"</p><p class=\'tooltip-info\'>" + d["Fatalities"] + " fatalities" + | |
"</p>") | |
}) | |
.on("mouseleave", function(d){ | |
d3.select(this).classed("is-hover", false); | |
}); | |
gRects.append("text.bar-label") | |
// append labels | |
.attr("y", function(d) { return yscale(d[settings.axisCategory]) + yscale.bandwidth()/2; }) | |
.attr("x", function(d) { | |
if (d[axisValue] < settings.labelSplit) { | |
return (xscale(d[axisValue]) + 5); | |
} else { | |
return (xscale(d[axisValue]) - 10); | |
} | |
}) | |
.attr("dy", "5px") | |
.style("text-anchor",function(d){ | |
if (d[axisValue] < settings.labelSplit) { | |
return "start"; | |
} else { | |
return "end"; | |
} | |
}) | |
.style("fill",function(d){ | |
if (d[axisValue] < settings.labelSplit) { | |
return "#45555f"; | |
} else { | |
return "#ffffff"; | |
} | |
}) | |
.style("opacity",0) | |
.transition().delay(function(d, i) { return i * 120; }) | |
.style("opacity",1) | |
.text(function(d) { | |
if (settings.axisValue == "Events" || settings.axisValue == "Fatalities") { | |
return d[axisValue]; | |
} else { | |
return "$" + d[axisValue] + " Billion"; | |
} | |
}); | |
// tick text line | |
yText | |
.style("text-anchor","start") | |
.attr("x",0) | |
.attr("y", function(d) { return -yscale.bandwidth() - 2; }) | |
.classed("axis-category-text", true); | |
yLine | |
.style("display","none"); | |
xTick | |
.style("display","none"); | |
} | |
function onchange() { | |
selectValue = d3.select(this).property('value'); | |
updateChart(fullRange, selectValue); | |
}; | |
function fireScales() { | |
// create axis element variables | |
yaxis = d3.axisLeft(yscale); | |
xaxis = d3.axisBottom() | |
.scale(xscale) | |
.tickSize(h); | |
svg.append("g.y-axis").call(yaxis); | |
svg.append("g.x-axis").call(xaxis); | |
// tick presentation | fire after axis appended | |
yText = svg.selectAll(".y-axis .tick text"), | |
xText = svg.selectAll(".x-axis .tick text"), | |
yTick = svg.selectAll(".y-axis .tick"), | |
xTick = svg.selectAll(".x-axis .tick"), | |
yLine = svg.selectAll(".y-axis .tick line"), | |
xLine = svg.selectAll(".x-axis .tick line"); | |
} | |
function updateChart(rangeOpt, axisValue) { | |
data.forEach(function(d){ | |
d[axisValue] = +d[axisValue]; | |
}); | |
var upyscale = d3.scaleBand() | |
.rangeRound([0,h]) | |
.domain(data.map(function(d) { return d[settings.axisCategory]; })) | |
.paddingInner(0.65); | |
var upxscale = d3.scaleLinear() | |
.domain([0, rangeOpt]) | |
.range([0,w]); | |
var upGreys = svg.selectAll("rect.bg-bar"); | |
upGreys | |
.transition().duration(settings.transSpeed) | |
.attr("y",function(d){ | |
return upyscale(d[settings.axisCategory]); | |
}); | |
var upgRects = svg.selectAll("g.g-comp-rect").data(data); | |
upgRects | |
.selectAll("rect.comp-rect") | |
.transition().duration(settings.transSpeed) | |
.attr("y",function(d){ | |
return upyscale(d[settings.axisCategory]); | |
}) | |
.attr("width", function(d){ | |
return upxscale(d[axisValue]); | |
}) | |
.attr("fill", function(d){ | |
if (axisValue == "Total losses") { | |
return "#0080b4"; | |
} else if (axisValue == "Insured losses") { | |
return "#D06F1A"; | |
} else if (axisValue == "Uninsured losses") { | |
return "#625BC4"; | |
} | |
}); | |
upgRects.selectAll("text.bar-label") | |
.style("opacity",0) | |
.transition().duration(settings.transSpeed) | |
.attr("y", function(d) { return upyscale(d[settings.axisCategory]) + upyscale.bandwidth()/2; }) | |
.attr("x", function(d) { | |
if (d[axisValue] < settings.labelSplit) { | |
return (upxscale(d[axisValue]) + 5); | |
} else { | |
return (upxscale(d[axisValue]) - 10); | |
} | |
}) | |
.attr("dy", "5px") | |
.style("text-anchor",function(d){ | |
if (d[axisValue] < settings.labelSplit) { | |
return "start"; | |
} else { | |
return "end"; | |
} | |
}) | |
.style("fill",function(d){ | |
if (d[axisValue] < settings.labelSplit) { | |
return "#45555f"; | |
} else { | |
return "#ffffff"; | |
} | |
}) | |
.style("opacity",1) | |
.text(function(d) { | |
if (axisValue == "Events" || axisValue == "Fatalities") { | |
return d[axisValue]; | |
} else { | |
if (d[axisValue] < 1) { | |
return "$" + Math.floor(d[axisValue]*1000) + " Million"; | |
} else { | |
return "$" + d[axisValue] + " Billion"; | |
} | |
} | |
}); | |
} // end update | |
// The table generation function | |
function tabulate(data, columns) { | |
var table = d3.select(".chart-container-table").append("table"), | |
thead = table.append("thead"), | |
tbody = table.append("tbody"); | |
// append the header row | |
thead.append("tr") | |
.selectAll("th") | |
.data(columns) | |
.enter() | |
.append("th") | |
.text(function(column) { return column; }); | |
// create a row for each object in the data | |
var rows = tbody.selectAll("tr") | |
.data(data) | |
.enter() | |
.append("tr"); | |
// create a cell in each row for each column | |
var cells = rows.selectAll("td") | |
.data(function(row) { | |
return columns.map(function(column) { | |
return {column: column, value: row[column]}; | |
}); | |
}) | |
.enter() | |
.append("td") | |
.html(function(d) { return d.value; }); | |
return table; | |
} | |
if (settings.showDataOpt) { | |
// render the table | show Hide jQuery | |
var headers = d3.keys(data[0]); | |
var rawWrap = chartContainer.append("div.raw-wrap"); | |
var showBtn = rawWrap | |
.append('p.raw-btn').html("View Raw Data"); | |
var $chartTable = $(".chart-container-table").hide(); | |
showBtn.on('click', function(){ | |
if ($chartTable.is(':visible')) { | |
$chartTable.slideUp(); | |
showBtn.html("View Raw Data"); | |
} else { | |
$chartTable.html(''); | |
tabulate(data, headers); | |
$chartTable.append("<a class=\'dnld-btn\'>Download Raw Data</a>").on("click", function(){ | |
window.open(settings.dataPath); | |
return false; | |
}); | |
$chartTable.slideDown(); | |
showBtn.html("Hide Raw Data"); | |
} | |
}); | |
} | |
} // end ready | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Catastrophe | Events | Fatalities | Total losses | Insured losses | |
---|---|---|---|---|---|
Severe Thunderstorm | 43 | 40 | 19.0 | 14 | |
Flood, Flash Flood | 19 | 83 | 15.0 | 4.3 | |
Wildfire, Heat Waves, Drought | 18 | 32 | 1.2 | 1.0 | |
Winter Storms, Cold Waves | 7 | 55 | 1.7 | 1.0 | |
Tropical Cyclone | 2 | 52 | 7.0 | 3.5 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment