D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
FergusDevelopmentLLC
Full window
Github gist
Tomato Varieties
Built with
blockbuilder.org
<!DOCTYPE html> <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