Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
joining x_axis and y_axis into a single class axis
- splitting it into 3 subclasses (Axis, AxisLabels, AxisScale)
- converting to ES6 classes + style fixes
- adding more customization options
  • Loading branch information
ppisljar committed Sep 12, 2016
commit bc4508d795b50a8357089f7966b36b17c3ec9833
228 changes: 228 additions & 0 deletions src/ui/public/vislib/lib/axis.js
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 = {
Copy link
Contributor

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 to this, and reading from there and this._attr is pretty confusing.

Copy link
Member Author

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

Copy link
Contributor

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 different Axis___ classes. I was thinking it would help document all of the options in one place, and also would help pass necessary configuration values into AxisScale and such.

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);
Copy link
Contributor

@spalger spalger Sep 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second arg here looks like a mistake

Copy link
Member Author

Choose a reason for hiding this comment

The 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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the naming of functions like this and draw() normal to you? I would expect this and draw() to be called something like getDrawFunction() and getAdjustSizeFunction().

That said, I also don't understand why these functions don't just take the selection and do the job.

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
};
164 changes: 164 additions & 0 deletions src/ui/public/vislib/lib/axis_labels.js
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;
};
Loading