Built with blockbuilder.org
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="https://d3js.org/d3.v4.min.js"></script>
<title>Tomato Varieties</title>
<style>
body {
margin: 0px;
}
.tick {
font-size: 12pt;
}
g.tick line {
opacity: 0.4;
}
.axis-label {
fill: black;
font-size: 13pt;
font-family: sans-serif;
}
.title {
fill: black;
font-size: 18pt;
font-weight: bold;
font-family: sans-serif;
}
div.tooltip {
position: absolute;
max-width: 300px;
padding: 3px 6px;
color: grey;
font-family: 'Droid Sans Mono', monospace;
font-size: .7em;
background: whitesmoke;
border: 1px solid grey;
border-radius: 3px;
pointer-events: none;
}
td#legend-wrapper {
padding-top: 220px;
}
.tomato-image-container {
margin: 3px 0 0 0;
}
</style>
</head>
<body>
<table>
<tr>
<td>
<svg id="chart" width="800" height="500"></svg>
</td>
<td id="legend-wrapper">
<svg width="140" height="152">
<g
id="g5230"
transform="translate(0,0.6779661)">
<g
id="g5246"
transform="translate(-12.881356,-52.20339)">
<g
transform="translate(0,-0.6779661)"
id="g5208">
<circle
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1.5;stroke-dasharray:1, 0;stroke-opacity:1"
id="circle3750-7"
r="30"
cy="171.5"
cx="63.437881" />
<circle
style="fill:#ff0000;fill-opacity:0;stroke:#000000;stroke-width:1.5;stroke-dasharray:1, 0;stroke-opacity:1"
id="circle3764-5"
r="3.5510204081632653"
cy="197.5"
cx="62.63118" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 63.29408,141 58.95904,0"
id="path5002"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 63.29408,194 58.95904,0"
id="path5002-5"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="126.11049"
y="105.75459"
id="text5079"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5081"
x="126.11049"
y="144"
style="font-size:11.25px;line-height:125%">25oz</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="126.11049"
y="193.75459"
id="text5079-6"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5081-2"
x="126.11049"
y="198.5"
style="font-size:11.25px;line-height:125%">2oz</tspan></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:200%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="46.82235"
y="61.246117"
id="text5079-1"
sodipodi:linespacing="200%"><tspan
sodipodi:role="line"
id="tspan5081-27"
x="46.82235"
y="100"
style="font-size:11.25px;line-height:200%">High Yield</tspan><tspan
sodipodi:role="line"
x="46.82235"
y="120"
style="font-size:11.25px;line-height:200%"
id="tspan5204">Heat Tolerant</tspan></text>
<path
style="fill:#ff0000;fill-rule:evenodd;stroke:#ff0000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 16.271186,95 23.728814,0"
id="path5159"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:3, 1.5;stroke-dashoffset:0;stroke-opacity:1"
d="M 16.271186,115 40,115"
id="path5159-9"
inkscape:connector-curvature="0" />
</g>
</g></svg>
</td>
</tr>
</table>
<script>
var tooltip;
tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
const title = "Tomato Varieties";
const xValue = d => d.matures_avg_days;
const xLabel = 'Plant Maturity (days)';
const yValue = d => d.height_avg_ft;
const yLabel = 'Plant Height (feet)';
const margin = {
left: 100,
right: 40,
top: 65,
bottom: 70
};
const svg = d3.select('#chart');
const width = svg.attr('width');
const height = svg.attr('height');
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const xAxisG = g.append('g')
.attr('transform', `translate(0, ${innerHeight})`);
const yAxisG = g.append('g');
xAxisG.append('text')
.attr('class', 'axis-label')
.attr('x', innerWidth / 2)
.attr('y', 55)
.text(xLabel);
yAxisG.append('text')
.attr('class', 'axis-label')
.attr('x', -innerHeight / 2)
.attr('y', -50)
.attr('transform', `rotate(-90)`)
.style('text-anchor', 'middle')
.text(yLabel);
const xScale = d3.scaleLinear();
const yScale = d3.scaleLinear();
const xTickCnt = 10;
const yTickCnt = 5;
const xAxis = d3.axisBottom()
.scale(xScale)
.ticks(xTickCnt)
.tickPadding(15)
.tickSize(-innerHeight);
const yAxis = d3.axisLeft()
.scale(yScale)
.ticks(yTickCnt)
.tickPadding(15)
.tickSize(-innerWidth);
const row = d => {
d.fruit_size_low_oz = Number(d.fruit_size_low_oz);
d.fruit_size_high_oz = Number(+d.fruit_size_high_oz);
d.fruit_size_avg_oz = Number(+d.fruit_size_avg_oz);
d.circle_radius = 2 * Number(+d.fruit_size_avg_oz);
d.matures_low_days = Number(+d.matures_low_days);
d.matures_high_days = Number(+d.matures_high_days);
d.matures_avg_days = Number(+d.matures_avg_days);
d.spacing_low_in = Number(+d.spacing_low_in);
d.spacing_avg_in = Number(+d.spacing_avg_in);
d.spacing_high_in = Number(+d.spacing_high_in);
d.height_low_ft = Number(+d.height_low_ft);
d.height_high_ft = Number(+d.height_high_ft);
d.height_avg_ft = Number(+d.height_avg_ft);
d.yield_num = Number(+d.yield_num);
d.heat_tolerance_num = Number(+d.heat_tolerance_num);
return d;
};
svg.append('text')
.attr('class', 'title')
.attr('x', width / 2)
.attr('y', 40)
.style('text-anchor', 'middle')
.text(title + "v2");
d3.csv('tomato_varieties.csv', row, data => {
addScaledRadiusTo(data);
const xaxis_domain_adjusted = d3.extent(data, xValue);
const yaxis_domain_adjusted = d3.extent(data, yValue);
const maxMinCircleEdges = getMaxMinCircleEdges(data);
xScale
.domain(xaxis_domain_adjusted)
.range([0, innerWidth])
.nice();
yScale
.domain(yaxis_domain_adjusted)
.range([innerHeight, 0])
.nice();
const originalLeftEdge = xScale.invert(0);
const originalRightEdge = xScale.invert(innerWidth);
const originalTopEdge = yScale.invert(0);
const originalBottomEdge = yScale.invert(innerHeight);
const xTickDist = Math.ceil((d3.max(xScale.ticks()) - d3.min(xScale.ticks())) / xTickCnt);
const yTickDist = Math.ceil((d3.max(yScale.ticks()) - d3.min(yScale.ticks())) / yTickCnt);
if (maxMinCircleEdges.rightEdge > originalRightEdge) { //if the maxRightCircleEdge > originalRightEdge, then add one more tick to the right
xaxis_domain_adjusted[1] = xaxis_domain_adjusted[1] + xTickDist; //so tomatoes don't go off right edge
}
if (maxMinCircleEdges.leftEdge < originalLeftEdge) { //if minLeftCircleEdge < originalLeftEdge, then add one more tick to the left
xaxis_domain_adjusted[0] = xaxis_domain_adjusted[0] - xTickDist; //so tomatoes don't go off left edge
}
if (maxMinCircleEdges.topEdge > originalTopEdge) { //if maxTopCircleEdge > yaxis_domain_adjusted[1], then add one more tick to the top
yaxis_domain_adjusted[1] = yaxis_domain_adjusted[1] + yTickDist; //so tomatoes don't go off top edge
}
if (maxMinCircleEdges.bottomEdge < originalBottomEdge) { //if minBottomCircleEdge < xaxis_domain_adjusted[0], then add one more tick to the bottom (negative?)
yaxis_domain_adjusted[0] = yaxis_domain_adjusted[0] - yTickDist; //so tomatoes don't go off bottom edge
}
xScale
.domain(xaxis_domain_adjusted)
.range([0, innerWidth])
.nice();
yScale
.domain(yaxis_domain_adjusted)
.range([innerHeight, 0])
.nice();
var prev_stroke_color = 'rgb(0, 0, 0)';
//sort by fruit_size_avg_oz largest to smallest, so smaller go on top of larger (and are hoverable)
data.sort(dynamicSort("fruit_size_avg_oz"));
data.reverse();
g.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', d => xScale(xValue(d)))
.attr('cy', d => yScale(yValue(d)))
.attr('fill', d => d.color)
.attr('fill-opacity', 0.4)
.attr('stroke', d => getStrokeBasedOnYield(d))
.attr('stroke-width', '1.5')
.attr('stroke-opacity', '1')
.attr('stroke-dasharray', d => getStrokeDashArrayBasedOnHeatTolerance(d))
.attr('r', d => d.scaled_radius)
.on('mouseover', function(d) {
prev_stroke_color = d3.select(this).style('stroke');
var tooltip_msg = '<div>';
tooltip_msg = `Common Name: ${d.common_name}`;
tooltip_msg = tooltip_msg + `<br/>Heat tolerance: ${d.heat_tolerance}`;
tooltip_msg = tooltip_msg + `<br/>Fruit Size: ${d.fruit_size_avg_oz}oz`;
tooltip_msg = tooltip_msg + `<br/>Plant height: ${d.height_avg_ft}ft`;
tooltip_msg = tooltip_msg + `<br/>Maturity: ${d.matures_avg_days} days`;
if (`${d.description}`.length > 0) {
tooltip_msg = tooltip_msg + `<br/>${d.description}`;
}
tooltip_msg = tooltip_msg + '</div>';
tooltip_msg = tooltip_msg + '<div class="tomato-image-container">';
tooltip_msg = tooltip_msg + `<img src='${d.image_url}' width='250' height='225' />`;
tooltip_msg = tooltip_msg + '</div>';
tooltip.transition().style("opacity", 1);
tooltip.html(tooltip_msg).style("left", (d3.event.pageX + 15) + "px").style("top", (d3.event.pageY - 28) + "px");
d3.select(this).style("stroke", "green");
d3.select(this).style("stroke-width", "4");
})
.on('mouseout', function(d) {
d3.select(this).style("stroke", prev_stroke_color);
d3.select(this).style("stroke-width", "1.5");
tooltip.style("opacity", 0);
});
g.selectAll("circle")
.on("click", function(d) {
window.open(d.url, '_blank');
});
bbox = g.node().getBBox();
vx = bbox.x; // container x co-ordinate
vy = bbox.y; // container y co-ordinate
vw = bbox.width; // container width
vh = bbox.height; // container height
defaultView = "" + vx + " " + vy + " " + vw + " " + vh;
console.log(defaultView);
defaultView = "0 0 800 500"
svg.attr("viewBox", defaultView)
.attr("preserveAspectRatio", "xMidYMid meet")
.call(d3.zoom().on("zoom", zoomed));
xAxisG.call(xAxis);
yAxisG.call(yAxis);
});
function zoomed() {
console.log(d3.event.transform);
var translateX = d3.event.transform.x;
var translateY = d3.event.transform.y;
var xScale = d3.event.transform.k;
svg.attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + xScale + ")");
}
function getStrokeBasedOnYield(d) {
returnColor = 'black';
if (d.yield == 'High') {
returnColor = 'red';
}
return returnColor;
}
function getStrokeDashArrayBasedOnHeatTolerance(d) {
returnSDA = '1, 0';
if (d.heat_tolerance == 'High') {
returnSDA = '3, 2';
}
return returnSDA;
}
function getMaxMinCircleEdges(data) {
const maxMinCircleEdges = {};
//how to figure these out?
// maxMinCircleEdges.rightEdge = 93;
// maxMinCircleEdges.bottomEdge = -.75;
// maxMinCircleEdges.leftEdge = 44;
// maxMinCircleEdges.topEdge = 11;
const rightEdges = [];
const leftEdges = [];
const topEdges = [];
const bottomEdges = [];
//how to get .1 and .04 below?
for (let index = 0; index < data.length; ++index) {
let tomato = data[index];
rightEdges.push(xScale(tomato.matures_avg_days + .1 * (tomato.scaled_radius)));
leftEdges.push(xScale(tomato.matures_avg_days - .1 * (tomato.scaled_radius)));
topEdges.push(yScale(tomato.height_avg_ft + .04 * (tomato.scaled_radius)));
bottomEdges.push(yScale(tomato.height_avg_ft - .04 * (tomato.scaled_radius)));
}
maxMinCircleEdges.rightEdge = d3.max(rightEdges);
maxMinCircleEdges.bottomEdge = d3.min(bottomEdges);
maxMinCircleEdges.leftEdge = d3.min(leftEdges);
maxMinCircleEdges.topEdge = d3.max(topEdges);
return maxMinCircleEdges;
}
function addScaledRadiusTo(data) {
const fruitSizes = [];
for (let index = 0; index < data.length; ++index) {
let tomato = data[index];
fruitSizes.push(tomato.fruit_size_avg_oz);
}
const minFruitSize = d3.min(fruitSizes);
const maxFruitSize = d3.max(fruitSizes);
var linearScale = d3.scaleLinear().domain([minFruitSize, maxFruitSize]).range([3, 30]);
for (var i = 0; i < data.length; i++) {
data[i].scaled_radius = linearScale(fruitSizes[i]);
}
return data;
}
function dynamicSort(property) {
var sortOrder = 1;
if (property[0] === "-") {
sortOrder = -1;
property = property.substr(1);
}
return function(a, b) {
var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
return result * sortOrder;
}
}
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js