I am trying to use d3-geo-voronoi to display vector tile data using d3-tile. My initial attempt in displaying the data, with fill set to "none" worked, which was very exiting!
Voronoi tile map without color fill
However, when I attempted to fill the polygons, some of the tiles were distorted.
Voronoi tile map with color fill
I've not been able to figure out why this is happening. I checked the svg's in the dom, and everything looks correct. The svg's are correct where there are no polygons, they are just not being rendered properly, possibly they are being covered up. Below is the code I used:
const d3 = require('d3');
const d3tile = require('d3-tile');
const d3geovoronoi = require('d3-geo-voronoi');
const vt2geojson = require('#mapbox/vt2geojson');
const pi = Math.PI,
tau = 2 * pi;
const width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
const map = d3.select("body").append("div")
.attr("class", "map")
.style("width", width + "px")
.style("height", height + "px")
.on("mousemove", mousemoved);
let projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
let center = projection([-76.3, 38.794745]);
const tile = d3tile.tile()
.size([width, height]);
const zoom = d3.zoom()
.scaleExtent([1 << 15, 1 << 24])
.on("zoom", zoomed);
const svg = map.append("g")
.attr("pointer-events", "none")
.attr("class", "svg");
const info = map.append("g")
.attr("class", "info");
const ramp = d3.scaleLinear().domain([0.05,0.07]).interpolate(d3.interpolateHcl).range(['#34d8eb','#3a34eb']).unknown("#5c5752")
map.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 21)
.translate(-center[0], -center[1]));
function zoomed() {
let transform = d3.event.transform;
let tiles = tile(transform);
let image = svg
.style("transform", stringify(tiles.scale, tiles.translate))
.selectAll(".tile")
.data(tiles, function(d) { return d; })
.enter().append("svg")
.attr("class", "tile")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-width", "0.5")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.style("left", function(d) { return d[0] * 256 + "px"; })
.style("top", function(d) { return d[1] * 256 + "px"; })
.each(function(d) { this._xhr = render(d, this); });
projection
.scale(transform.k / tau)
.translate([transform.x, transform.y]);
}
function render(d, xnode) {
let k = Math.pow(2, d[2]) * 256;
vt2geojson({
uri: 'http://localhost:7800/public.r3sim_fort_temp/'+d[2]+'/'+d[0]+'/'+d[1]+'.pbf?properties=node,zeta,mask,bathymetry'
}, function (err, json) {
if (err) throw err;
d3.select(xnode)
.selectAll("path")
.data(d3geovoronoi.geoVoronoi().polygons(json).features)
.enter().append("path")
//.attr('fill', 'none')
.attr("fill", function(d) {return ramp(d.properties.site.properties.zeta)})
.attr("stroke", "#fff")
.attr("stroke-width", "0.5")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", d3.geoPath()
.projection(d3.geoMercator()
.scale(k / tau)
.translate([k / 2 - d[0] * 256, k / 2 - d[1] * 256])
.precision(0)));
})
}
function stringify(scale, translate) {
const k = scale / 256, r = scale % 1 ? Number : Math.round;
return "matrix3d(" + [k, 0, 0, 0, 0, k, 0, 0, 0, 0, k, 0, r(translate[0] * scale), r(translate[1] * scale), 0, 1 ] + ")";
}
function mousemoved() {
info.text(formatLocation(projection.invert(d3.mouse(this)), d3.zoomTransform(this).k));
}
function formatLocation(p, k) {
const format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + "°N") + " "
+ (p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E");
}
<!doctype html>
<head>
<meta charset="utf-8">
<title>D3 V5 Vector Tile Example</title>
<style>
body {
margin: 0;
}
.map {
background: #5c5752;
position: relative;
overflow: hidden;
}
.svg {
position: absolute;
will-change: transform;
}
.tile {
position: absolute;
width: 256px;
height: 256px;
}
.info {
position: absolute;
bottom: 10px;
left: 10px;
}
</style>
</head>
<body>
<script src="bundle.js"></script>
</body>
In this example I filled the polygons with varying color values. However, the exact same distortions occur if I use a single color value. The distortions are also always in the same place, if I reload all of the data.
I did a deeper dive into the data, and found the bad svg path, and then found the data related to it. It looks like d3.geo.voronoi is producing some bad coordinates, but the input data looks okay. Below are two printouts of node 1192. The first is the input geojson data, showing the coordinates, and the second is the voronoi geometry. The voronoi geometry contains longitude values in the eastern hemisphere (103.86...), which is way outside of the range of the data. I'm still trying to determine why these bad values are being produced. Again, the input coordinates look correct, but possibly it is other data that goes into the voronoi calculation?
1192
{…}
geometry: {…}
coordinates: (2) […]
0: -76.12801194190979
1: 38.78622954627738
length: 2
<prototype>: Array []
type: "Point"
<prototype>: Object { … }
properties: Object { node: 180407, zeta: "NaN", mask: "True", … }
type: "Feature"
<prototype>: Object { … }
1192 (11) […]
0: Array [ 103.86695733932268, -44.964779133003304 ]
1: Array [ -76.13308210176842, 38.75793814039401 ]
2: Array [ -76.13020999558496, 38.782688154120585 ]
3: Array [ -76.12890669699081, 38.78647064351637 ]
4: Array [ -76.12807302385534, 38.786723650244355 ]
5: Array [ -76.12754554182737, 38.78651000385868 ]
6: Array [ -76.12640847594942, 38.78408839960177 ]
7: Array [ -76.11435851540921, 38.636536130021334 ]
8: Array [ 103.858096036925, -39.00570100251519 ]
9: Array [ 103.860092112702, -39.367933188411186 ]
10: Array [ 103.86695733932268, -44.964779133003304 ]
length: 11
<prototype>: []
Related
I'm trying to show a line and % changes in a single highchart plot, but the changes are very little and It can't be seen in the plot. I made a simplified code to show my problem:
a <- c(300,200, 400, 10, 40, 80)
b <- c(0.8, 2, -2, -1.5, -1.1, 2)
d<-cbind(a,b)
dt <- seq(as.Date("2018-01-01"), as.Date("2018-01-06"), by = "days")
ts <- xts(d, dt )
highchart(type="stock") %>%
hc_add_series(ts$a,
type = "line",
color="black") %>%
hc_add_series(ts$b,
type = "lollipop",
color="red")
I need to increase the size of "ts$b" in the plot, how can I do it? I also tried with two axis, but It seems doesn't solve the problem.
I see two solutions to achieve that.
The first you mentioned - using two yAxis and manipulating their height and top distance.
Example JS code:
yAxis: [{
height: '90%',
opposite: false
},
{
visible: false,
top: '83%',
height: '15%',
}
]
Demo:
https://jsfiddle.net/BlackLabel/0826r7sh/
Another way is using a modified logarithmic axis. Negative values can't be plotted on a log axis, because by nature, the axis will only show positive values. In that case you need to use a custom extension according to the following thread:
Highcharts negative logarithmic scale solution stopped working
(function(H) {
H.addEvent(H.Axis, 'afterInit', function() {
const logarithmic = this.logarithmic;
if (logarithmic && this.options.custom.allowNegativeLog) {
// Avoid errors on negative numbers on a log axis
this.positiveValuesOnly = false;
// Override the converter functions
logarithmic.log2lin = num => {
const isNegative = num < 0;
let adjustedNum = Math.abs(num);
if (adjustedNum < 10) {
adjustedNum += (10 - adjustedNum) / 10;
}
const result = Math.log(adjustedNum) / Math.LN10;
return isNegative ? -result : result;
};
logarithmic.lin2log = num => {
const isNegative = num < 0;
let result = Math.pow(10, Math.abs(num));
if (result < 10) {
result = (10 * (result - 1)) / (10 - 1);
}
return isNegative ? -result : result;
};
}
});
}(Highcharts));
.
yAxis: {
type: 'logarithmic',
custom: {
allowNegativeLog: true
}
},
Demo
https://jsfiddle.net/BlackLabel/nw6osucm/
I am building a component that will display any image inside a parent div and allows dragging of the image (when larger than the div) as well as scaling on both a double click or pinch on mobile. I am using inline style changes on the image to test the behavior and so far everything works as I wanted...except that when I change the image transform:scale() all the calculations that effectively set the correct limits to prevent offsetting an image outside the parent div no longer behave as expected. It does work perfectly if I keep the scale=1.
So for example, with a parent Div width and height of 500/500 respectively, and an image that say is 1500x1000, I prevent any "over"offsetting when dragging the image by setting limits of leftLimit = -(1500-500) = -1000 and topLimit = -(1000-500) = -500. This works perfectly at the initial scale of 1. However, I have a dblClick event that scales the image upwards at .5 intervals, and once the scale changes from 1 to any other number, the methods I use above to calculate offset Limits are no longer value. So for example if I increase the scale to 2, in theory that same 1500x1000 image in a 500x500 div should NOW have leftLimit = -(3000-500) = -2500 and topLimit = -(2000-500) = 1500. But these new calculated limits allow the image to be dragged right out of the parent div region. For reference here is the code. Any help or methods for testing what's actually going on would be very much appreciated.
Note the image is being loaded as a file for test, it's a fairly large base64 string. The code is as follows (btw, I am figuring my use of so many 'state' variables probably exposes my ignorance of how such values could/should really persist across renderings. I am still quite new to React) :
import * as React from 'react'
import * as types from '../../types/rpg-types'
import imgSource from './testwebmap.jpg'
import * as vars from '../../data/mapImage'
const testImg = require('./testwebmap.jpg')
export default function MyMapImage() {
let divRef = React.useRef<HTMLDivElement>(null)
let imgRef = React.useRef<HTMLImageElement>(null)
const [loaded, setLoaded] = React.useState<boolean>(false)
const [imgTop, setImgTop] = React.useState<number>(0)
const [imgLeft, setImgLeft] = React.useState<number>(0)
const [scHeight, setSCHeight] = React.useState<number>(100)
const [scWidth, setSCWidth] = React.useState<number>(100)
const [imgScale, setImgScale] = React.useState<number>(1)
const [natHeight, setNatHeight] = React.useState<number>(100)
const [natWidth, setNatWidth] = React.useState<number>(100)
const [oldXCoord, setOldXCoord] = React.useState<number>(0)
const [oldYCoord, setOldYCoord] = React.useState<number>(0)
const [topLimit, setTopLimit] = React.useState<number>(0)
const [leftLimit, setLeftLimit] = React.useState<number>(0)
const [isScaling, setIsScaling] = React.useState<boolean>(false)
const [isDragging, setIsDragging] = React.useState<boolean>(false)
const [isFirstPress, setIsFirstPress] = React.useState<boolean>(false)
const [accel, setAccel] = React.useState<number>(1)
const [touchDist, setTouchDist] = React.useState<number>(0)
const [cfg, setCfg] = React.useState<types.ImageConfig>({
img: '',
imgTOP: 0,
imgLEFT: 0,
offsetX: 0,
offsetY: 0,
isFirstPress: true,
isDragging: false,
isScaling: false,
divHeight: 500,
divWidth: 500,
topLimit: 0,
leftLimit: 0,
isLoaded: true,
oldMouseX: 0,
oldMouseY: 0,
touchDist: 0,
})
const setNewImageLimits = () => {
const img = imgRef
let heightLimit: number
let widthLimit: number
console.log(`imgScale is: ${imgScale}`)
//console.log(`current offsets: ${imgLeft}:${imgTop}`)
console.log(`img width/Height: ${img.current?.width}:${img.current?.height}`)
console.log(img)
img.current
? (heightLimit = Math.floor(imgScale * img.current.naturalHeight - cfg.divHeight))
: (heightLimit = 0)
img.current
? (widthLimit = Math.floor(imgScale * img.current.naturalWidth - cfg.divWidth))
: (widthLimit = 0)
setTopLimit(-heightLimit)
setLeftLimit(-widthLimit)
setImgLeft(0)
setImgTop(0)
console.log(
'New Image limits set with topLimit:' + heightLimit + ' and leftLimit:' + widthLimit
)
}
const handleImageLoad = () => {
if (imgRef) {
const img = imgRef
//console.log(imgRef)
let heightLimit: number
let widthLimit: number
img.current ? (heightLimit = img.current.naturalHeight - cfg.divHeight) : (heightLimit = 0)
img.current ? (widthLimit = img.current.naturalWidth - cfg.divWidth) : (widthLimit = 0)
setTopLimit(-heightLimit)
setLeftLimit(-widthLimit)
setNatHeight(img.current ? img.current.naturalHeight : 0)
setNatWidth(img.current ? img.current.naturalWidth : 0)
setSCHeight(img.current ? img.current.naturalHeight : 0)
setSCWidth(img.current ? img.current.naturalWidth : 0)
console.log('Image Loaded with topLimit:' + heightLimit + ' and leftLimit:' + widthLimit)
}
}
React.useEffect(() => {
if (imgRef.current?.complete) {
handleImageLoad()
}
}, [])
React.useEffect(() => {
setNewImageLimits()
console.log(`imgScale is: ${imgScale}`)
console.log('Image has with topLimit:' + topLimit + ' and leftLimit:' + leftLimit)
}, [imgScale])
function distance(e: any) {
let zw = e.touches[0].pageX - e.touches[1].pageX
let zh = e.touches[0].pageY - e.touches[1].pageY
if (zw * zw + zh * zh != 0) {
return Math.sqrt(zw * zw + zh * zh)
} else return 0
}
function setCoordinates(e: any) {
let canMouseX: number
let canMouseY: number
if (e?.nativeEvent?.clientX && e?.nativeEvent?.clientY) {
//console.log(e)
//canMouseX = parseInt(e.clientX - cfg.offsetX)
canMouseX = e.nativeEvent.clientX - cfg.offsetX
canMouseY = e.nativeEvent.clientY - cfg.offsetY
//console.log(`${canMouseX}:${canMouseY}`)
} else if (e?.nativeEvent?.targetTouches) {
canMouseX = e.nativeEvent.targetTouches.item(0)?.clientX - cfg.offsetX
canMouseY = e.nativeEvent.targetTouches.item(0)?.clientY - cfg.offsetY
// This isn't doing anything (noticeable)
// e.preventDefault();
} else return {}
return {
canMouseX,
canMouseY,
}
}
const handleMouseUp = (e: any) => {
let { canMouseX, canMouseY } = setCoordinates(e)
setIsScaling(false)
setIsDragging(false)
setIsFirstPress(true)
setAccel(1)
console.log('Mouse UP Event function')
}
const handleMouseDown = (e: any) => {
const { canMouseX, canMouseY } = setCoordinates(e)
//console.log('Mouse DOWN Event function')
e.preventDefault()
//console.log(`Mouse Down ${canMouseX}:${canMouseY}`)
canMouseX ? setOldXCoord(canMouseX) : setOldXCoord(0)
canMouseY ? setOldYCoord(canMouseY) : setOldYCoord(0)
setIsDragging(true)
setCfg({ ...cfg, isDragging: true })
if (e?.targetTouches) {
e.preventDefault()
if (e?.nativeEvent?.touches?.length > 1) {
// detected a pinch
setTouchDist(distance(e))
setCfg({ ...cfg, touchDist: distance(e), isScaling: true })
setIsScaling(true)
setIsDragging(false)
} else {
// set the drag flag
setIsScaling(false)
setIsDragging(true)
}
}
setIsFirstPress(false)
setCfg({ ...cfg, isFirstPress: true })
}
const handleDoubleClick = (e: any) => {
const { canMouseX, canMouseY } = setCoordinates(e)
if (imgScale === 3) {
setImgScale(1)
} else {
setImgScale(imgScale + 0.5)
}
}
const handleMouseMove = (e: any) => {
let scaling = isScaling
let dragging = isDragging
let tempImgScale: number = 1
const { canMouseX, canMouseY } = setCoordinates(e)
let yDiff: number
let xDiff: number
let newLeft: number
let newTop: number
if (e.targetTouches) {
e.preventDefault()
if (e.touches.length > 1) {
//detected a pinch
setIsScaling(true)
setIsDragging(false)
scaling = true
} else {
setIsScaling(false)
setIsDragging(true)
}
}
//console.log(`isScaling : ${isScaling}`)
if (scaling) {
//...adding rndScaleTest to force processing of scaling randomly
let dist = distance(e)
//Can't divide by zero, so return dist in denom. if touchDist still at initial 0 value
tempImgScale = dist / (touchDist === 0 ? dist : touchDist)
//console.log(`imgScale is: ${imgScale}`)
if (tempImgScale < 1) tempImgScale = 1 //for now no scaling down allowed...
if (tempImgScale > 2) tempImgScale = 2 //...and scaling up limited to 2.5x
setSCHeight(Math.floor(imgScale * natHeight))
setSCWidth(Math.floor(imgScale * natWidth))
setImgScale(tempImgScale)
setTouchDist(dist)
}
// if the drag flag is set, clear the canvas and draw the image
if (isDragging) {
yDiff = canMouseY && oldYCoord ? accel * (canMouseY - oldYCoord) : 0
xDiff = canMouseX && oldXCoord ? accel * (canMouseX - oldXCoord) : 0
if (imgLeft + xDiff <= leftLimit) {
setImgLeft(leftLimit)
} else if (imgLeft + xDiff >= 0) {
setImgLeft(0)
} else setImgLeft(imgLeft + xDiff)
if (imgTop + yDiff <= topLimit) {
setImgTop(topLimit)
} else if (imgTop + yDiff >= 0) {
setImgTop(0)
} else setImgTop(imgTop + yDiff)
if (accel < 4) {
setAccel(accel + 1)
}
}
//console.log('Mouse **MOVE Event function')
setOldXCoord(canMouseX || 0)
setOldYCoord(canMouseY || 0)
}
const handleMouseLeave = (e: any) => {
setIsScaling(false)
setIsDragging(false)
setIsFirstPress(true)
setAccel(1)
console.log('Mouse LEAVE Event function')
}
return (
<div>
<div className="portrait">
<div
ref={divRef}
className="wrapper"
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onDoubleClick={handleDoubleClick}
>
<img
ref={imgRef}
src={`data:image/jpeg;base64,${vars.bigImage}`}
style={{
transform: `scale(${imgScale})`,
transformOrigin: `top left`,
objectPosition: `${imgLeft}px ${imgTop}px`,
}}
onLoad={handleImageLoad}
/>
</div>
</div>
<span>{`imgLeft: ${imgLeft}px `}</span>
<span>{`imgTop: ${imgTop}px `}</span>
</div>
)
}
In the end, the problem does lie with the use of Scale(), and I could not rely on it for purposes of tracking position and offset. So I ended up explicitly scaling 'height' and 'width' for the img and keeping the scaling value as a reference when required to calculate new values for these. The parent Div and Img essentially now look like this:
<div
ref={divRef}
className="wrapper"
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onTouchEnd={handleMouseUp}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onDoubleClick={handleDoubleClick}
>
<img
ref={imgRef}
src={`data:image/jpeg;base64,${vars.bigImage}`}
style={{
transform: `translate(${imgLeft}px, ${imgTop}px)`,
height: `${scHeight}px`,
width: `${scWidth}px)`,
transformOrigin: `top left`,
}}
onLoad={handleImageLoad}
/>
</div>
This works fine, calculated offsets match the expected values, and translation of the image within the parent div behaves as expected.
Updates
Updated fiddle to simplify what is going on:
added four buttons to move the stick, each button increments the value by 30 in the direction
plotted x and y axis
red line is the stick, with bottom end coordinates at (ax,ay) and top end coordinates at (bx,by)
green line is (presumably) previous position of the stick, with bottom end coordinates at (ax, ay) and top end coordinates at (bx0, by0)
So, after having my ninja moments. I'm still nowhere near understanding the sorcery behind unknownFunctionA and unknownFunctionB
For the sake of everyone (all two of you) here is what I've sort of learnt so far
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
In the unknownFunctionB above, variable o is length of the red sitck.
Still don't understand
What is variable i and how is (bx,by) calculated? essentially:
bx = bx - (bx - ax) * 0.5 * 0.5
by = by - (by - ay) * 0.5 * 0.5
In unknownFunctionA what are those magic numbers 1.825 and 0.825?
Below is irrelevant
I'm trying to deconstruct marker drag animation used on smartypins
I've managed to get the relevant code for marker move animation but I'm struggling to learn how it all works, especially 2 functions (that I've named unknownFunctionA and unknownFunctionB)
Heres the StickModel class used on smartypins website, unminified to best of my knowledge
function unknownFunctionA(e) {
var t = 1.825
, n = .825
, a = t * e.x - n * e.x0
, o = t * e.y - n * e.y0 - 5;
e.x0 = e.x,
e.y0 = e.y,
e.x = a,
e.y = o;
}
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
function StickModel() {
this._props = function(e) {
return {
length: e,
lengthSq: e * e,
a: {
x: 0,
y: 0
},
b: {
x: 0,
y: 0 - e,
x0: 0,
y0: 0 - e
},
angle: 0
}
}
(60)
}
var radianToDegrees = 180 / Math.PI;
StickModel.prototype = {
pos: {
x: 0,
y: 0
},
angle: function() {
return this._props.angle
},
reset: function(e, t) {
var n = e - this._props.a.x
, a = t - this._props.a.y;
this._props.a.x += n,
this._props.a.y += a,
this._props.b.x += n,
this._props.b.y += a,
this._props.b.x0 += n,
this._props.b.y0 += a
},
move: function(e, t) {
this._props.a.x = e,
this._props.a.y = t
},
update: function() {
unknownFunctionA(this._props.b),
unknownFunctionB(this._props),
this.pos.x = this._props.a.x,
this.pos.y = this._props.a.y;
var e = this._props.b.x - this._props.a.x
, t = this._props.b.y - this._props.a.y
, o = Math.atan2(t, e);
this._props.angle = o * radianToDegrees;
}
}
StickModel.prototype.constructor = StickModel;
Fiddle link with sample implementation on canvas: http://jsfiddle.net/vff1w82w/3/
Again, Everything works as expected, I'm just really curious to learn the following:
What could be the ideal names for unknownFunctionA and unknownFunctionB and an explanation of their functionality
What are those magic numbers in unknownFunctionA (1.825 and .825) and .5 in unknownFunctionB.
Variable o in unknownFunctionB appears to be hypotenuse. If that's the case, then what exactly is i = (o - e.length) / o in other words, i = (hypotenuse - stickLength) / hypotenuse?
First thing I'd recommend is renaming all those variables and methods until they start making sense. I also removed unused code.
oscillator
adds wobble to the Stick model by creating new position values for the Stick that follows the mouse
Exaggerates its movement by multiplying its new position by 1.825 and also subtracting the position of an "echo" of its previous position multiplied by 0.825. Sort of looking for a middle point between them. Helium makes the stick sit upright.
overshooter minus undershooter must equal 1 or you will have orientation problems with your stick. overshooter values above 2.1 tend to make it never settle.
seekerUpdate
updates the seeker according to mouse positions.
The distance_to_cover variable measures the length of the total movement. You were right: hypothenuse (variable o).
The ratio variable calculates the ratio of the distance that can be covered subtracting the size of the stick. The ratio is then used to limit the adjustment of the update on the seeker in both directions (x and y). That's how much of the update should be applied to prevent overshooting the target.
easing slows down the correct updates.
There are lots of interesting info related to vectors on the book The nature of code.
function oscillator(seeker) {
var overshooter = 1.825;
var undershooter = .825;
var helium = -5;
var new_seeker_x = overshooter * seeker.x - undershooter * seeker.echo_x;
var new_seeker_y = overshooter * seeker.y - undershooter * seeker.echo_y + helium;
seeker.echo_x = seeker.x;
seeker.echo_y = seeker.y;
seeker.x = new_seeker_x;
seeker.y = new_seeker_y;
}
function seekerUpdate(stick) {
var dX = stick.seeker.x - stick.mouse_pos.x;
var dY = stick.seeker.y - stick.mouse_pos.y;
var distance_to_cover = Math.sqrt(dX * dX + dY * dY);
var ratio = (distance_to_cover - stick.length) / distance_to_cover;
var easing = .25;
stick.seeker.x -= dX * ratio * easing;
stick.seeker.y -= dY * ratio * easing;
}
function StickModel() {
this._props = function(length) {
return {
length: length,
lengthSq: length * length,
mouse_pos: {
x: 0,
y: 0
},
seeker: {
x: 0,
y: 0 - length,
echo_x: 0,
echo_y: 0 - length
}
}
}(60)
}
StickModel.prototype = {
move: function(x, y) {
this._props.mouse_pos.x = x;
this._props.mouse_pos.y = y;
},
update: function() {
oscillator(this._props.seeker);
seekerUpdate(this._props);
}
};
StickModel.prototype.constructor = StickModel;
// Canvas to draw stick model coordinates
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
canvas.width = window.outerWidth;
canvas.height = window.outerHeight;
var canvasCenterX = Math.floor(canvas.width / 2);
var canvasCenterY = Math.floor(canvas.height / 2);
context.translate(canvasCenterX, canvasCenterY);
var stickModel = new StickModel();
draw();
setInterval(function() {
stickModel.update();
draw();
}, 16);
$(window).mousemove(function(e) {
var mouseX = (e.pageX - canvasCenterX);
var mouseY = (e.pageY - canvasCenterY);
stickModel.move(mouseX, mouseY);
stickModel.update();
draw();
});
function draw() {
context.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
// red line from (ax, ay) to (bx, by)
context.beginPath();
context.strokeStyle = "#ff0000";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.fillText('mouse_pos x:' + stickModel._props.mouse_pos.x + ' y: ' + stickModel._props.mouse_pos.y, stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.fillText('seeker x:' + stickModel._props.seeker.x + ' y: ' + stickModel._props.seeker.y, stickModel._props.seeker.x - 30, stickModel._props.seeker.y);
context.lineWidth = 1;
context.stroke();
context.closePath();
// green line from (ax, ay) to (bx0, by0)
context.beginPath();
context.strokeStyle = "#00ff00";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.fillText('echo x:' + stickModel._props.seeker.echo_x + ' y: ' + stickModel._props.seeker.echo_y, stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y - 20);
context.lineWidth = 1;
context.stroke();
context.closePath();
// blue line from (bx0, by0) to (bx, by)
context.beginPath();
context.strokeStyle = "#0000ff";
context.moveTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.stroke();
context.closePath();
}
body {
margin: 0px;
padding: 0px;
}
canvas {
display: block;
}
p {
position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<p>Move your mouse to see the stick (colored red) follow</p>
<canvas id="myCanvas"></canvas>
I am trying to have the widget for the rickshawgraph in dashing change the background color depending on the highest value on the highest graph for the newest incoming data. I have it working for one series, but I am having trouble getting it to handle multiple series in one graph.
This is a snippet from the rickshawgraph.coffee file. I know I need a loop to get each series and check which one has the highest value, then perform the rest of the logic, but I'm having a really tough time with the syntax. Any help would be appreciated. I included the complete files below as well.
node = $(#node)
series = #_parseData {points: #get('points'), series: #get('series')}
data = series[0].data
values = data[data.length - 1].y
#cool = parseInt(#get('cool'))
cool = parseInt node.data "cool"
#warm = parseInt(#get('warm'))
warm = parseInt node.data "warm"
level = switch
when values <= cool then 0
when values >= warm then 4
else
bucketSize = (warm - cool) / 3 # Total # of colours in middle
Math.ceil (values - cool) / bucketSize
backgroundClass = "hotness#{level}"
lastClass = #get "lastClass"
node.toggleClass "#{lastClass} #{backgroundClass}"
#set "lastClass", backgroundClass
My erb file calls the widget here.
</li>
My scss rickshawgraph.scss is here.
// ----------------------------------------------------------------------------
// Mixins
// ----------------------------------------------------------------------------
#mixin transition($transition-property, $transition-time, $method) {
-webkit-transition: $transition-property $transition-time $method;
-moz-transition: $transition-property $transition-time $method;
-o-transition: $transition-property $transition-time $method;
transition: $transition-property $transition-time $method;
}
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: #00C176;
// ----------------------------------------------------------------------------
// Widget-graph styles
// ----------------------------------------------------------------------------
.widget-rickshawgraph {
background-color: #00C176;
position: relative;
}
.widget-rickshawgraph .rickshaw_graph {
position: absolute;
left: 0px;
top: 0px;
}
.widget-rickshawgraph svg {
position: absolute;
left: 0px;
top: 0px;
}
.widget-rickshawgraph .title, .widget-rickshawgraph .value {
position: relative;
z-index: 99;
}
.widget-rickshawgraph .title {
color: rgba(126, 126, 126, 0.7);
}
.widget-rickshawgraph .more-info {
color: rgba(0, 0, 0, 0);
font-weight: 600;
font-size: 20px;
margin-top: 0;
opacity: 0;
}
.widget-rickshawgraph .x_tick {
position: absolute;
bottom: 0;
}
.widget-rickshawgraph .x_tick .title {
font-size: 40px;
color: rgba(0, 0, 0, 0.4);
opacity: 0.5;
padding-bottom: 3px;
}
.widget-rickshawgraph .y_ticks {
font-size: 40px;
fill: rgba(0, 0, 0, 0.4);
color: rgba(0, 0, 0, 0.4);
font-weight: bold;
}
.widget-rickshawgraph .y_ticks text {
font-size: 20px;
color: rgba(0, 0, 0, 0.4);
fill: rgba(0, 0, 0, 0.4);
font-weight: bold;
}
.widget-rickshawgraph .domain {
display: none;
}
.widget-rickshawgraph .rickshaw_legend {
position: absolute;
left: 0px;
bottom: 0px;
white-space: nowrap;
overflow-x: scroll;
font-size: 80px;
height: 20px;
}
.widget-rickshawgraph .rickshaw_legend ul {
margin: 0;
padding: 0;
list-style-type: none;
text-align: center;
}
.widget-rickshawgraph .rickshaw_legend ul li {
display: inline;
}
.widget-rickshawgraph .rickshaw_legend .swatch {
display: inline-block;
width: 14px;
height: 14px;
margin-left: 5px;
}
.widget-rickshawgraph .rickshaw_legend .label {
display: inline-block;
margin-left: 5px;
/*Change the font size and the text size and make sure the label comes to the front for the legend */
font-size: 200%;
color: rgba(255, 255, 255, 0.7);
}
.hotness0 { background-color: #00C176; }
.hotness1 { background-color: #88C100; }
.hotness2 { background-color: #FABE28; }
.hotness3 { background-color: #FF8A00; }
.hotness4 { background-color: #FF003C; }
// // More colour-blind friendly palette
// .hotness0 { background-color: #046D8B; }
// .hotness1 { background-color: #309292; }
// .hotness2 { background-color: #2FB8AC; }
// .hotness3 { background-color: #93A42A; }
// .hotness4 { background-color: #ECBE13; }
My rickshawgraph.coffee is here.
# Rickshawgraphhot v0.1.0
class Dashing.Rickshawgraphhot extends Dashing.Widget
DIVISORS = [
{number: 100000000000000000000000, label: 'Y'},
{number: 100000000000000000000, label: 'Z'},
{number: 100000000000000000, label: 'E'},
{number: 1000000000000000, label: 'P'},
{number: 1000000000000, label: 'T'},
{number: 1000000000, label: 'G'},
{number: 1000000, label: 'M'},
{number: 1000, label: 'S'},
{number: 1, label: 'MS'}
]
# Take a long number like "2356352" and turn it into "2.4M"
formatNumber = (number) ->
for divisior in DIVISORS
if number > divisior.number
number = "#{Math.round(number / (divisior.number/10))/10}#{divisior.label}"
break
else
number = " number + 'ms'"
return number
getRenderer: () -> return #get('renderer') or #get('graphtype') or 'area'
# Retrieve the `current` value of the graph.
#accessor 'current', ->
answer = null
# Return the value supplied if there is one.
if #get('displayedValue') != null and #get('displayedValue') != undefined
answer = #get('displayedValue')
if answer == null
# Compute a value to return based on the summaryMethod
series = #_parseData {points: #get('points'), series: #get('series')}
if !(series?.length > 0)
# No data in series
answer = ''
else
switch #get('summaryMethod')
when "sum"
answer = 0
answer += (point?.y or 0) for point in s.data for s in series
when "sumLast"
answer = 0
answer += s.data[s.data.length - 1].y or 0 for s in series
when "highest"
answer = 0
if #get('unstack') or (#getRenderer() is "line")
answer = Math.max(answer, (point?.y or 0)) for point in s.data for s in series
else
# Compute the sum of values at each point along the graph
for index in [0...series[0].data.length]
value = 0
for s in series
value += s.data[index]?.y or 0
answer = Math.max(answer, value)
when "none"
answer = ''
else
# Otherwise if there's only one series, pick the most recent value from the series.
if series.length == 1 and series[0].data?.length > 0
data = series[0].data
answer = data[data.length - 1].y
else
# Otherwise just return nothing.
answer = ''
if #get('numformat') == 'ms'
answer = formatNumber answer
return answer
ready: ->
#assignedColors = #get('colors').split(':') if #get('colors')
#strokeColors = #get('strokeColors').split(':') if #get('strokeColors')
#graph = #_createGraph()
#graph.render()
clear: ->
# Remove the old graph/legend if there is one.
$node = $(#node)
$node.find('.rickshaw_graph').remove()
if #$legendDiv
#$legendDiv.remove()
#$legendDiv = null
# Handle new data from Dashing.
onData: (data) ->
series = #_parseData data
if #graph
# Remove the existing graph if the number of series has changed or any names have changed.
needClear = false
needClear |= (series.length != #graph.series.length)
if #get("legend") then for subseries, index in series
needClear |= #graph.series[index]?.name != series[index]?.name
if needClear then #graph = #_createGraph()
# Copy over the new graph data
for subseries, index in series
#graph.series[index] = subseries
#graph.render()
node = $(#node)
series = #_parseData {points: #get('points'), series: #get('series')}
data = series[0].data
values = data[data.length - 1].y
#cool = parseInt(#get('cool'))
cool = parseInt node.data "cool"
#warm = parseInt(#get('warm'))
warm = parseInt node.data "warm"
level = switch
when values <= cool then 0
when values >= warm then 4
else
bucketSize = (warm - cool) / 3 # Total # of colours in middle
Math.ceil (values - cool) / bucketSize
backgroundClass = "hotness#{level}"
lastClass = #get "lastClass"
node.toggleClass "#{lastClass} #{backgroundClass}"
#set "lastClass", backgroundClass
# Create a new Rickshaw graph.
_createGraph: ->
$node = $(#node)
$container = $node.parent()
#clear()
# Gross hacks. Let's fix this.
width = (Dashing.widget_base_dimensions[0] * $container.data("sizex")) + Dashing.widget_margins[0] * 2 * ($container.data("sizex") - 1)
height = (Dashing.widget_base_dimensions[1] * $container.data("sizey"))
if #get("legend")
# Shave 20px off the bottom of the graph for the legend
height -= 20
$graph = $("<div style='height: #{height}px;'></div>")
$node.append $graph
series = #_parseData {points: #get('points'), series: #get('series')}
graphOptions = {
element: $graph.get(0),
renderer: #getRenderer(),
width: width,
height: height,
series: series
}
if !!#get('stroke') then graphOptions.stroke = true
if #get('min') != null then graphOptions.max = #get('min')
if #get('max') != null then graphOptions.max = #get('max')
try
graph = new Rickshaw.Graph graphOptions
catch err
if err.toString() is "x and y properties of points should be numbers instead of number and object"
# This will happen with older versions of Rickshaw that don't support nulls in the data set.
nullsFound = false
for s in series
for point in s.data
if point.y is null
nullsFound = true
point.y = 0
if nullsFound
# Try to create the graph again now that we've patched up the data.
graph = new Rickshaw.Graph graphOptions
if !#rickshawVersionWarning
console.log "#{#get 'id'} - Nulls were found in your data, but Rickshaw didn't like" +
" them. Consider upgrading your rickshaw to 1.4.3 or higher."
#rickshawVersionWarning = true
else
# No nulls were found - this is some other problem, so just re-throw the exception.
throw err
graph.renderer.unstack = !!#get('unstack')
xAxisOptions = {
graph: graph
}
if Rickshaw.Fixtures.Time.Local
xAxisOptions.timeFixture = new Rickshaw.Fixtures.Time.Local()
x_axis = new Rickshaw.Graph.Axis.Time xAxisOptions
y_axis = new Rickshaw.Graph.Axis.Y(graph: graph, tickFormat: Rickshaw.Fixtures.Number.formatMS)
if #get("legend")
# Add a legend
#$legendDiv = $("<div style='position:fixed; z-index:99; width: #{width}px;'></div>")
$node.append(#$legendDiv)
legend = new Rickshaw.Graph.Legend {
graph: graph
element: #$legendDiv.get(0)
}
return graph
# Parse a {series, points} object with new data from Dashing.
#
_parseData: (data) ->
series = []
# Figure out what kind of data we've been passed
if data.series
dataSeries = if isString(data.series) then JSON.parse data.series else data.series
for subseries, index in dataSeries
try
series.push #_parseSeries subseries
catch err
console.log "Error while parsing series: #{err}"
else if data.points
points = data.points
if isString(points) then points = JSON.parse points
if points[0]? and !points[0].x?
# Not already in Rickshaw format; assume graphite data
points = graphiteDataToRickshaw(points)
series.push {data: points}
if series.length is 0
# No data - create a dummy series to keep Rickshaw happy
series.push {data: [{x:0, y:0}]}
#_updateColors(series)
# Fix any missing data in the series.
if Rickshaw.Series.fill then Rickshaw.Series.fill(series, null)
return series
# Parse a series of data from an array passed to `_parseData()`.
# This accepts both Graphite and Rickshaw style data sets.
_parseSeries: (series) ->
if series?.datapoints?
# This is a Graphite series
answer = {
name: series.target
data: graphiteDataToRickshaw series.datapoints
color: series.color
stroke: series.stroke
}
else if series?.data?
# Rickshaw data. Need to clone, otherwise we could end up with multiple graphs sharing
# the same data, and Rickshaw really doesn't like that.
answer = {
name: series.name
data: series.data
color: series.color
stroke: series.stroke
}
else if !series
throw new Error("No data received for #{#get 'id'}")
else
throw new Error("Unknown data for #{#get 'id'}. series: #{series}")
answer.data.sort (a,b) -> a.x - b.x
return answer
# Update the color assignments for a series. This will assign colors to any data that
# doesn't have a color already.
_updateColors: (series) ->
# If no colors were provided, or of there aren't enough colors, then generate a set of
# colors to use.
if !#defaultColors or #defaultColors?.length != series.length
#defaultColors = computeDefaultColors #, #node, series
for subseries, index in series
# Preferentially pick supplied colors instead of defaults, but don't overwrite a color
# if one was supplied with the data.
subseries.color ?= #assignedColors?[index] or #defaultColors[index]
subseries.stroke ?= #strokeColors?[index] or "#000"
# Convert a collection of Graphite data points into data that Rickshaw will understand.
graphiteDataToRickshaw = (datapoints) ->
answer = []
for datapoint in datapoints
# Need to convert potential nulls from Graphite into a real number for Rickshaw.
answer.push {x: datapoint[1], y: (datapoint[0] or 0)}
answer
# Compute a pleasing set of default colors. This works by starting with the background color,
# and picking colors of intermediate luminance between the background and white (or the
# background and black, for light colored backgrounds.) We use the brightest color for the
# first series, because then multiple series will appear to blend in to the background.
computeDefaultColors = (self, node, series) ->
defaultColors = []
# Use a neutral color if we can't get the background-color for some reason.
backgroundColor = parseColor($(node).css('background-color')) or [50, 50, 50, 1.0]
hsl = rgbToHsl backgroundColor
alpha = if self.get('defaultAlpha')? then self.get('defaultAlpha') else 1
if self.get('colorScheme') in ['rainbow', 'near-rainbow']
saturation = (interpolate hsl[1], 1.0, 3)[1]
luminance = if (hsl[2] < 0.6) then 0.7 else 0.3
hueOffset = 0
if self.get('colorScheme') is 'rainbow'
# Note the first and last values in `hues` will both have the same hue as the background,
# hence the + 2.
hues = interpolate hsl[0], hsl[0] + 1, (series.length + 2)
hueOffset = 1
else
hues = interpolate hsl[0] - 0.25, hsl[0] + 0.25, series.length
for hue, index in hues
if hue > 1 then hues[index] -= 1
if hue < 0 then hues[index] += 1
for index in [0...series.length]
defaultColors[index] = rgbToColor hslToRgb([hues[index + hueOffset], saturation, luminance, alpha])
else
hue = if self.get('colorScheme') is "compliment" then hsl[0] + 0.5 else hsl[0]
if hsl[0] > 1 then hsl[0] -= 1
saturation = hsl[1]
saturationSource = if (saturation < 0.6) then 0.7 else 0.3
saturations = interpolate saturationSource, saturation, (series.length + 1)
luminance = hsl[2]
luminanceSource = if (luminance < 0.6) then 0.9 else 0.1
luminances = interpolate luminanceSource, luminance, (series.length + 1)
for index in [0...series.length]
defaultColors[index] = rgbToColor hslToRgb([hue, saturations[index], luminances[index], alpha])
return defaultColors
# Helper functions
# ================
isString = (obj) ->
return toString.call(obj) is "[object String]"
# Parse a `rgb(x,y,z)` or `rgba(x,y,z,a)` string.
parseRgbaColor = (colorString) ->
match = /^rgb\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString)
if match
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), 1.0]
match = /^rgba\(\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*,\s*([\d]+)\s*\)/.exec(colorString)
if match
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4])]
return null
# Parse a color string as RGBA
parseColor = (colorString) ->
answer = null
# Try to use the browser to parse the color for us.
div = document.createElement('div')
div.style.color = colorString
if div.style.color
answer = parseRgbaColor div.style.color
if !answer
match = /^#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/.exec(colorString)
if match then answer = [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16), 1.0]
if !answer
match = /^#([\da-fA-F])([\da-fA-F])([\da-fA-F])/.exec(colorString)
if match then answer = [parseInt(match[1], 16) * 0x11, parseInt(match[2], 16) * 0x11, parseInt(match[3], 16) * 0x11, 1.0]
if !answer then answer = parseRgbaColor colorString
return answer
# Convert an RGB or RGBA color to a CSS color.
rgbToColor = (rgb) ->
if (!3 of rgb) or (rgb[3] == 1.0)
return "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})"
else
return "rgba(#{rgb[0]},#{rgb[1]},#{rgb[2]},#{rgb[3]})"
# Returns an array of size `steps`, where the first value is `source`, the last value is `dest`,
# and the intervening values are interpolated. If steps < 2, then returns `[dest]`.
#
interpolate = (source, dest, steps) ->
if steps < 2
answer =[dest]
else
stepSize = (dest - source) / (steps - 1)
answer = (num for num in [source..dest] by stepSize)
# Rounding errors can cause us to drop the last value
if answer.length < steps then answer.push dest
return answer
# Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
#
# Converts an RGBA color value to HSLA. Conversion formula
# adapted from http://en.wikipedia.org/wiki/HSL_color_space.
# Assumes r, g, and b are contained in the set [0, 255] and
# a in [0, 1]. Returns h, s, l, a in the set [0, 1].
#
# Returns the HSLA representation as an array.
rgbToHsl = (rgba) ->
[r,g,b,a] = rgba
r /= 255
g /= 255
b /= 255
max = Math.max(r, g, b)
min = Math.min(r, g, b)
l = (max + min) / 2
if max == min
h = s = 0 # achromatic
else
d = max - min
s = if l > 0.5 then d / (2 - max - min) else d / (max + min)
switch max
when r then h = (g - b) / d + (g < b ? 6 : 0)
when g then h = (b - r) / d + 2
when b then h = (r - g) / d + 4
h /= 6;
return [h, s, l, a]
# Adapted from http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
#
# Converts an HSLA color value to RGBA. Conversion formula
# adapted from http://en.wikipedia.org/wiki/HSL_color_space.
# Assumes h, s, l, and a are contained in the set [0, 1] and
# returns r, g, and b in the set [0, 255] and a in [0, 1].
#
# Retunrs the RGBA representation as an array.
hslToRgb = (hsla) ->
[h,s,l,a] = hsla
if s is 0
r = g = b = l # achromatic
else
hue2rgb = (p, q, t) ->
if(t < 0) then t += 1
if(t > 1) then t -= 1
if(t < 1/6) then return p + (q - p) * 6 * t
if(t < 1/2) then return q
if(t < 2/3) then return p + (q - p) * (2/3 - t) * 6
return p
q = if l < 0.5 then l * (1 + s) else l + s - l * s
p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a]
My rickshawgraph.html is here.
<h1 class="title" data-bind="title" style="color:white;"> </h1>
<h2 class="value" data-bind="current | prepend prefix"></h2>
<p class="more-info" data-bind="moreinfo"></p>
I suggest you do this instead. There are warning and danger colors set in "assets/stylesheets/application.scss". You can add new colors in there.
In your rickshaw graph widget add this
onData: (data) ->
if data.status
# clear existing "status-*" classes
$(#get('node')).attr 'class', (i,c) ->
c.replace /\bstatus-\S+/g, ''
# add new class
$(#get('node')).addClass "status-#{data.status}"
In your job's .rb, set a status and send it.
For example:
if count < 50
status = 'warning'
else
status = 'danger'
end
send_event('thread-count', { value: count, status: status } )
In the above case if my count is less than 50, it blinks yellow or else its red.
NOTE: the animation doesnt work in Firefox. Works in Safari and Chrome only.
The answer here is a simple and nice one. I had to make a few tweaks though as my charts used data-colors and data-stroke colors. Posting it for sample usage.
plug into rickhawgraph.coffee in the top of onData section:
onData: (data) ->
if data.status
# clear existing "status-*" classes
$(#get('node')).attr 'class', (i,c) ->
c.replace /\bstatus-\S+/g, ''
#assignedColors = ""
#strokeColors = ""
# add new class
$(#get('node')).addClass "status-#{data.status}"
else
#assignedColors = #get('colors').split(':') if #get('colors')
#strokeColors = ""
the html
<li data-row="1" data-col="1" data-sizex="2" data-sizey="4">
<div data-id="apdex_score_stage" data-view="Rickshawgraph" data-bind-data-min="0" data-max="1" data-title="Apdex Score (1-Excellent)" class="" data-colors="#4D4D94" data-stroke-colors="#707070" data-unstack="false" data-stroke="true" data-default-alpha="0.5" data-legend="false" data-summary-method="last"></div>
</li>
in the job
apdex_status=""
if apdex_score_values_array_min.min < 1 #yellow if one of the values is less than 1
apdex_status="danger"
end
print apdex_score_values_array_min[1]
if apdex_score_values_array_min[1] < 1 #red if last value is less than 1
apdex_status="warning"
end
#red if last value is less than 1
if apdex_score_values_array_min.min == 1.0 #nothing to worry, no status
apdex_status=""
end
send_event("apdex_score_stage", min: apdex_score_values_array_min.round(2), status: apdex_status, points: apdex_score_array)
Data-binding is also heavily used here for rickshaw graph.
I have a curve running above or below a sloped line and I want to fill the area just above the line and below the curve, but not the area below the line and above the curve, as illustrated in the left image.
Again, it is easy to fill like right image, but I want to fill like in left image. So, how to get rid of the unwanted fill?
svg.append("path").attr("class", "line").attr("d", peak(pp)).style({ fill: peakColor, opacity: 0.5 });
Okay, this is something I came up with for me in case anybody cares about it.
var slope = (dp.rightY - dp.leftY) / (dp.rightX - dp.leftX); // slope of the line
var pp = [];
pp.push({ x: dp.leftX, y: dp.leftY });
var wasAbove = true; // track if it's above the line
data.forEach(function (d) {
if (d.x >= dp.leftX && d.x <= dp.rightX) {
var yAtLine = (d.x - dp.leftX) * slope + dp.leftY;
if (d.y > yAtLine) {
if (!wasAbove)
pp.push({ x: d.x, y: yAtLine });
pp.push(d);
wasAbove = true;
} else if (wasAbove) {
pp.push({ x: d.x, y: yAtLine });
wasAbove = false;
}
}
});
pp.push({ x: dp.rightX, y: dp.rightY });
var peak = d3.svg.line().x(function (d) { return xScale(d.x) }).y(function (d) { return yScale(d.y) });
svg.append("path").attr("class", "line").attr("d", peak(pp)).style({ fill: peakColor, opacity: 0.5 });