-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vislib/axis #8174
Vislib/axis #8174
Changes from 1 commit
bc4508d
8edc0e9
a630025
3708697
82f9087
f36da8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- splitting it into 3 subclasses (Axis, AxisLabels, AxisScale) - converting to ES6 classes + style fixes - adding more customization options
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
import d3 from 'd3'; | ||
import _ from 'lodash'; | ||
import $ from 'jquery'; | ||
import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; | ||
import AxisTitleProvider from 'ui/vislib/lib/axis_title'; | ||
import AxisLabelsProvider from 'ui/vislib/lib/axis_labels'; | ||
import AxisScaleProvider from 'ui/vislib/lib/axis_scale'; | ||
|
||
export default function AxisFactory(Private) { | ||
const ErrorHandler = Private(ErrorHandlerProvider); | ||
const AxisTitle = Private(AxisTitleProvider); | ||
const AxisLabels = Private(AxisLabelsProvider); | ||
const AxisScale = Private(AxisScaleProvider); | ||
const defaults = { | ||
show: true, | ||
type: 'value', | ||
elSelector: '.axis-wrapper-{pos} .axis-div', | ||
position: 'left', | ||
axisFormatter: null, // TODO: create default axis formatter | ||
scale: 'linear', | ||
expandLastBucket: true, //TODO: rename ... bucket has nothing to do with vis | ||
inverted: false, | ||
style: { | ||
color: '#ddd', | ||
lineWidth: '1px', | ||
opacity: 1, | ||
tickColor: '#ddd', | ||
tickWidth: '1px', | ||
tickLength: '6px' | ||
} | ||
}; | ||
|
||
const categoryDefaults = { | ||
type: 'category', | ||
position: 'bottom', | ||
labels: { | ||
rotate: 0, | ||
rotateAnchor: 'end', | ||
filter: true | ||
} | ||
}; | ||
/** | ||
* Appends y axis to the visualization | ||
* | ||
* @class Axis | ||
* @constructor | ||
* @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}} | ||
*/ | ||
class Axis extends ErrorHandler { | ||
constructor(args) { | ||
super(); | ||
if (args.type === 'category') { | ||
_.extend(this, defaults, categoryDefaults, args); | ||
} else { | ||
_.extend(this, defaults, args); | ||
} | ||
|
||
this._attr = args.vis._attr; | ||
this.elSelector = this.elSelector.replace('{pos}', this.position); | ||
this.scale = new AxisScale(this, {scale: this.scale}); | ||
this.axisTitle = new AxisTitle(this, this.axisTitle); | ||
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. The second arg here looks like a mistake 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. this.axisTitle ? what happens is that i would get in configuration object .... now i am replacing parts of it with real class instances ... so before this.axisTitle is config object for axisTitle ... and then i create an AxisTitle instance with that same config. |
||
this.axisLabels = new AxisLabels(this, this.labels); | ||
} | ||
|
||
render() { | ||
d3.select(this.vis.el).selectAll(this.elSelector).call(this.draw()); | ||
} | ||
|
||
isHorizontal() { | ||
return (this.position === 'top' || this.position === 'bottom'); | ||
} | ||
|
||
getAxis(length) { | ||
const scale = this.scale.getScale(length); | ||
|
||
return d3.svg.axis() | ||
.scale(scale) | ||
.tickFormat(this.tickFormat(this.domain)) | ||
.ticks(this.tickScale(length)) | ||
.orient(this.position); | ||
} | ||
|
||
getScale() { | ||
return this.scale.scale; | ||
} | ||
|
||
addInterval(interval) { | ||
return this.scale.addInterval(interval); | ||
} | ||
|
||
substractInterval(interval) { | ||
return this.scale.substractInterval(interval); | ||
} | ||
|
||
tickScale(length) { | ||
const yTickScale = d3.scale.linear() | ||
.clamp(true) | ||
.domain([20, 40, 1000]) | ||
.range([0, 3, 11]); | ||
|
||
return Math.ceil(yTickScale(length)); | ||
} | ||
|
||
tickFormat() { | ||
if (this.axisFormatter) return this.axisFormatter; | ||
if (this.isPercentage()) return d3.format('%'); | ||
return d3.format('n'); | ||
} | ||
|
||
getLength(el, n) { | ||
if (this.isHorizontal()) { | ||
return $(el).parent().width() / n - this._attr.margin.left - this._attr.margin.right - 50; | ||
} | ||
return $(el).parent().height() / n - this._attr.margin.top - this._attr.margin.bottom; | ||
} | ||
|
||
updateXaxisHeight() { | ||
const self = this; | ||
const selection = d3.select(this.vis.el).selectAll('.vis-wrapper'); | ||
|
||
|
||
selection.each(function () { | ||
const visEl = d3.select(this); | ||
|
||
if (visEl.select('.inner-spacer-block').node() === null) { | ||
visEl.selectAll('.y-axis-spacer-block') | ||
.append('div') | ||
.attr('class', 'inner-spacer-block'); | ||
} | ||
|
||
const height = visEl.select(`.axis-wrapper-${self.position}`).style('height'); | ||
visEl.selectAll(`.y-axis-spacer-block-${self.position} .inner-spacer-block`).style('height', height); | ||
}); | ||
} | ||
|
||
adjustSize() { | ||
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. Is the naming of functions like this and That said, I also don't understand why these functions don't just take the selection and do the job. 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. thats the convention vislib follows ... didnt want to change everything at once, but yeah function namings are not the best. using this "function factory" is not really clear to me ... but its used everywhere. |
||
const self = this; | ||
const xAxisPadding = 15; | ||
|
||
return function (selection) { | ||
const text = selection.selectAll('.tick text'); | ||
const lengths = []; | ||
|
||
text.each(function textWidths() { | ||
lengths.push((() => { | ||
if (self.isHorizontal()) { | ||
return d3.select(this.parentNode).node().getBBox().height; | ||
} else { | ||
return d3.select(this.parentNode).node().getBBox().width; | ||
} | ||
})()); | ||
}); | ||
const length = _.max(lengths); | ||
|
||
if (self.isHorizontal()) { | ||
selection.attr('height', length); | ||
self.updateXaxisHeight(); | ||
if (self.position === 'top') { | ||
selection.select('g') | ||
.attr('transform', `translate(0, ${length - parseInt(self.style.lineWidth)})`); | ||
selection.select('path') | ||
.attr('transform', 'translate(1,0)'); | ||
} | ||
} else { | ||
selection.attr('width', length + xAxisPadding); | ||
if (self.position === 'left') { | ||
const translateWidth = length + xAxisPadding - 2 - parseInt(self.style.lineWidth); | ||
selection.select('g') | ||
.attr('transform', `translate(${translateWidth},${self._attr.margin.top})`); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
draw() { | ||
const self = this; | ||
|
||
return function (selection) { | ||
const n = selection[0].length; | ||
if (self.axisTitle) { | ||
self.axisTitle.render(selection); | ||
} | ||
selection.each(function () { | ||
const el = this; | ||
const div = d3.select(el); | ||
const width = $(el).parent().width(); | ||
const height = $(el).height(); | ||
const length = self.getLength(el, n); | ||
|
||
// Validate whether width and height are not 0 or `NaN` | ||
self.validateWidthandHeight(width, height); | ||
|
||
const axis = self.getAxis(length); | ||
|
||
if (self.show) { | ||
const svg = div.append('svg') | ||
.attr('width', width) | ||
.attr('height', height); | ||
|
||
svg.append('g') | ||
.attr('class', `axis ${self.id}`) | ||
.call(axis); | ||
|
||
const container = svg.select('g.axis').node(); | ||
if (container) { | ||
svg.select('path') | ||
.style('stroke', self.style.color) | ||
.style('stroke-width', self.style.lineWidth) | ||
.style('stroke-opacity', self.style.opacity); | ||
svg.selectAll('line') | ||
.style('stroke', self.style.tickColor) | ||
.style('stroke-width', self.style.tickWidth) | ||
.style('stroke-opacity', self.style.opacity); | ||
// TODO: update to be depenent on position ... | ||
//.attr('x1', -parseInt(self.style.lineWidth) / 2) | ||
//.attr('x2', -parseInt(self.style.lineWidth) / 2 - parseInt(self.style.tickLength)); | ||
|
||
if (self.axisLabels) self.axisLabels.render(svg); | ||
svg.call(self.adjustSize()); | ||
} | ||
} | ||
}); | ||
}; | ||
} | ||
} | ||
|
||
return Axis; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import d3 from 'd3'; | ||
import $ from 'jquery'; | ||
import _ from 'lodash'; | ||
import ErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; | ||
export default function AxisLabelsFactory(Private) { | ||
|
||
const ErrorHandler = Private(ErrorHandlerProvider); | ||
const defaults = { | ||
show: true, | ||
rotate: 0, | ||
rotateAnchor: 'center', | ||
filter: false, | ||
color: '#ddd', | ||
font: '"Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif', // TODO | ||
fontSize: '8pt', | ||
truncate: 100 | ||
}; | ||
|
||
/** | ||
* Appends axis title(s) to the visualization | ||
* | ||
* @class AxisLabels | ||
* @constructor | ||
* @param el {HTMLElement} DOM element | ||
* @param xTitle {String} X-axis title | ||
* @param yTitle {String} Y-axis title | ||
*/ | ||
|
||
class AxisLabels extends ErrorHandler { | ||
constructor(axis, attr) { | ||
super(); | ||
_.extend(this, defaults, attr); | ||
this.axis = axis; | ||
|
||
// horizontal axis with ordinal scale should have labels rotated (so we can fit more) | ||
if (this.axis.isHorizontal() && this.axis.scale.isOrdinal()) { | ||
this.filter = attr && attr.filter ? attr.filter : false; | ||
this.rotate = attr && attr.rotate ? attr.rotate : 70; | ||
} | ||
} | ||
|
||
render(selection) { | ||
selection.call(this.draw()); | ||
}; | ||
|
||
rotateAxisLabels() { | ||
const self = this; | ||
return function (selection) { | ||
const text = selection.selectAll('.tick text'); | ||
|
||
if (self.rotate) { | ||
text | ||
.style('text-anchor', function () { | ||
return self.rotateAnchor === 'center' ? 'center' : 'end'; | ||
}) | ||
.attr('dy', function () { | ||
if (self.axis.isHorizontal()) { | ||
if (self.axis.position === 'top') return '-0.9em'; | ||
else return '0.3em'; | ||
} | ||
return '0'; | ||
}) | ||
.attr('dx', function () { | ||
return self.axis.isHorizontal() ? '-0.9em' : '0'; | ||
}) | ||
.attr('transform', function rotate(d, j) { | ||
if (self.rotateAnchor === 'center') { | ||
const coord = text[0][j].getBBox(); | ||
const transX = ((coord.x) + (coord.width / 2)); | ||
const transY = ((coord.y) + (coord.height / 2)); | ||
return `rotate(${self.rotate}, ${transX}, ${transY})`; | ||
} else { | ||
const rotateDeg = self.axis.position === 'top' ? self.rotate : -self.rotate; | ||
return `rotate(${rotateDeg})`; | ||
} | ||
}); | ||
} | ||
}; | ||
}; | ||
|
||
truncateLabel(text, size) { | ||
const node = d3.select(text).node(); | ||
let str = $(node).text(); | ||
const width = node.getBBox().width; | ||
const chars = str.length; | ||
const pxPerChar = width / chars; | ||
let endChar = 0; | ||
const ellipsesPad = 4; | ||
|
||
if (width > size) { | ||
endChar = Math.floor((size / pxPerChar) - ellipsesPad); | ||
while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') { | ||
endChar = endChar - 1; | ||
} | ||
str = str.substr(0, endChar) + '...'; | ||
} | ||
return str; | ||
}; | ||
|
||
truncateLabels() { | ||
const self = this; | ||
return function (selection) { | ||
selection.selectAll('.tick text') | ||
.text(function () { | ||
// TODO: add title to trancuated labels | ||
return self.truncateLabel(this, self.truncate); | ||
}); | ||
}; | ||
}; | ||
|
||
filterAxisLabels() { | ||
const self = this; | ||
let startX = 0; | ||
let maxW; | ||
let par; | ||
let myX; | ||
let myWidth; | ||
let halfWidth; | ||
let padding = 1.1; | ||
|
||
return function (selection) { | ||
if (!self.filter) return; | ||
|
||
selection.selectAll('.tick text') | ||
.text(function (d) { | ||
par = d3.select(this.parentNode).node(); | ||
myX = self.axis.scale.scale(d); | ||
myWidth = par.getBBox().width * padding; | ||
halfWidth = myWidth / 2; | ||
maxW = $(self.axis.vis.el).find(self.axis.elSelector).width(); | ||
|
||
if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) { | ||
startX = myX + halfWidth; | ||
return self.axis.axisFormatter(d); | ||
} else { | ||
d3.select(this.parentNode).remove(); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
draw() { | ||
const self = this; | ||
|
||
return function (selection) { | ||
selection.each(function () { | ||
selection.selectAll('text') | ||
.attr('style', function () { | ||
const currentStyle = d3.select(this).attr('style'); | ||
return `${currentStyle} font-size: ${self.fontSize};`; | ||
}); | ||
//.attr('x', -3 - parseInt(self.style.lineWidth) / 2 - parseInt(self.style.tickLength)); | ||
if (!self.show) selection.selectAll('test').attr('style', 'display: none;'); | ||
|
||
selection.call(self.truncateLabels()); | ||
selection.call(self.rotateAxisLabels()); | ||
selection.call(self.filterAxisLabels()); | ||
}); | ||
}; | ||
}; | ||
} | ||
|
||
return AxisLabels; | ||
}; |
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.
What do you think about pulling these defaults into a
AxisOptions
class? I think the combination of writing these properties tothis
, and reading from there andthis._attr
is pretty confusing.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.
in my opinion it would make it more confusing ... maybe not with the axis itself ... but AxisLabels have their own config ... following same pattern we would then need AxisLabelsOptions as well ... and AxisTitleOptions ... and in the future i hope to do similar update to other parts of vislib where this would make even less sense. i think by defining defaults like this its quite clear what all the options that this class can handle are. let me know if you dissagree
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.
My thought was that a single
AxisConfig
class could be used across the differentAxis___
classes. I was thinking it would help document all of the options in one place, and also would help pass necessary configuration values intoAxisScale
and such.