-
Notifications
You must be signed in to change notification settings - Fork 13
Vanilla radar - initial commit #42
base: master
Are you sure you want to change the base?
Changes from all commits
885ef94
103844e
eac5448
a07826c
179346e
75e0d89
fb70542
59c749a
7f47a08
ad971e1
df09e47
e17aa16
7ea6772
fa040b7
de21e2d
d22bbd1
9bf2dfa
673a87c
25e5c75
747cce4
afe3ef4
a719124
c5f0378
e665b24
9e2a642
388c663
20624a4
df9e0b3
25024b5
d4ae651
5af23f1
b141f03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
Copyright 2017 IBM | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# `@ibm-design/charts-vanilla-radar` | ||
|
||
## Usage | ||
|
||
#### Install | ||
```bash | ||
# you can use yarn | ||
yarn add @ibm-design/charts-vanilla-radar | ||
|
||
# or npm to add it to your project | ||
npm install @ibm-design/charts-vanilla-radar | ||
``` | ||
|
||
## Development | ||
|
||
#### Start dev environment | ||
```bash | ||
npm start | ||
``` | ||
|
||
Then go to http://localhost:8082/ to see this package rendered. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
const gulp = require('gulp'); | ||
const sass = require('gulp-sass'); | ||
const plumber = require('gulp-plumber'); | ||
const sourcemaps = require('gulp-sourcemaps'); | ||
|
||
gulp.task('compile-scss', () => { | ||
return gulp.src('src/styles/*.scss') | ||
.pipe(plumber()) | ||
.pipe(sourcemaps.init()) | ||
.pipe(sass().on('error', sass.logError)) | ||
.pipe(sourcemaps.write('maps')) | ||
.pipe(gulp.dest('lib')); | ||
}); | ||
|
||
gulp.task('default', ['compile-scss']); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>Vanilla Radar Chart</title> | ||
<link rel="stylesheet" href="lib/styles.css"> | ||
</head> | ||
<body> | ||
<svg class="radar"></svg> | ||
<script src="lib/index.js"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "@ibm-design/charts-vanilla-radar", | ||
"version": "0.0.0", | ||
"main": "src/index.js", | ||
"scripts": { | ||
"build": "npm run clean && webpack && gulp", | ||
"start": "npm run build && webpack-dev-server --progress --colors", | ||
"clean": "rimraf lib" | ||
}, | ||
"license": "Apache-2.0", | ||
"devDependencies": { | ||
"gulp": "^3.9.1", | ||
"gulp-plumber": "^1.1.0", | ||
"gulp-sass": "^3.1.0", | ||
"gulp-sourcemaps": "^2.4.1", | ||
"rimraf": "^2.5.4", | ||
"webpack": "^2.2.1", | ||
"webpack-dev-server": "^2.4.1" | ||
}, | ||
"dependencies": { | ||
"d3-array": "^1.0.2", | ||
"d3-scale": "^1.0.4", | ||
"d3-selection": "^1.0.3", | ||
"d3-selection-multi": "^1.0.1", | ||
"d3-shape": "^1.0.4" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// import Chart from '../'; | ||
|
||
describe('Radar Component', () => { | ||
it('should render', () => { | ||
expect(true).toBe(true, 'This is a placeholder true'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
import { selection, select } from 'd3-selection'; | ||
import { scaleOrdinal, scaleLinear } from 'd3-scale'; | ||
import { max, merge, range } from 'd3-array'; | ||
import { radialLine } from 'd3-shape'; | ||
import 'd3-selection-multi'; | ||
|
||
export { selection }; // XXX | ||
|
||
export default class Radar { | ||
|
||
constructor(graphNode, data, opts) { | ||
this.data = this.formatData(data); | ||
this.target = select(graphNode); | ||
|
||
this.scale; | ||
this.line; | ||
this.color = scaleOrdinal() | ||
.range(opts.colors); | ||
this.axis = {}; | ||
|
||
this.cfg = (() => { | ||
const w = opts.size || 500; | ||
const h = w; | ||
const numAxis = this.data[0].length; | ||
const margin = opts.margins; | ||
const r = w / 2 - max([margin.top + margin.bottom, margin.right + margin.left]); | ||
|
||
return { | ||
w: w, | ||
h: h, | ||
r: r, | ||
margin: margin, | ||
units: opts.units, | ||
levels: opts.levels, | ||
max: null, // will be determined by data | ||
labelFactor: opts.labelFactor, // how much further outside the radius does the label appears | ||
wrapWidth: opts.wrapWidth || 60, // word wrap after this number of pixels | ||
opacityArea: opts.opacityArea, | ||
strokeWidth: 2, | ||
numAxis: numAxis, | ||
angleSlice: Math.PI * 2 / numAxis, // angle of slice in radians | ||
shape: opts.shape || 'circle', //circle, polygon, square, | ||
dotSize: 4, //if not set, dots will not be drawn. | ||
axisStyle: { | ||
'stroke': 'black', | ||
'stroke-width': 1, | ||
}, | ||
}; | ||
})(); | ||
} // constructor | ||
|
||
|
||
draw() { | ||
|
||
//setup | ||
this.scalesAndLinesFunctions(); | ||
this.wrapper = this.setupRadar(); | ||
|
||
const blobs = this.wrapper.append('g').attr('class', 'blobs'); //draw blobs & dots | ||
|
||
for (let i = 0; i < this.data.length; i++) { | ||
const g = blobs.append('g').attr('class', 'blob-' + i); //blob | ||
|
||
g.append('path').datum(this.data[i]).attrs({ | ||
'stroke-width': 1.5, | ||
'd' : this.line, | ||
'fill': () => { return this.color(i); }, | ||
}); | ||
|
||
//dots | ||
g.selectAll('.dot').data(this.data[i]).enter().append('circle').attrs({ | ||
'stroke-width': 1.5, | ||
'cx': (d, i) => { return this.scale( d.value) * Math.sin(this.cfg.angleSlice * i + Math.PI / 2);}, | ||
'cy': (d, i) => { return this.scale( d.value) * Math.cos(this.cfg.angleSlice * i + Math.PI / 2);}, | ||
'r': this.cfg.dotSize, | ||
'fill': () => { return this.color(i);}, | ||
'opacity': 0.4, | ||
'class': 'dot', | ||
}); | ||
} | ||
}//draw | ||
|
||
scalesAndLinesFunctions() { | ||
const that = this; | ||
|
||
this.cfg.max = max(merge(this.data), d => d.value); | ||
this.scale = scaleLinear() | ||
.domain([0, this.cfg.max]) | ||
.range([0, this.cfg.r]); | ||
|
||
this.line = radialLine() | ||
.angle((d, i) => { return -(i * that.cfg.angleSlice) + Math.PI / 2;}) | ||
.radius((d) => { return that.scale(d.value);}); | ||
|
||
}//scalesAndLinesFunctions | ||
|
||
setupRadar() { | ||
this.target.styles({ | ||
width: this.cfg.w, | ||
height: this.cfg.h, | ||
}); | ||
|
||
const globalGroup = this.target.append('g').attrs({ | ||
'class': 'chart', | ||
'transform': `translate(${this.cfg.w / 2} ${this.cfg.h / 2})`, | ||
}); | ||
|
||
this.axis.names = this.data[0].map( el => el.axis); | ||
this.axis.length = this.axis.names.length; | ||
|
||
//puts 0,0 in the center of the svg | ||
|
||
// draws the axis | ||
if (this.cfg.axisStyle) { | ||
this.axisGroup = globalGroup.append('g').attr('class', 'axis-lines'); | ||
this.axisGroup.selectAll('.axis-line').data(this.axis.names).enter().append('line') | ||
.attrs({ | ||
'x1': 0, | ||
'y1': 0, | ||
// `+ Math.PI/2 to make the Axis line up with the correct Data` | ||
'x2': (d, i) => { | ||
return this.scale(this.cfg.max * 1.05) * Math.sin( this.cfg.angleSlice * i + Math.PI / 2); | ||
}, | ||
'y2': (d, i) => { | ||
return this.scale(this.cfg.max * 1.05) * Math.cos( this.cfg.angleSlice * i + Math.PI / 2); | ||
}, | ||
'opacity': 0.3, | ||
'class': 'axis-line', | ||
}).styles(this.cfg.axisStyle); | ||
} | ||
|
||
if (this.cfg.shape == 'circle') { | ||
//draw rings | ||
const ringInterval = this.cfg.max / this.cfg.levels; | ||
//add 1 so you include the MAX | ||
const ringRadiusValues = range(ringInterval, this.cfg.max + 1, ringInterval).reverse(); | ||
|
||
let g = globalGroup.append('g').attr('class', 'grid-circle'); | ||
g.append('g').attr('class', 'rings') | ||
.selectAll('.ring').data(ringRadiusValues) | ||
.enter().append('circle').attrs({ | ||
'fill': 'none', | ||
'cx': 0, | ||
'cy': 0, | ||
'r': (d) => { return this.scale(d); }, | ||
'stroke': 'black', | ||
'stroke-width': 1, | ||
'stroke-dasharray': '5 5', | ||
'opacity': 0.2, | ||
'class': 'ring', | ||
}); | ||
|
||
//add the unit labels on the rings | ||
g.append('g').attr('class', 'rings-labels') | ||
.selectAll('.ring-label').data(ringRadiusValues) | ||
.enter().append('text').attrs({ | ||
'class': 'ring-label', | ||
'x': '0', | ||
'y': (d) => {return -this.scale(d);}, | ||
'dy': '-0.3em', | ||
'dx': '0.5em', | ||
'font-size': '0.8em', | ||
'fill': 'grey', | ||
'opacity': 0.4, | ||
}) | ||
.text((d) => {return d + this.cfg.units;}); | ||
g.append('g').attr('class', 'legend') | ||
.selectAll('.axis-label').data(this.data[0]).enter().append('text').attrs({ | ||
'class': 'axis-label', | ||
'x': (d, i) => { | ||
return (this.scale(this.cfg.max) * 1.05 + this.cfg.labelFactor ) * | ||
Math.sin(this.cfg.angleSlice * i + Math.PI / 2); | ||
}, | ||
'y': (d, i) => { | ||
return (this.scale(this.cfg.max) * 1.05 + this.cfg.labelFactor ) * | ||
Math.cos(this.cfg.angleSlice * i + Math.PI / 2); | ||
}, | ||
'font-size': '0.8em', | ||
'fill': 'grey', | ||
'opacity': 0.4, | ||
'text-anchor': 'middle', | ||
}) | ||
.text((d) => {return d.axis;}) | ||
.call(this.wrap, this.cfg.wrapWidth); | ||
|
||
|
||
|
||
|
||
} else if (this.cfg.shape == 'polygon') { | ||
//TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to write a user story around this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still want me to merge it now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #48 (issue) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seejamescode if you've looked at the code, and are happy with it then it should be merged. I created stories for these to do's, and we can tackle them after this pull request goes in. |
||
} else if (this.cfg.shape == 'square') { | ||
//TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to write a user story around this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #47 (issue) |
||
} | ||
return globalGroup; | ||
}//setupRadar | ||
|
||
//customize this function to fit data model you want | ||
formatData(data) { | ||
data.map(d => { | ||
d.map(v => { | ||
v.value = Math.round(v.value * 100); | ||
}); | ||
}); | ||
|
||
return data; | ||
} // formatData | ||
|
||
|
||
//Adapted from http://bl.ocks.org/mbostock/7555321 | ||
//Wraps SVG text | ||
wrap(text, width) { | ||
text.each(function() { | ||
const text = select(this); | ||
const words = text.text().split(/\s+/).reverse(); | ||
const lineHeight = 1.4; // ems | ||
const y = text.attr('y'); | ||
const x = text.attr('x'); | ||
const dy = parseFloat(text.attr('dy')) || 0; | ||
|
||
let word; | ||
let line = []; | ||
let lineNumber = 0; | ||
let tspan = text.text(null).append('tspan').attr('x', x).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', x) | ||
.attr('y', y) | ||
.attr('dy', ++lineNumber * lineHeight + dy + 'em') | ||
.text(word); | ||
} | ||
} | ||
}); | ||
}//wrap | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
export default [ | ||
[//iPhone | ||
{axis:'Apdex', value:0.80}, | ||
{axis:'Alerts', value:0.66}, | ||
{axis:'Request Volume', value:0.35}, | ||
{axis:'Memory', value:0.54}, | ||
{axis:'CPU', value:0.38}, | ||
{axis:'Disk', value:0.23}, | ||
{axis:'Network', value:0.23}, | ||
{axis:'Heap', value:0.16}, | ||
], | ||
[//Samsung | ||
{axis:'Apdex', value:0.58}, | ||
{axis:'Alerts', value:0.66}, | ||
{axis:'Request Volume', value:0.35}, | ||
{axis:'Memory', value:0.23}, | ||
{axis:'CPU', value:0.36}, | ||
{axis:'Disk', value:0.13}, | ||
{axis:'Network', value:0.11}, | ||
{axis:'Heap', value:0.35}, | ||
], | ||
[//Nokia Smartphone | ||
{axis:'Apdex', value:0.26}, | ||
{axis:'Alerts', value:0.10}, | ||
{axis:'Request Volume', value:0.30}, | ||
{axis:'Memory', value:0.14}, | ||
{axis:'CPU', value:0.22}, | ||
{axis:'Disk', value:0.04}, | ||
{axis:'Network', value:0.23}, | ||
{axis:'Heap', value:0.41}, | ||
], | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import data from './data'; | ||
import Radar from '../index'; | ||
|
||
const a = new Radar('svg', data, { | ||
'size': 800, | ||
'margins': {top: 70, right: 70, bottom: 70, left: 70}, | ||
'colors': ['#EDC951', '#CC333F', '#00A0B0'], | ||
'units': '%', | ||
'levels': 5, | ||
'opacityArea': 0.4, | ||
'labelFactor': 30, | ||
'shape': 'circle', | ||
'wrapWidth': 60, | ||
}); | ||
|
||
a.draw(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine we should probably add tests for these? @seejamescode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes please 😄