I need some help with this kinda specific animation. It is a square spiral pattern that keeps going inwards until it's fully complete. I somewhat did manage to get it going but I don't know how to stop the animation properly, and I'm not sure if the math behind it is mostly efficient/correct.
Here's what I have for now:
function createSquareSpiralPath(
strokeWidth,
width,
height,
) {
const maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth); // ???
let path = '';
for (let i = 0; i <= maxIterations; i++) {
const step = strokeWidth * i;
const computed = [
`${step},${height - step}`,
`${step},${step}`,
`${width - step - strokeWidth},${step}`,
`${width - step - strokeWidth},${height - step - strokeWidth} `,
];
path += computed.join(' ');
}
return path.trim();
}
.spiral {
stroke-dasharray: 6130;
stroke-dashoffset: 6130;
animation: moveToTheEnd 5s linear forwards;
}
#keyframes moveToTheEnd {
to {
stroke-dashoffset: 0;
}
}
<svg viewBox="-10 -10 350 350" height="350" width="350">
<polyline class="spiral" points="
0,350 0,0 330,0 330,330 20,330 20,20 310,20 310,310 40,310 40,40 290,40 290,290 60,290 60,60 270,60 270,270 80,270 80,80 250,80 250,250 100,250 100,100 230,100 230,230 120,230 120,120 210,120 210,210 140,210 140,140 190,140 190,190 160,190 160,160 170,160 170,170"
style="fill:transparent;stroke:black;stroke-width:20" />
Sorry, your browser does not support inline SVG.
</svg>
I added the js function just to demonstrate how I'm generating the points. As you can see the animation plays exactly how I want, I just can't find a way to wrap it up properly. Also, I'm unsure if this function would generate correct points for varying width/height/strokeWidth.
I appreciate any help! Thanks in advance. :)
PS.: I could not find a mathematical term for this pattern (square-ish spiral) so I'm more than happy to learn how to call it properly.
Edit
Based on #enxaneta answers (thank you!) it seems I'm incorrectly calculating the max number of iterations. This can be seen whenever width !== height. I'll do some research on how I'm producing this value, maybe this formula isn't adequate to properly "stop" the animation without any blank space.
I guess you also need to check if your current drawing position has already reached a maximum x/y (close to you center).
The calculation for the loops iterations works fine.
Currently you're drawing 4 new points in each step.
Depending on your stroke-width you might need to stop drawing e.g after the 2. or 3. point when you're close to the center X/Y coordinates.
let spiral1 = createSquareSpiralPath(50, 500, 1000);
let spiral1_2 = createSquareSpiralPath(20, 1000, 500);
let spiral2 = createSquareSpiralPath(150, 300, 300);
function createSquareSpiralPath(strokeWidth, width, height) {
let maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth);
let coords = [];
//calculate max X/Y coordinates according to stroke-width
let strokeToWidthRatio = width * 1 / strokeWidth;
let strokeToHeightRatio = height * 1 / strokeWidth;
let maxX = (width - strokeWidth / strokeToWidthRatio) / 2;
let maxY = (height - strokeWidth / strokeToHeightRatio) / 2;
for (let i = 0; i <= maxIterations; i++) {
const step = strokeWidth * i;
// calculate points in iteration
let [x1, y1] = [step, (height - step)];
let [x2, y2] = [step, step];
let [x3, y3] = [(width - step - strokeWidth), step];
let [x4, y4] = [(width - step - strokeWidth), (height - step - strokeWidth)];
//stop drawing if max X/Y coordinates are reached
if (x1 <= maxX && y1 >= maxY) {
coords.push(x1, y1)
}
if (x2 <= maxX && y2 <= maxY) {
coords.push(x2, y2)
}
if (x3 >= maxX && y3 <= maxY) {
coords.push(x3, y3)
}
if (x4 >= maxX && y4 >= maxY) {
coords.push(x4, y4)
}
}
let points = coords.join(' ');
//calc pathLength from coordinates
let pathLength = 0;
for (let i = 0; i < coords.length - 2; i += 2) {
let x1 = coords[i];
let y1 = coords[i + 1];
let x2 = coords[i + 2];
let y2 = coords[i + 3];
let length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
pathLength += length;
}
//optional: render svg
renderSpiralSVG(points, pathLength, width, height, strokeWidth);
return [points, pathLength];
}
function renderSpiralSVG(points, pathLength, width, height, strokeWidth) {
const ns = "http://www.w3.org/2000/svg";
let svgTmp = document.createElementNS(ns, "svg");
svgTmp.setAttribute(
"viewBox", [-strokeWidth / 2, -strokeWidth / 2, width, height].join(" ")
);
let newPolyline = document.createElementNS(ns, "polyline");
newPolyline.classList.add("spiral");
newPolyline.setAttribute("points", points);
svgTmp.appendChild(newPolyline);
document.body.appendChild(svgTmp);
newPolyline.setAttribute(
"style",
`fill:transparent;
stroke:black;
stroke-linecap: square;
stroke-width:${strokeWidth};
stroke-dashoffset: ${pathLength};
stroke-dasharray: ${pathLength};`
);
}
svg {
border: 1px solid red;
}
svg {
display: inline-block;
height: 20vw;
}
.spiral {
stroke-width: 1;
animation: moveToTheEnd 1s linear forwards;
}
.spiral:hover {
stroke-width: 1!important;
}
#keyframes moveToTheEnd {
to {
stroke-dashoffset: 0;
}
}
<p> Hover to see spiral lines</p>
To control the animation, instead of CSS, use the Web Animations API
https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
Wrap all in a standard Web Component <svg-spiral> with shadowDOM, so you can have multiple components on screen without any global CSS conflicts.
set a pathLenght="100" on the polygon, so you don't have to do calculations
stroke-dasharray must be written as: strokeDasharray in WAAPI
The animation triggers an onfinish function
clicking an <svg-spiral> in the SO snippet below will restart the animation
<div style="display:grid;grid:1fr/repeat(4,1fr)">
<svg-spiral></svg-spiral>
<svg-spiral stroke="rebeccapurple" width="1000" strokewidth="10"></svg-spiral>
<svg-spiral stroke="blue" duration="10000"></svg-spiral>
<svg-spiral stroke="red" width="6000" duration="1e4"></svg-spiral>
</div>
<script>
customElements.define("svg-spiral", class extends HTMLElement {
connectedCallback() {
let strokewidth = this.getAttribute("strokewidth") || 30;
let width = this.getAttribute("width") || 500; let height = this.getAttribute("height") || width;
let points = '';
for (let i = 0; i <= ~~(Math.min(width, height) / 2 / strokewidth); i++) {
const step = strokewidth * i;
points += `${step},${height - step} ${step},${step} ${width - step - strokewidth},${step} ${width - step - strokewidth},${height - step - strokewidth} `;
}
this.attachShadow({mode:"open"}).innerHTML = `<svg viewBox="-${strokewidth/2}-${strokewidth/2} ${width} ${height}"><polyline class="spiral" pathLength="100" points="${points}z"
fill="transparent" stroke-width="${strokewidth}" /></svg>`;
this.onclick = (e) => this.animate();
this.animate();
}
animate() {
let spiral = this.shadowRoot.querySelector(".spiral");
spiral.setAttribute("stroke", this.getAttribute("stroke") || "black");
let player = spiral.animate(
[{ strokeDashoffset: 100, strokeDasharray: 100, opacity: 0 },
{ strokeDashoffset: 0, strokeDasharray: 100, opacity: 1 }],
{
duration: ~~(this.getAttribute("duration") || 5000),
iterations: 1
});
player.onfinish = (e) => { spiral.setAttribute("stroke", "green") }
}
})
</script>
How do you rotate, using Matrix4x4 transform, a QML item around another axis than z, with the center of the item as origin of the transformation?
To rotate around the y axis with (0,0) as origin, I tried naively:
Image {
source: "..."
width: 100
height: 100
transform: Matrix4x4 {
property real a: Math.PI / 4
matrix: Qt.matrix4x4(
Math.cos(a), 0, -Math.sin(a), 0,
0, 1, 0, 0,
Math.sin(a), 0, Math.cos(a), 0,
0, 0, 0, 1)
}
}
As a result, I get a cut width item whereas I am looking for a perspective effect.
Can anyone explain how the transformation matrix of QML items works?
Here's comment from Unity 8:
// Rotating 3 times at top/bottom because that increases the perspective.
// This is a hack, but as QML does not support real 3D coordinates
// getting a higher perspective can only be done by a hack. This is the most
// readable/understandable one I could come up with.
Link to source code: https://github.com/ubports/unity8/blob/xenial/qml/Launcher/LauncherDelegate.qml#L287
The thing to be careful about is there appears to be clipping around z >= 0, where the object is technically in front of your monitor. To ensure the object stays on screen, you need to translate it so that it remains behind the monitor. In the following example, because I know the object is 300x300 and that it is centered, I know that I only need to push it into the screen by 150 pixels.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
property real xrotation: 0
property real yrotation: 0
property real zrotation: 0
Image {
width: 300
height: 300
anchors.centerIn: parent
source: "image-32.svg"
sourceSize: Qt.size(width, height)
transform: Matrix4x4 {
matrix: (() => {
let m = Qt.matrix4x4();
m.translate(Qt.vector3d(150, 150, -150));
m.rotate(zrotation, Qt.vector3d(0, 0, 1));
m.rotate(yrotation, Qt.vector3d(0, 1, 0));
m.rotate(xrotation, Qt.vector3d(1, 0, 0));
m.translate(Qt.vector3d(-150, -150, 0));
return m;
})()
}
}
Timer {
running: xbutton.pressed
repeat: true
interval: 100
onTriggered: xrotation += 5
}
Timer {
running: ybutton.pressed
repeat: true
interval: 100
onTriggered: yrotation += 5
}
Timer {
running: zbutton.pressed
repeat: true
interval: 100
onTriggered: zrotation += 5
}
footer: Frame {
RowLayout {
width: parent.width
Button {
id: xbutton
text: "X"
}
Button {
id: ybutton
text: "Y"
}
Button {
id: zbutton
text: "Z"
}
Button {
text: "Reset"
onClicked: {
xrotation = yrotation = zrotation = 0;
}
}
}
}
}
// image-32.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M2 5v22h28V5zm27 21H3v-5.474l4.401-3.5 1.198.567L14 13.106l5 4.531 3.506-3.123L29 20.39zm-5.997-12.422a.652.652 0 0 0-.926-.033L19 16.293l-4.554-4.131a.652.652 0 0 0-.857-.013L8.45 16.417l-.826-.391a.642.642 0 0 0-.72.117L3 19.248V6h26v13.082zM19 8a2 2 0 1 0 2 2 2.002 2.002 0 0 0-2-2zm0 3a1 1 0 1 1 1-1 1.001 1.001 0 0 1-1 1z"/><path fill="none" d="M0 0h32v32H0z"/></svg>
You can Try it Online!
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.