D3.v3 Brush Strange Behavior - javascript

I'm trying to draw a brush on my histogram. The brush controls only appear after a click event (not on the initial page load). Obviously, not the desired behavior.
How do I instantiate the chart AND the brushes on the initial page load before the first click event?
// TEST Data //
var caldata = [
{"cal_start_yr": "1945"}, {"cal_start_yr": "1948"},
{"cal_start_yr": "1945"}, {"cal_start_yr": "1950"},
{"cal_start_yr": "1945"}, {"cal_start_yr": "1941"},
{"cal_start_yr": "1944"}, {"cal_start_yr": "1949"}
];
// CROSSFILTER Aggregations //
var cals = crossfilter(caldata);
var total = cals.groupAll().reduceCount().value();
var year = cals.dimension(function(d) {
return d.cal_start_yr;
});
var countYear = year.group().reduceCount();
var yearCount = countYear.all();
// Some helper AGGREGATION Values
var keys = countYear.all().map(function(d) {return d.value;}),
min = d3.min(countYear.all(), function(d) {return d.key;}),
max = d3.max(countYear.all(), function(d) {return d.key;}),
range = max - min;
// Histogram dimensions
var margin = {top: 10, right: 20, bottom: 10,left: 10 },
height = 250 - margin.top - margin.bottom,
width = 450 - margin.left - margin.right,
barPadding = 5;
// Histogram SCALES
var xScale = d3.scale.linear()
.domain([min, max])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(countYear.all(), function(d) {return d.value;})])
.range([height / 2, 0]);
// D3 Tool Tip
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(function(d) {return d.key});
// CANVAS setup //
var histogram1 = d3.select("#histogram1").append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g");
// Initiate Tooltip //
histogram1.call(tip);
// DRAW Histogram //
histogram1.selectAll("rect")
.data(yearCount)
.enter().append("rect")
.attr("x", function(d) {
return xScale(d.key) + 0.5 * (width / range)
})
.attr("width", width / range)
.attr("y", function(d) {
return yScale(d.value);
})
.attr("height", function(d) {
return (height / 2 - yScale(d.value));
})
.attr("fill", "green")
.attr("fill-opacity", .25)
.attr("stroke", "white")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
// X AXIS //
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(5)
.orient("bottom")
.tickFormat(d3.format("d"));
histogram1.append("g")
.attr("class", "axis")
.call(xAxis)
.attr("transform", "translate(" + margin.left + "," + height / 2 + ")");
var brush = d3.svg.brush()
.x(xScale)
.extent([xScale(+1945), xScale(+1946)])
.on("brush", function(d) {console.log(d);});
var brushg = histogram1.append("g")
.attr("class", "brush")
.call(brush)
brushg.selectAll("rect")
.attr("height", height / 2);
brushg.selectAll(".resize")
.append("path")
.attr("d", resizePath);
function resizePath(d) {
// Style the brush resize handles. No idea what these vals do...
var e = +(d == "e"),
x = e ? 1 : -1,
y = height / 4; // Relative positon if handles
return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) + "M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8);
}
/*** d3-tip styles */
.as-console-wrapper { max-height: 20% !important;}
.d3-tip {
line-height: 1.5;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 0px;
text-align: center;
}
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
.d3-tip.n:after {
top: 100%;
left: 0;
margin: -1px 0 0;
}
/*** D3 brush */
.brush .extent {
stroke: #222;
fill-opacity: .125;
shape-rendering: crispEdges;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
<div id="histogram1"></div>

The brush's extent is set to a value from the scale's domain, not its range! From the docs:
The scale is typically defined as a quantitative scale, in which case the extent is in data space from the scale's domain
To set the initial extent you have to use
.extent([1945, 1946])
instead of
.extent([xScale(+1945), xScale(+1946)])
For a working demo have a look at the updated snippet:
// TEST Data //
var caldata = [
{"cal_start_yr": "1945"}, {"cal_start_yr": "1948"},
{"cal_start_yr": "1945"}, {"cal_start_yr": "1950"},
{"cal_start_yr": "1945"}, {"cal_start_yr": "1941"},
{"cal_start_yr": "1944"}, {"cal_start_yr": "1949"}
];
// CROSSFILTER Aggregations //
var cals = crossfilter(caldata);
var total = cals.groupAll().reduceCount().value();
var year = cals.dimension(function(d) {
return d.cal_start_yr;
});
var countYear = year.group().reduceCount();
var yearCount = countYear.all();
// Some helper AGGREGATION Values
var keys = countYear.all().map(function(d) {return d.value;}),
min = d3.min(countYear.all(), function(d) {return d.key;}),
max = d3.max(countYear.all(), function(d) {return d.key;}),
range = max - min;
// Histogram dimensions
var margin = {top: 10, right: 20, bottom: 10,left: 10 },
height = 250 - margin.top - margin.bottom,
width = 450 - margin.left - margin.right,
barPadding = 5;
// Histogram SCALES
var xScale = d3.scale.linear()
.domain([min, max])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(countYear.all(), function(d) {return d.value;})])
.range([height / 2, 0]);
// D3 Tool Tip
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(function(d) {return d.key});
// CANVAS setup //
var histogram1 = d3.select("#histogram1").append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g");
// Initiate Tooltip //
histogram1.call(tip);
// DRAW Histogram //
histogram1.selectAll("rect")
.data(yearCount)
.enter().append("rect")
.attr("x", function(d) {
return xScale(d.key) + 0.5 * (width / range)
})
.attr("width", width / range)
.attr("y", function(d) {
return yScale(d.value);
})
.attr("height", function(d) {
return (height / 2 - yScale(d.value));
})
.attr("fill", "green")
.attr("fill-opacity", .25)
.attr("stroke", "white")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
// X AXIS //
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(5)
.orient("bottom")
.tickFormat(d3.format("d"));
histogram1.append("g")
.attr("class", "axis")
.call(xAxis)
.attr("transform", "translate(" + margin.left + "," + height / 2 + ")");
var brush = d3.svg.brush()
.x(xScale)
.extent([1945, 1946])
.on("brush", function(d) {console.log(brush.extent());});
var brushg = histogram1.append("g")
.attr("class", "brush")
.call(brush)
brushg.selectAll("rect")
.attr("height", height / 2);
brushg.selectAll(".resize")
.append("path")
.attr("d", resizePath);
function resizePath(d) {
// Style the brush resize handles. No idea what these vals do...
var e = +(d == "e"),
x = e ? 1 : -1,
y = height / 4; // Relative positon if handles
return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) + "M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8);
}
/*** d3-tip styles */
.d3-tip {
line-height: 1.5;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 0px;
text-align: center;
}
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
.d3-tip.n:after {
top: 100%;
left: 0;
margin: -1px 0 0;
}
/*** D3 brush */
.brush .extent {
stroke: #222;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.as-console-wrapper { max-height: 30% !important;}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
<div id="histogram1"></div>

Related

Mouse hovering over circle data point using D3.js

I am trying to add a hover effect without success. How to show data when hovering over datapoint?
https://plnkr.co/edit/Va1Dw3hg2D5jPoNGKVWp?p=preview&preview
<!DOCTYPE html>
svg {
font: 12px sans-serif;
text-anchor: middle;
}
rect {
stroke: lightgray;
stoke-width: 1px;
fill: none;
}
.y.axis path {
fill: none;
stroke: none;
}
</style>
d3.csv('data.csv', function (error, rows) {
var data = [];
rows.forEach(function (d) {
var x = d[''];
delete d[''];
for (prop in d) {
var y = prop,
value = d[prop];
data.push({
x: x,
y: y,
value: +value,
});
}
});
var margin = {
top: 25,
right: 80,
bottom: 25,
left: 25,
},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
domain = d3
.set(
data.map(function (d) {
return d.x;
})
)
.values(),
num = Math.sqrt(data.length),
color = d3.scale
.linear()
.domain([-1, 0, 1])
.range(['#B22222', '#fff', '#000080']);
var x = d3.scale.ordinal().rangePoints([0, width]).domain(domain),
y = d3.scale.ordinal().rangePoints([0, height]).domain(domain),
xSpace = x.range()[1] - x.range()[0],
ySpace = y.range()[1] - y.range()[0];
var svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr(
'transform',
'translate(' + margin.left + ',' + margin.top + ')'
);
var cor = svg
.selectAll('.cor')
.data(data)
.enter()
.append('g')
.attr('class', 'cor')
.attr('transform', function (d) {
return 'translate(' + x(d.x) + ',' + y(d.y) + ')';
});
cor
.append('rect')
.attr('width', xSpace)
.attr('height', ySpace)
.attr('x', -xSpace / 2)
.attr('y', -ySpace / 2);
cor
.filter(function (d) {
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = ypos + 1; i < num; i++) {
if (i === xpos) return false;
}
return true;
})
.append('text')
.attr('y', 5)
.text(function (d) {
if (d.x === d.y) {
return d.x;
} else {
return d.value.toFixed(2);
}
})
.style('fill', function (d) {
if (d.value === 1) {
return '#000';
} else {
return color(d.value);
}
});
cor
.filter(function (d) {
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = ypos + 1; i < num; i++) {
if (i === xpos) return true;
}
return false;
})
.append('circle')
.attr('r', function (d) {
return (width / (num * 2)) * (Math.abs(d.value) + 0.1);
})
.style('fill', function (d) {
if (d.value === 1) {
return '#000';
} else {
return color(d.value);
}
});
var aS = d3.scale
.linear()
.range([-margin.top + 5, height + margin.bottom - 5])
.domain([1, -1]);
var yA = d3.svg.axis().orient('right').scale(aS).tickPadding(7);
var aG = svg
.append('g')
.attr('class', 'y axis')
.call(yA)
.attr(
'transform',
'translate(' + (width + margin.right / 2) + ' ,0)'
);
var iR = d3.range(-1, 1.01, 0.01);
var h = height / iR.length + 3;
iR.forEach(function (d) {
aG.append('rect')
.style('fill', color(d))
.style('stroke-width', 0)
.style('stoke', 'none')
.attr('height', h)
.attr('width', 10)
.attr('x', 0)
.attr('y', aS(d));
});
});
</script>
Create a new div and give it some styles. also set the opacity to 0.
var popupDiv = d3
.select("#root")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Next, you need to add a mouseenter and mouseout to your desired elements
d3.append("div") // Change this to your element(s)
.on("mouseenter", function (d, i) {
popupDiv.transition().duration(200).style("opacity", 0.9);
popupDiv
.html(i.Description) // Example of display a data item description
.style("left", d.pageX + "px") // X offset can be added here
.style("top", d.pageY + "px"); // Y offset can be added here
})
.on("mouseout", function (d, i) {
popupDiv.transition().duration(200).style("opacity", 0);
})
Bear in mind the first parameter d to the mouseenter and mouseout functions is the mouse event itself which is being used to find out the x and y position of the mouse. The second parameter i is the data attached to your element if you wanted to use any of the data for specific descriptions or colours etc.
These are the base styles I used when creating a popup
div.tooltip {
position: absolute;
text-align: center;
width: auto;
height: auto;
max-width: 200px;
padding: 8px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
z-index: 10;
}

Unable to position tooltip on d3js bar chart

I've a bar chart using d3. When I hover over a bar I'm showing a tooltip. Whenever I've different set of data(number of bars), I'm unable to position the tooltip pointer to the center of the bar on the bar-chart. I need the pointer to be at the center of the bar for any number of bars on the chart. I'm using the x-axis values but, the tooltip is not placing at the right position.
Following is the snippet for the same
var width = 216;
var height = 200;
var barPadding = 18;
var barWidth = 58;
var dataSize = d3.selectAll(dataset).size();
var margin = {
top: 10,
right: 0,
bottom: 58,
left: 30
};
var width_box_sizing_border_box = width + margin.left + margin.right;
var height_box_sizing_border_box = height + margin.bottom + margin.top;
var graph;
var xScale;
var yScale;
var dataset;
var xTicks = 6;
var yTicks = 6;
var tooltipEl = function(d) {
return (
'<div>' + d.val + '</div>'
)
}
dataset = [{
desc: 'test1',
val: 40
}, {
desc: 'some dummy text here',
val: 120
}];
xScale = d3.scaleBand()
.domain(dataset.map(function(d) {
return d.desc;
}))
.range([margin.left, width - margin.right]);
yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, 350]);
graph = d3.select("#graph")
.append("svg")
.attr("class", "bar-chart")
.attr("width", width_box_sizing_border_box)
.attr("height", height_box_sizing_border_box);
// Tool Tip
const div = d3
.select('#graph')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
graph.append("g")
.attr("class", "x-scale")
.attr("transform", "translate(0," + (height + margin.top) + ")")
.call(d3.axisBottom(xScale).ticks(xTicks))
.selectAll(".tick text")
.call(wrap, xScale.bandwidth());
graph.append("g")
.attr("class", "y-scale")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(d3.axisLeft(yScale).ticks(yTicks).tickPadding(10));
graph
.append("g")
.attr("transform", "translate(" + (xScale.bandwidth() / 2 - (barWidth - barPadding) / 2) + "," + margin.top + ")")
.attr('class', 'graph-placeholder')
.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bar1")
.attr("height", height)
.attr("width", barWidth - barPadding)
.attr('x', d => xScale(d.desc));
graph
.append("g")
.attr("transform", "translate(" + (xScale.bandwidth() / 2 - (barWidth - barPadding) / 2) + "," + margin.top + ")")
.attr('class', 'graph-main')
.selectAll("bar1")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bar2")
.attr('x', d => xScale(d.desc))
.attr("y", function(d) {
return yScale(d.val);
})
.attr("height", function(d) {
return height - yScale(d.val);
})
.attr("width", barWidth - barPadding)
.on('mouseover', d => {
div
.html(tooltipEl(d));
div
.transition()
.duration(200)
.style('display', 'block')
.style('opacity', 1);
div
.style('left', xScale(d.desc) + 'px')
.style('top', (height + margin.top + 8) + 'px');
})
.on('mouseout', () => {
div
.transition()
.duration(500)
.style('opacity', 0)
.style('display', 'none')
});
graph
.append("g")
.attr("transform", "translate(" + (xScale.bandwidth() / 2 - (barWidth - barPadding) / 2) + "," + margin.top + ")")
.attr('class', 'bar-label')
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(d => d.val + '%')
.attr("y", function(d) {
return yScale(d.val) - 5;
}).attr('x', function(d) {
return xScale(d.desc) + ((barWidth - barPadding) / 2 - d3.select(this).node().getBBox().width / 2);
});
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
.bar-chart {
background-color: #ccc;
}
.bar2 {
fill: steelblue;
}
.bar1 {
fill: #f2f2f2;
}
text {
font-size: 12px;
text-anchor: middle;
}
.bar-label text {
text-anchor: start;
}
path.domain {
stroke-width: 0;
display: none;
}
.tooltip {
background: #FFFFFF;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.33);
font-family: "Segoe UI";
line-height: normal;
padding: 15px;
width: 400px;
position: absolute;
display: none;
}
.tooltip__container {
display: flex;
}
.tooltip::before {
content: "";
position: absolute;
left: 22px;
top: -8px;
transition: all 0.5s ease;
border: 8px solid #fff;
box-shadow: -5px -5px 5px rgba(0, 0, 0, 0.1);
transform: rotate(45deg);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="container">
<div id="graph"></div>
</div>
Fiddle
-- Edited
This should be generic enough. Tested it with between 2 to 8 bars and seems on point.
.on('mouseover', d => {
div
.html(tooltipEl(d));
div
.transition()
.duration(200)
.style('display', 'block')
.style('opacity', 1);
div
.style('left', xScale(d.desc) + (xScale.bandwidth() / 2 - (barWidth - barPadding) / 2) - 1 + 'px')
.style('top', (height + margin.top + 8) + 'px');
})

D3.js jumpy zoom behaviour

I am trying to do a zoomable heatmap and the community here on SO have helped massively, however I am now stuck the whole day today trying to fix a glitch and I am hitting the wall every single time.
The issue is that the zoom looks jumpy, ie the plot is rendered fine, however when the zoom event is triggered some kind of transformation happens that changes the axes and the scaling in an abrupt way. The code below demonstrates this issue. The problem does not always happen, it depends on the heatmap dimension and/or the number of the dots.
Some similar cases from people with the same problem here on SO turned out to be that the zoom was not applied to the correct object but I think I am not doing that mistake. Many thanks in advance
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
}
.x.axis path {
//display: none;
}
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
#tooltip {
position: absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none;
/*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
</style>
<title>Heatmap Chart</title>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<script src='heatmap.js' type='text/javascript'></script>
</head>
<body>
<div id="chart">
<svg width="550" height="1000"></svg>
</div>
<script>
var dataset = [];
for (let i = 1; i < 60; i++) { //360
for (j = 1; j < 70; j++) { //75
dataset.push({
xKey: i,
xLabel: "xMark " + i,
yKey: j,
yLabel: "yMark " + j,
val: Math.random() * 25,
})
}
};
var svg = d3.select("#chart")
.select("svg")
var xLabels = [],
yLabels = [];
for (i = 0; i < dataset.length; i++) {
if (i == 0) {
xLabels.push(dataset[i].xLabel);
var j = 0;
while (dataset[j + 1].xLabel == dataset[j].xLabel) {
yLabels.push(dataset[j].yLabel);
j++;
}
yLabels.push(dataset[j].yLabel);
} else {
if (dataset[i - 1].xLabel == dataset[i].xLabel) {
//do nothing
} else {
xLabels.push(dataset[i].xLabel);
}
}
};
var margin = {
top: 0,
right: 25,
bottom: 40,
left: 75
};
var width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var dotSpacing = 0,
dotWidth = width / (2 * (xLabels.length + 1)),
dotHeight = height / (2 * yLabels.length);
// var dotWidth = 1,
// dotHeight = 3,
// dotSpacing = 0.5;
var daysRange = d3.extent(dataset, function(d) {
return d.xKey
}),
days = daysRange[1] - daysRange[0];
var hoursRange = d3.extent(dataset, function(d) {
return d.yKey
}),
hours = hoursRange[1] - hoursRange[0];
var tRange = d3.extent(dataset, function(d) {
return d.val
}),
tMin = tRange[0],
tMax = tRange[1];
// var width = (dotWidth * 2 + dotSpacing) * days,
// height = (dotHeight * 2 + dotSpacing) * hours;
// var width = +svg.attr("width") - margin.left - margin.right,
// height = +svg.attr("height") - margin.top - margin.bottom;
var colors = ['#2C7BB6', '#00A6CA', '#00CCBC', '#90EB9D', '#FFFF8C', '#F9D057', '#F29E2E', '#E76818', '#D7191C'];
// the scale
var scale = {
x: d3.scaleLinear()
.domain([-1, d3.max(dataset, d => d.xKey)])
.range([-1, width]),
y: d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.yKey)])
.range([height, 0]),
//.range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]),
};
var xBand = d3.scaleBand().domain(xLabels).range([0, width]),
yBand = d3.scaleBand().domain(yLabels).range([height, 0]);
var axis = {
x: d3.axisBottom(scale.x).tickFormat((d, e) => xLabels[d]),
y: d3.axisLeft(scale.y).tickFormat((d, e) => yLabels[d]),
};
function updateScales(data) {
scale.x.domain([-1, d3.max(data, d => d.xKey)]),
scale.y.domain([0, d3.max(data, d => d.yKey)])
}
var colorScale = d3.scaleQuantile()
.domain([0, colors.length - 1, d3.max(dataset, function(d) {
return d.val;
})])
.range(colors);
var zoom = d3.zoom()
.scaleExtent([dotWidth, dotHeight])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// SVG canvas
svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
//.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height + dotHeight);
// Heatmap dots
var heatDotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
heatDotsGroup.call(zoom);
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + scale.y(-1) + ")")
.call(axis.x)
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
.call(axis.y);
function zoomed() {
d3.event.transform.y = 0;
d3.event.transform.x = Math.min(d3.event.transform.x, 5);
d3.event.transform.x = Math.max(d3.event.transform.x, (1 - d3.event.transform.k) * width);
d3.event.transform.k = Math.max(d3.event.transform.k, 1);
//console.log(d3.event.transform)
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale\((.*?)\)/, "scale($1, 1)"));
}
svg.call(renderPlot, dataset)
function renderPlot(selection, dataset) {
//updateScales(dataset);
heatDotsGroup.selectAll("ellipse")
.data(dataset)
.enter()
.append("ellipse")
.attr("cx", function(d) {
return scale.x(d.xKey) - xBand.bandwidth();
})
.attr("cy", function(d) {
return scale.y(d.yKey) + yBand.bandwidth();
})
.attr("rx", dotWidth)
.attr("ry", dotHeight)
.attr("fill", function(d) {
return colorScale(d.val);
})
.on("mouseover", function(d) {
$("#tooltip").html("X: " + d.xKey + "<br/>Y: " + d.yKey + "<br/>Value: " + Math.round(d.val * 100) / 100);
var xpos = d3.event.pageX + 10;
var ypos = d3.event.pageY + 20;
$("#tooltip").css("left", xpos + "px").css("top", ypos + "px").animate().css("opacity", 1);
}).on("mouseout", function() {
$("#tooltip").animate({
duration: 500
}).css("opacity", 0);
});
}
</script>
</body>
</html>
Change the zoom scaleExtend
var zoom = d3.zoom()
.scaleExtent([1, dotHeight])
.on("zoom", zoomed);
Call the zoom on the whole svg not on the heatDotsGroup because this node receives the tranformation, and also not on the g node that has the graph transformation here variable svg (to keep things a bit obscure)
svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// heatDotsGroup.call(zoom);
Don't limit the zoom scale k in the tick. Already taken care of by the scaleExtent()
// d3.event.transform.k = Math.max(d3.event.transform.k, 1);
Why calculate all the d3.max() when you already have calculated the d3.extent() of these values?

Positioning of canvas after the conversion from D3

I just managed to convert the respective heatmap from SVG to canvas. (due to large dataset vs performance issue) However the position of the generated heatmap has goes to a new region. I not sure how am I going to do about this. By changing the transform does not change anything as well.
My code:
var units = [];
for(var unit_i = 0; unit_i<=101;){
if(unit_i==0){
units.push(1);
unit_i = unit_i + 5;
}
else{
units.push(unit_i);
unit_i = unit_i + 4;
}
}
var times = [];
for(var times_i = 0; times_i<=1440;){
if(times_i==0){
times.push(1);
times_i = times_i + 10;
}
else{
times.push(times_i);
times_i = times_i + 9;
}
}
var newSample = [{unit:null, timestamp: null, level: null}];
//by using below method we can observe the delay is not due to the data during insertion
for(var unit=1; unit<=99; unit++){
for(var timestamp = 1; timestamp<=100; timestamp++){
var i = Math.random() * 1400;
newSample.push({unit:unit, timestamp: timestamp, level:i});
}
}
var hours = 0;
var hoursIndicator = 0;
var margin = {
top: 170,
right: 100,
bottom: 70,
left: 100
};
var width = 2500,//optimized width
//gridSize = Math.floor(width / times.length),//optimized gridsize
gridSize = 10;//if 20 each interval will have 5
height = 50 * (units.length); //optimized, the greater the length, the greater the height
console.log("this is gridSize:" + gridSize +", this is height: " + height + ", and this is width: " + width);
//SVG container
var svg = d3.select('.trafficCongestions')
.append("svg")
//.style("position", "absolute")
.attr("width", width + margin.left + margin.right)//optimization
.attr("height", height + margin.top + margin.bottom)//optimization
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = d3.select('.trafficCongestions').append("canvas")
.attr("id", "canvas")
.attr("width", width + margin.left + margin.right)//optimization
.attr("height", height + margin.top + margin.bottom);//optimization
var context = canvas.node().getContext("2d");
context.clearRect(0, 0, width, height);
var detachedContainer = document.createElement("custom");
var dataContainer = d3.select(detachedContainer);
//Reset the overall font size
var newFontSize = width * 62.5 / 900;
//heatmap drawing starts from here
var colorScale = d3.scaleLinear()
.domain([0, d3.max(newSample, function(d, i) {return d.level; })/2, d3.max(newSample, function(d, i) {return d.level; })])
.range(["#009933", "#FFCC00", "#990000"])//obtain the max data value of count
//y-axis (solely based on data of days)
var dayLabels = svg.selectAll(".dayLabel")
.data(units)
.enter().append("text")
.text(function (d) { return d; })
.attr("x", 0)
.attr("y", function (d, i) {
return (i) * (gridSize * 4)/*adjusts the interval distance with (n - 1) concept*/; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize + ")");
//x-axis (solely based on data of times)
var timeLabels = svg.selectAll(".timeLabel")
.data(times)
.enter().append("text")
.text(function(d, i) {
var hrs = Math.floor(d/60);
var mins = d%60;
if(hrs<10){
if(mins<10){
return "0"+hrs + ":0" + mins;
}
return "0"+ hrs + ":" + mins;
}
return hrs +":"+ mins;
})
.attr("x", function(d, i) { return i * (gridSize * 9)/*adjusts the interval distance with (n - 1) concept*/; })
.attr("y", 0)
.style("text-anchor", "middle")
.attr("transform", "translate(" + 1 + ", -6)")
var heatMap = dataContainer.selectAll("custom.rect")
.data(newSample)
.enter().append("custom")
.attr("x", function(d) {
return (d.timestamp - 1) * (gridSize);
})
.attr("y", function(d) {
console.log(d.unit);
return (d.unit - 1) * (gridSize);
})
.classed("rect", true)
.attr("class", " rect bordered")
.attr("width", gridSize)
.attr("height", gridSize)
.attr("strokeStyle", "rgba(255,255,255, 0.6)")//to have the middle line or not
.attr("fillStyle", function(d,i){
return colorScale(d.level);
});
drawCanvas();
//Append title to the top
svg.append("text")
.attr("class", "title")
.attr("x", width/2)
.attr("y", -90)
.style("text-anchor", "middle")
.text("Sample Result");
svg.append("text")
.attr("class", "subtitle")
.attr("x", width/2)
.attr("y", -60)
.style("text-anchor", "middle")
.text("HEATMAP");
//Append credit at bottom
svg.append("text")
.attr("class", "credit")
.attr("x", width/2)
.attr("y", gridSize * (units.length+1) + 80)
.style("text-anchor", "middle");
//Extra scale since the color scale is interpolated
var countScale = d3.scaleLinear()
.domain([0, d3.max(newSample, function(d) {return d.level; })])
.range([0, width])
//Calculate the variables for the temp gradient
var numStops = 10;
countRange = countScale.domain();
countRange[2] = countRange[1] - countRange[0];
countPoint = [];
for(var i = 0; i < numStops; i++) {
countPoint.push(i * countRange[2]/(numStops-1) + countRange[0]);
}//for i
//Create the gradient
svg.append("defs")
.append("linearGradient")
.attr("id", "legendLevel")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%")
.selectAll("stop")
.data(d3.range(numStops))
.enter().append("stop")
.attr("offset", function(d,i) {
return countScale( countPoint[i] )/width;
})
.attr("stop-color", function(d,i) {
return colorScale( countPoint[i] );
});
var legendWidth = Math.min(width, 400);//the width of the legend
console.log(width);
//Color Legend container
var legendsvg = svg.append("g")
.attr("class", "legendWrapper")
.attr("transform", "translate(" + (width/2) + "," + (gridSize * 100 + 40) + ")");
//Draw the Rectangle
legendsvg.append("rect")
.attr("class", "legendRect")
.attr("x", -legendWidth/2)
.attr("y", 0)
.attr("width", legendWidth)
.attr("height", 10)
.style("fill", "url(#legendLevel)");
//Append title
legendsvg.append("text")
.attr("class", "legendTitle")
.attr("x", 0)
.attr("y", -10)
.style("text-anchor", "middle")
.text("Level");
//Set scale for x-axis
var xScale = d3.scaleLinear()
.range([-legendWidth/2, legendWidth/2])
.domain([ 0, d3.max(newSample, function(d) { return d.level; })] );
//Define x-axis
var xAxis = d3.axisBottom()
.ticks(5)
.scale(xScale);
//Set up X axis
legendsvg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (10) + ")")
.call(xAxis);
function drawCanvas(){
var elements = dataContainer.selectAll("custom.rect");
elements.each(function(d){
var node = d3.select(this);
context.beginPath();
context.fillStyle = node.attr("fillStyle");
context.rect(node.attr("x"), node.attr("y"), node.attr("width"), node.attr("height"));
context.fill();
context.closePath();
});
}
html { font-size: 100%; }
.timeLabel, .dayLabel {
font-size: 1rem;
fill: #AAAAAA;
font-weight: 300;
}
.title {
font-size: 1.8rem;
fill: #4F4F4F;
font-weight: 300;
}
.subtitle {
font-size: 1.0rem;
fill: #AAAAAA;
font-weight: 300;
}
.credit {
font-size: 1.2rem;
fill: #AAAAAA;
font-weight: 400;
}
.axis path, .axis tick, .axis line {
fill: none;
stroke: none;
}
.legendTitle {
font-size: 1.3rem;
fill: #4F4F4F;
font-weight: 300;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="trafficCongestions" class="trafficCongestions"></div>
As you're using svg to just show up the text and legend, I'd say you can absolute position the canvas on top of the SVG - by CSS.
Here are the CSS changes:
div#trafficCongestions {
position: relative;
}
canvas {
position: absolute;
top: 170px;
left: 100px;
}
You can do the above using d3 style as well `cause the margins are defined in the script. I just wanted to show that this is an option to use.
var units = [];
for(var unit_i = 0; unit_i<=101;){
if(unit_i==0){
units.push(1);
unit_i = unit_i + 5;
}
else{
units.push(unit_i);
unit_i = unit_i + 4;
}
}
var times = [];
for(var times_i = 0; times_i<=1440;){
if(times_i==0){
times.push(1);
times_i = times_i + 10;
}
else{
times.push(times_i);
times_i = times_i + 9;
}
}
var newSample = [{unit:null, timestamp: null, level: null}];
//by using below method we can observe the delay is not due to the data during insertion
for(var unit=1; unit<=99; unit++){
for(var timestamp = 1; timestamp<=100; timestamp++){
var i = Math.random() * 1400;
newSample.push({unit:unit, timestamp: timestamp, level:i});
}
}
var hours = 0;
var hoursIndicator = 0;
var margin = {
top: 170,
right: 100,
bottom: 70,
left: 100
};
var width = 2500,//optimized width
//gridSize = Math.floor(width / times.length),//optimized gridsize
gridSize = 10;//if 20 each interval will have 5
height = 50 * (units.length); //optimized, the greater the length, the greater the height
//console.log("this is gridSize:" + gridSize +", this is height: " + height + ", and this is width: " + width);
//SVG container
var svg = d3.select('.trafficCongestions')
.append("svg")
//.style("position", "absolute")
.attr("width", width + margin.left + margin.right)//optimization
.attr("height", height + margin.top + margin.bottom)//optimization
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = d3.select('.trafficCongestions').append("canvas")
.attr("id", "canvas")
.attr("width", width + margin.left + margin.right)//optimization
.attr("height", height + margin.top + margin.bottom);//optimization
var context = canvas.node().getContext("2d");
context.clearRect(0, 0, width, height);
var detachedContainer = document.createElement("custom");
var dataContainer = d3.select(detachedContainer);
//Reset the overall font size
var newFontSize = width * 62.5 / 900;
//heatmap drawing starts from here
var colorScale = d3.scaleLinear()
.domain([0, d3.max(newSample, function(d, i) {return d.level; })/2, d3.max(newSample, function(d, i) {return d.level; })])
.range(["#009933", "#FFCC00", "#990000"])//obtain the max data value of count
//y-axis (solely based on data of days)
var dayLabels = svg.selectAll(".dayLabel")
.data(units)
.enter().append("text")
.text(function (d) { return d; })
.attr("x", 0)
.attr("y", function (d, i) {
return (i) * (gridSize * 4)/*adjusts the interval distance with (n - 1) concept*/; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize + ")");
//x-axis (solely based on data of times)
var timeLabels = svg.selectAll(".timeLabel")
.data(times)
.enter().append("text")
.text(function(d, i) {
var hrs = Math.floor(d/60);
var mins = d%60;
if(hrs<10){
if(mins<10){
return "0"+hrs + ":0" + mins;
}
return "0"+ hrs + ":" + mins;
}
return hrs +":"+ mins;
})
.attr("x", function(d, i) { return i * (gridSize * 9)/*adjusts the interval distance with (n - 1) concept*/; })
.attr("y", 0)
.style("text-anchor", "middle")
.attr("transform", "translate(" + 1 + ", -6)")
var heatMap = dataContainer.selectAll("custom.rect")
.data(newSample)
.enter().append("custom")
.attr("x", function(d) {
return (d.timestamp - 1) * (gridSize);
})
.attr("y", function(d) {
//console.log(d.unit);
return (d.unit - 1) * (gridSize);
})
.classed("rect", true)
.attr("class", " rect bordered")
.attr("width", gridSize)
.attr("height", gridSize)
.attr("strokeStyle", "rgba(255,255,255, 0.6)")//to have the middle line or not
.attr("fillStyle", function(d,i){
return colorScale(d.level);
});
drawCanvas();
//Append title to the top
svg.append("text")
.attr("class", "title")
.attr("x", width/2)
.attr("y", -90)
.style("text-anchor", "middle")
.text("Sample Result");
svg.append("text")
.attr("class", "subtitle")
.attr("x", width/2)
.attr("y", -60)
.style("text-anchor", "middle")
.text("HEATMAP");
//Append credit at bottom
svg.append("text")
.attr("class", "credit")
.attr("x", width/2)
.attr("y", gridSize * (units.length+1) + 80)
.style("text-anchor", "middle");
//Extra scale since the color scale is interpolated
var countScale = d3.scaleLinear()
.domain([0, d3.max(newSample, function(d) {return d.level; })])
.range([0, width])
//Calculate the variables for the temp gradient
var numStops = 10;
countRange = countScale.domain();
countRange[2] = countRange[1] - countRange[0];
countPoint = [];
for(var i = 0; i < numStops; i++) {
countPoint.push(i * countRange[2]/(numStops-1) + countRange[0]);
}//for i
//Create the gradient
svg.append("defs")
.append("linearGradient")
.attr("id", "legendLevel")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%")
.selectAll("stop")
.data(d3.range(numStops))
.enter().append("stop")
.attr("offset", function(d,i) {
return countScale( countPoint[i] )/width;
})
.attr("stop-color", function(d,i) {
return colorScale( countPoint[i] );
});
var legendWidth = Math.min(width, 400);//the width of the legend
//console.log(width);
//Color Legend container
var legendsvg = svg.append("g")
.attr("class", "legendWrapper")
.attr("transform", "translate(" + (width/2) + "," + (gridSize * 100 + 40) + ")");
//Draw the Rectangle
legendsvg.append("rect")
.attr("class", "legendRect")
.attr("x", -legendWidth/2)
.attr("y", 0)
.attr("width", legendWidth)
.attr("height", 10)
.style("fill", "url(#legendLevel)");
//Append title
legendsvg.append("text")
.attr("class", "legendTitle")
.attr("x", 0)
.attr("y", -10)
.style("text-anchor", "middle")
.text("Level");
//Set scale for x-axis
var xScale = d3.scaleLinear()
.range([-legendWidth/2, legendWidth/2])
.domain([ 0, d3.max(newSample, function(d) { return d.level; })] );
//Define x-axis
var xAxis = d3.axisBottom()
.ticks(5)
.scale(xScale);
//Set up X axis
legendsvg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (10) + ")")
.call(xAxis);
function drawCanvas(){
var elements = dataContainer.selectAll("custom.rect");
elements.each(function(d){
var node = d3.select(this);
context.beginPath();
context.fillStyle = node.attr("fillStyle");
context.rect(node.attr("x"), node.attr("y"), node.attr("width"), node.attr("height"));
context.fill();
context.closePath();
});
}
html { font-size: 100%; }
div#trafficCongestions {
position: relative;
}
canvas {
position: absolute;
top: 170px;
left: 100px;
}
.timeLabel, .dayLabel {
font-size: 1rem;
fill: #AAAAAA;
font-weight: 300;
}
.title {
font-size: 1.8rem;
fill: #4F4F4F;
font-weight: 300;
}
.subtitle {
font-size: 1.0rem;
fill: #AAAAAA;
font-weight: 300;
}
.credit {
font-size: 1.2rem;
fill: #AAAAAA;
font-weight: 400;
}
.axis path, .axis tick, .axis line {
fill: none;
stroke: none;
}
.legendTitle {
font-size: 1.3rem;
fill: #4F4F4F;
font-weight: 300;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="trafficCongestions" class="trafficCongestions"></div>
Let me know if this makes sense. If not, let's look for other approaches.
Edit as per comments: (visual studio didn't support the CSS styling added by above approach)
Added the style using d3:
canvas.style('position', 'absolute').style('top', margin.top+'px').style('left', margin.left+'px')
And this worked.

Constraints on slider jquery + callng javascript function at page loadto call javascript function

I have some code which plots a certain mathematical function and enables the user to change certain specific parameters of the function on the fly enabling the user to see the change of it.
I have two questions with which I need some help.
i) I am using the jquery UI library which provides a slider. I use this slider to enable the user to change a specific parameter in a certain range. Normally there are no constraints on the parameter. However, in this case I need that the following equality holds min <= mode <= max. Currently I have solved this with some code in the slide event of the slider. The code basically checks the equality and if it is violated it sets the parameter to either its minimum or maximum possible value.
Visually, however, it is possible to slide further. However, after I the slider is let loose by the user, the change event has been called and I change the parameter to either its minimum or maximum possible value. Is it possible to make sure that also visually the slider is does not violates the constraint?
ii) When I load the page there is no plot visible yet on the page. That is because I seem to be unable to call the function redraw() when the page is loading, see the code at the bottom. Why is redraw() not being called?
Snippet, see below or http://jsfiddle.net/qraLt1p3/11/
<!DOCTYPE html>
<html lang="en">
<head>
<title>MPERT</title>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="//code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<style type="text/css">
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: transparent;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
clip-path: url(#clip);
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
#slider-pertmin, #slider-pertmode, #slider-pertmax {
float: right;
width: 120px;
margin: 7px;
}
.ui-slider-horizontal {
height: 8px;
width: 200px;
}
.ui-slider .ui-slider-handle {
height: 15px;
width: 5px;
padding-left: 5px;
}
#plot {
background-color:#f9f9f9;
border:solid 1px #ddd;
padding:10px;
width:250px;
}
#plotOptions {
background-color:#f9f9f9;
border:solid 1px #ddd;
padding:10px;
}
.block label {
display: inline-block;
width: 60px;
text-align: right;
}
</style>
<script type="text/javascript">
// Log-gamma function
gammaln = function gammaln(x)
{
var cof = [ 76.18009172947146, -86.50532032941677, 24.01409824083091,
-1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5];
var j = 0;
var ser = 1.000000000190015;
var xx, y, tmp;
tmp = (y = xx = x) + 5.5;
tmp -= (xx + 0.5) * Math.log(tmp);
for (; j < 6; j++)
ser += cof[j] / ++y;
return Math.log(2.5066282746310005 * ser / xx) - tmp;
};
// Gamma function
gammafn = function gammafn(x)
{
var p = [ -1.716185138865495, 24.76565080557592, -379.80425647094563,
629.3311553128184, 866.9662027904133, -31451.272968848367,
-36144.413418691176, 66456.14382024054];
var q = [ -30.8402300119739, 315.35062697960416, -1015.1563674902192,
-3107.771671572311, 22538.118420980151, 4755.8462775278811,
-134659.9598649693, -115132.2596755535];
var fact = false,
n = 0,
xden = 0,
xnum = 0,
y = x,
i,
z,
yi,
res,
sum,
ysq;
if (y <= 0) {
res = y % 1 + 3.6e-16;
if (res) {
fact = (!(y & 1) ? 1 : -1) * Math.PI / Math.sin(Math.PI * res);
y = 1 - y;
} else
return Infinity;
}
yi = y;
if (y < 1)
z = y++;
else
z = (y -= n = (y | 0) - 1) - 1;
for (i = 0; i < 8; ++i) {
xnum = (xnum + p[i]) * z;
xden = xden * z + q[i];
}
res = xnum / xden + 1;
if (yi < y)
res /= yi;
else if (yi > y) {
for (i = 0; i < n; ++i) {
res *= y;
y++;
}
}
if (fact)
res = fact / res;
return res;
};
// Beta function
betafn = function betafn(x,y)
{
// ensure arguments are positive
if (x <= 0 || y <= 0)
return undefined;
// make sure x + y doesn't exceed the upper limit of usable values
return (x + y > 170)
? Math.exp(betaln(x, y))
: gammafn(x) * gammafn(y) / gammafn(x + y);
};
// Natural logarithm of Beta function
betaln = function betaln(x, y)
{
return gammaln(x) + gammaln(y) - gammaln(x + y);
};
$(function() {
$("#slider-pertmin").slider({
range: "min",
value: 1,
min: 1,
max: 20,
step: 0.1,
slide: function(event,ui) {
if (ui.value > $("#slider-pertmode").slider("value")) {
$("#pertmin").val($("#slider-pertmode").slider("value") - 0.1);
$("#slider-pertmin").slider("value",$("#slider-pertmode").slider("value") - 0.1)
} else
$("#pertmin").val(ui.value);
redraw();
},
change: function(event,ui) {
$("#slider-pertmin").slider("value",$("#pertmin").val());
}
});
$("#slider-pertmode").slider({
range: "min",
value: 3,
min: 1,
max: 20,
step: 0.1,
slide: function(event,ui) {
if (ui.value < $("#slider-pertmin").slider("value")) {
$("#pertmode").val($("#slider-pertmin").slider("value") + 0.1);
$("#slider-pertmode").slider("value",$("#slider-pertmin").slider("value") + 0.1);
} else if (ui.value > $("#slider-pertmax").slider("value")) {
$("#pertmode").val($("#slider-pertmax").slider("value") - 0.1);
$("#slider-pertmode").slider("value",$("#slider-pertmax").slider("value") - 0.1);
} else
$("#pertmode").val(ui.value);
redraw();
},
change: function(event,ui) {
$("#slider-pertmode").slider("value",$("#pertmode").val());
}
});
$("#slider-pertmax").slider({
range: "min",
value: 6,
min: 1,
max: 20,
step: 0.1,
slide: function(event,ui) {
if (ui.value < $("#slider-pertmode").slider("value")) {
$("#pertmax").val($("#slider-pertmode").slider("value") + 0.1);
$("#slider-pertmax").slider("value",$("#slider-pertmode").slider("value") + 0.1);
} else
$("#pertmax").val(ui.value);
redraw();
},
change: function(event,ui) {
$("#slider-pertmax").slider("value",$("#pertmax").val());
}
});
$("#pertmin").val($("#slider-pertmin").slider("value"));
$("#pertmode").val($("#slider-pertmode").slider("value"));
$("#pertmax").val($("#slider-pertmax").slider("value"));
});
</script>
</head>
<body>
<div id="plot"></div>
<div id="plotOptions" class="ui-widget" style="width:250px">
<div class="block">
<label for="pertmin">min:</label> <input type="value" id="pertmin" style="border:none; background: transparent; width:40px;" />
<div class="ui-slider" id="slider-pertmin" />
</div>
<div class="block">
<label for="pertmode">mode:</label> <input type="value" id="pertmode" style="border:none; background: transparent; width:40px;" />
<div class="ui-slider" id="slider-pertmode" />
</div>
<div class="block">
<label for="pertmax">max:</label> <input type="value" id="pertmax" style="border:none; background: transparent; width:40px;" />
<div class="ui-slider" id="slider-pertmax" />
</div>
</div>
<script type="text/javascript">
var pert = {
min: 2,
mode: 4,
max: 9
};
pert.mu = (pert.min + 4*pert.mode + pert.max)/6;
pert.a1 = 6*(pert.mu - pert.min)/(pert.max - pert.min);
pert.a2 = 6*(pert.max - pert.mu)/(pert.max - pert.min);
pert.beta = betafn(pert.a1,pert.a2);
var data = [];
for (var k = pert.min; k < pert.max; k += 0.1) {
data.push({
x: k,
y: (1/pert.beta)*(Math.pow(k - pert.min,pert.a1 - 1) *
Math.pow(pert.max - k,pert.a2 - 1)) /
Math.pow(pert.max - pert.min,pert.a1 + pert.a2 - 1)
});
}
var margin = {
top: 20,
right: 20,
bottom: 35,
left: 50
},
width = 250 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([pert.min, pert.max])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, 1])
.range([height, 0]);
var xAxis1 = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var yAxis1 = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var xAxis2 = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var yAxis2 = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height, -height, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
var line = d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
})
.interpolate("linear");
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 1])
.on("zoom",redraw);
var svg = d3.select("#plot").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
// Add x grid
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(xGrid);
// Add y grid
svg.append("g")
.attr("class", "y grid")
.call(yGrid);
svg.append("g")
.attr("class", "x1 axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis1);
svg.append("g")
.attr("class", "y1 axis")
.call(yAxis1);
/* append additional X axis */
svg.append("g")
.attr("class", "x2 axis")
.attr("transform", "translate(" + [0, 0] + ")")
.call(xAxis2);
/* append additional y axis */
svg.append("g")
.attr("class", "y2 axis")
.attr("transform", "translate(" + [width, 0] + ")")
.call(yAxis2);
// Add x axis label
svg.append("text")
.attr("transform", "translate(" + (width / 2) + "," + (height + margin.bottom) + ")")
.style("font-size","15")
.style("text-anchor", "middle")
.text("x axis");
// Add y axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y",0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("font-size","15")
.style("text-anchor", "middle")
.text("y axis");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("path")
.attr("class", "line")
function redraw() {
pert.min = $("#slider-pertmin").slider("value");
pert.mode = $("#slider-pertmode").slider("value");
pert.max = $("#slider-pertmax").slider("value");
pert.mu = (pert.min + 4*pert.mode + pert.max)/6;
pert.a1 = 6*(pert.mu - pert.min)/(pert.max - pert.min);
pert.a2 = 6*(pert.max - pert.mu)/(pert.max - pert.min);
pert.beta = betafn(pert.a1,pert.a2);
x.domain([pert.min, pert.max]);
svg.select(".x1.axis").call(xAxis1);
svg.select(".y1.axis").call(yAxis1);
svg.select(".x2.axis").call(xAxis2);
svg.select(".y2.axis").call(yAxis2);
svg.select(".x.grid").call(xGrid);
svg.select(".y.grid").call(yGrid);
var data = [];
for (var k = pert.min; k < pert.max; k += 0.1) {
data.push({
x: k,
y: (1/pert.beta)*(Math.pow(k - pert.min,pert.a1 - 1) *
Math.pow(pert.max - k,pert.a2 - 1)) /
Math.pow(pert.max - pert.min,pert.a1 + pert.a2 - 1)
});
}
d3.select(".line").attr("d",line(data));
}
redraw();
</script>
</body>
</html>
1.) On your UI question, I would use a range slider to represent my min/max. I'd then get a bit fancy and update the min/max (and width) of my mode slider to force it always between the min/max.
2.) You are calling your initial redraw before the sliders have been initialized. This is because slider initialization is done inside the document.ready event while your initial redraw is not. A simple fix is:
$(function() {
redraw();
});
To expand on my answer a bit, here's a rough example of how I would implement your first question. Note, I swapped out the jquery slider with a d3 one based on this example. I always look for a d3 based answer.
<!DOCTYPE html>
<html lang="en">
<head>
<title>MPERT</title>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<style type="text/css">
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: transparent;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
clip-path: url(#clip);
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
circle {
-webkit-transition: fill-opacity 250ms linear;
}
.selecting circle {
fill-opacity: .2;
}
.selecting circle.selected {
stroke: #f00;
}
.resize path {
fill: #666;
fill-opacity: .8;
stroke: #000;
stroke-width: 1.5px;
}
.brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
#plot {
background-color: #f9f9f9;
border: solid 1px #ddd;
padding: 10px;
width: 250px;
}
#plotOptions {
background-color: #f9f9f9;
border: solid 1px #ddd;
padding: 10px;
}
.block label {
display: inline-block;
width: 60px;
text-align: right;
}
</style>
<script type="text/javascript">
// Log-gamma function
gammaln = function gammaln(x) {
var cof = [76.18009172947146, -86.50532032941677, 24.01409824083091, -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5];
var j = 0;
var ser = 1.000000000190015;
var xx, y, tmp;
tmp = (y = xx = x) + 5.5;
tmp -= (xx + 0.5) * Math.log(tmp);
for (; j < 6; j++)
ser += cof[j] / ++y;
return Math.log(2.5066282746310005 * ser / xx) - tmp;
};
// Gamma function
gammafn = function gammafn(x) {
var p = [-1.716185138865495, 24.76565080557592, -379.80425647094563,
629.3311553128184, 866.9662027904133, -31451.272968848367, -36144.413418691176, 66456.14382024054
];
var q = [-30.8402300119739, 315.35062697960416, -1015.1563674902192, -3107.771671572311, 22538.118420980151, 4755.8462775278811, -134659.9598649693, -115132.2596755535];
var fact = false,
n = 0,
xden = 0,
xnum = 0,
y = x,
i,
z,
yi,
res,
sum,
ysq;
if (y <= 0) {
res = y % 1 + 3.6e-16;
if (res) {
fact = (!(y & 1) ? 1 : -1) * Math.PI / Math.sin(Math.PI * res);
y = 1 - y;
} else
return Infinity;
}
yi = y;
if (y < 1)
z = y++;
else
z = (y -= n = (y | 0) - 1) - 1;
for (i = 0; i < 8; ++i) {
xnum = (xnum + p[i]) * z;
xden = xden * z + q[i];
}
res = xnum / xden + 1;
if (yi < y)
res /= yi;
else if (yi > y) {
for (i = 0; i < n; ++i) {
res *= y;
y++;
}
}
if (fact)
res = fact / res;
return res;
};
// Beta function
betafn = function betafn(x, y) {
// ensure arguments are positive
if (x <= 0 || y <= 0)
return undefined;
// make sure x + y doesn't exceed the upper limit of usable values
return (x + y > 170) ? Math.exp(betaln(x, y)) : gammafn(x) * gammafn(y) / gammafn(x + y);
};
// Natural logarithm of Beta function
betaln = function betaln(x, y) {
return gammaln(x) + gammaln(y) - gammaln(x + y);
};
</script>
</head>
<body>
<div id="plot"></div>
<div id="plotOptions" class="ui-widget" style="width:250px">
</div>
<script>
var margin = {top: 5, right: 20, bottom: 5, left: 20},
width = 250 - margin.left - margin.right,
height = 50 - margin.top - margin.bottom
min = 0, max = 20;
var brushX = d3.scale.linear()
.range([0, width])
.domain([min, max]);
var brush = d3.svg.brush()
.x(brushX)
.extent([0, 20])
.on("brush", brushmove);
var arc = d3.svg.arc()
.outerRadius(height / 3)
.startAngle(0)
.endAngle(function(d, i) { return i ? -Math.PI : Math.PI; });
var svg = d3.select("#plotOptions").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height/2 + ")")
.call(d3.svg.axis().scale(brushX).orient("bottom"));
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush);
brushg.selectAll(".resize").append("path")
.attr("transform", "translate(0," + height / 2 + ")")
.attr("d", arc);
brushg.selectAll("rect")
.attr("height", height);
var mode = 10;
var modeRect = svg.append("rect")
.attr("height", height)
.attr("width", 10)
.attr("y", 0)
.attr("x", brushX(mode) - 5)
.style("fill", "steelblue")
.style("opacity", 0.5);
var drag = d3.behavior.drag()
.on("drag", modeDrag);
modeRect.call(drag);
function modeDrag(modeX){
var xPos = d3.mouse(this)[0],
tmp = brushX.invert(xPos);
if (tmp > min && tmp < max){
mode = tmp;
modeRect
.attr('x', xPos);
redraw();
}
}
function brushmove() {
console.log(d3.event)
var extent = brush.extent();
min = extent[0];
max = extent[1];
if (min >= mode){
mode = min;
modeRect.attr('x', brushX(mode) - 5);
} else if (max <= mode){
mode = max;
modeRect.attr('x', brushX(mode) - 5);
}
redraw();
}
</script>
<script type="text/javascript">
var pert = {
min: 2,
mode: 4,
max: 9
};
pert.mu = (pert.min + 4 * pert.mode + pert.max) / 6;
pert.a1 = 6 * (pert.mu - pert.min) / (pert.max - pert.min);
pert.a2 = 6 * (pert.max - pert.mu) / (pert.max - pert.min);
pert.beta = betafn(pert.a1, pert.a2);
var data = [];
for (var k = pert.min; k < pert.max; k += 0.1) {
data.push({
x: k,
y: (1 / pert.beta) * (Math.pow(k - pert.min, pert.a1 - 1) *
Math.pow(pert.max - k, pert.a2 - 1)) /
Math.pow(pert.max - pert.min, pert.a1 + pert.a2 - 1)
});
}
var margin = {
top: 20,
right: 20,
bottom: 35,
left: 50
},
width = 250 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([pert.min, pert.max])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, 1])
.range([height, 0]);
var xAxis1 = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var yAxis1 = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var xAxis2 = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var yAxis2 = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height, -height, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
var line = d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
})
.interpolate("linear");
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 1])
.on("zoom", redraw);
var svg = d3.select("#plot").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
// Add x grid
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(xGrid);
// Add y grid
svg.append("g")
.attr("class", "y grid")
.call(yGrid);
svg.append("g")
.attr("class", "x1 axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis1);
svg.append("g")
.attr("class", "y1 axis")
.call(yAxis1);
/* append additional X axis */
svg.append("g")
.attr("class", "x2 axis")
.attr("transform", "translate(" + [0, 0] + ")")
.call(xAxis2);
/* append additional y axis */
svg.append("g")
.attr("class", "y2 axis")
.attr("transform", "translate(" + [width, 0] + ")")
.call(yAxis2);
// Add x axis label
svg.append("text")
.attr("transform", "translate(" + (width / 2) + "," + (height + margin.bottom) + ")")
.style("font-size", "15")
.style("text-anchor", "middle")
.text("x axis");
// Add y axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("font-size", "15")
.style("text-anchor", "middle")
.text("y axis");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("path")
.attr("class", "line")
function redraw() {
pert.min = min - 0.001;
pert.mode = mode;
pert.max = max + 0.001;
pert.mu = (pert.min + 4 * pert.mode + pert.max) / 6;
pert.a1 = 6 * (pert.mu - pert.min) / (pert.max - pert.min);
pert.a2 = 6 * (pert.max - pert.mu) / (pert.max - pert.min);
pert.beta = betafn(pert.a1, pert.a2);
x.domain([pert.min, pert.max]);
svg.select(".x1.axis").call(xAxis1);
svg.select(".y1.axis").call(yAxis1);
svg.select(".x2.axis").call(xAxis2);
svg.select(".y2.axis").call(yAxis2);
svg.select(".x.grid").call(xGrid);
svg.select(".y.grid").call(yGrid);
var data = [];
for (var k = pert.min; k < pert.max; k += 0.1) {
data.push({
x: k,
y: (1 / pert.beta) * (Math.pow(k - pert.min, pert.a1 - 1) *
Math.pow(pert.max - k, pert.a2 - 1)) /
Math.pow(pert.max - pert.min, pert.a1 + pert.a2 - 1)
});
}
d3.select(".line").attr("d", line(data));
}
redraw();
</script>
</body>
</html>

Categories