Skip to content

Commit

Permalink
Refactor axis to use common implementation for x and y (#140)
Browse files Browse the repository at this point in the history
* Refactor axis to use a common implementation for x and y

Refactor to group by type of axis (linear/time/ordinal) rather than by direction.
Use a common implementation for each chart for creating an axis, based on the settings and data it will be working from.
Allow for aggregates that are strings turned into numbers (e.g. `count`) or left as string (e.g. `dominant`)

* Allow charts to say which axis types they can't use

* Applied new axis types to other charts

* Rename files

* Refactor `labelFunction` and remove `crossAxis`

* Fix variances in screenshot tests

* Restore heatmap vertical gridline

* Exclude "linear" ranges from various charts

* Default padding for chart axis, which can be overridden

* Restore label rotation/hiding code that was lost in merge
andy-lee-eng authored Apr 2, 2019
1 parent 1a7045c commit 51cad54
Showing 27 changed files with 743 additions and 440 deletions.
134 changes: 134 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
import * as fc from "d3fc";
import {axisType} from "./axisType";
import * as none from "./noAxis";
import * as linear from "./linearAxis";
import * as time from "./timeAxis";
import * as ordinal from "./ordinalAxis";

const axisTypes = {
none,
ordinal,
time,
linear
};

export const axisFactory = settings => {
let excludeType = null;
let orient = "horizontal";
let settingName = "crossValues";
let settingValue = null;
let valueNames = ["crossValue"];

const optionalParams = ["include", "paddingStrategy", "pad"];
const optional = {};

const _factory = data => {
const useType = axisType(settings)
.excludeType(excludeType)
.settingName(settingName)
.settingValue(settingValue)();

const axis = axisTypes[useType];
const domainFunction = axis.domain().valueNames(valueNames);

optionalParams.forEach(p => {
if (optional[p] && domainFunction[p]) domainFunction[p](optional[p]);
});
if (domainFunction.orient) domainFunction.orient(orient);

const domain = domainFunction(data);
const component = axis.component ? createComponent(axis, domain, data) : defaultComponent();

return {
scale: axis.scale(),
domain,
labelFunction: axis.labelFunction,
component: {
bottom: component.bottom,
left: component.left
},
size: component.size,
decorate: component.decorate,
label: settings[settingName].map(v => v.name).join(", ")
};
};

const createComponent = (axis, domain, data) =>
axis
.component(settings)
.orient(orient)
.settingName(settingName)
.domain(domain)(data);

const defaultComponent = () => ({
bottom: fc.axisBottom,
left: fc.axisLeft,
decorate: () => {}
});

_factory.excludeType = (...args) => {
if (!args.length) {
return excludeType;
}
excludeType = args[0];
return _factory;
};

_factory.orient = (...args) => {
if (!args.length) {
return orient;
}
orient = args[0];
return _factory;
};

_factory.settingName = (...args) => {
if (!args.length) {
return settingName;
}
settingName = args[0];
return _factory;
};
_factory.settingValue = (...args) => {
if (!args.length) {
return settingValue;
}
settingValue = args[0];
return _factory;
};

_factory.valueName = (...args) => {
if (!args.length) {
return valueNames[0];
}
valueNames = [args[0]];
return _factory;
};
_factory.valueNames = (...args) => {
if (!args.length) {
return valueNames;
}
valueNames = args[0];
return _factory;
};

optionalParams.forEach(p => {
_factory[p] = (...args) => {
if (!args.length) {
return optional[p];
}
optional[p] = args[0];
return _factory;
};
});

return _factory;
};
42 changes: 42 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
import {rebindAll} from "d3fc";
import {axisType} from "./axisType";
import {labelFunction as noLabel} from "./noAxis";
import {labelFunction as timeLabel} from "./timeAxis";
import {labelFunction as linearLabel} from "./linearAxis";
import {labelFunction as ordinalLabel} from "./ordinalAxis";

const labelFunctions = {
none: noLabel,
ordinal: ordinalLabel,
time: timeLabel,
linear: linearLabel
};

export const labelFunction = settings => {
const base = axisType(settings);
let valueName = "__ROW_PATH__";

const label = (d, i) => {
return labelFunctions[base()](valueName)(d, i);
};

rebindAll(label, base);

label.valueName = (...args) => {
if (!args.length) {
return valueName;
}
valueName = args[0];
return label;
};

return label;
};
73 changes: 73 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/axisType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

export const AXIS_TYPES = {
none: "none",
ordinal: "ordinal",
time: "time",
linear: "linear"
};

export const axisType = settings => {
let settingName = "crossValues";
let settingValue = null;
let excludeType = null;

const getType = () => {
const checkTypes = types => {
const list = settingValue ? settings[settingName].filter(s => settingValue == s.name) : settings[settingName];

if (settingName == "crossValues" && list.length > 1) {
// can't do multiple values on non-ordinal cross-axis
return false;
}

return list.some(s => types.includes(s.type));
};

if (settings[settingName].length === 0) {
return AXIS_TYPES.none;
} else if (excludeType != AXIS_TYPES.time && checkTypes(["datetime"])) {
return AXIS_TYPES.time;
} else if (excludeType != AXIS_TYPES.linear && checkTypes(["integer", "float"])) {
return AXIS_TYPES.linear;
}

if (excludeType == AXIS_TYPES.ordinal) {
return AXIS_TYPES.linear;
}
return AXIS_TYPES.ordinal;
};

getType.settingName = (...args) => {
if (!args.length) {
return settingName;
}
settingName = args[0];
return getType;
};

getType.settingValue = (...args) => {
if (!args.length) {
return settingValue;
}
settingValue = args[0];
return getType;
};

getType.excludeType = (...args) => {
if (!args.length) {
return excludeType;
}
excludeType = args[0];
return getType;
};

return getType;
};
38 changes: 38 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
import * as fc from "d3fc";

export const chartSvgFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartSvgCartesian);
export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartCanvasCartesian);

const chartFactory = (xAxis, yAxis, cartesian) => {
const chart = cartesian({
xScale: xAxis.scale,
yScale: yAxis.scale,
xAxis: xAxis.component,
yAxis: yAxis.component
})
.xDomain(xAxis.domain)
.xLabel(xAxis.label)
.xAxisHeight(xAxis.size)
.xDecorate(xAxis.decorate)
.yDomain(yAxis.domain)
.yLabel(yAxis.label)
.yAxisWidth(yAxis.size)
.yDecorate(yAxis.decorate)
.yOrient("left");

// Padding defaults can be overridden
chart.xPaddingInner && chart.xPaddingInner(1);
chart.xPaddingOuter && chart.xPaddingOuter(0.5);
chart.yPaddingInner && chart.yPaddingInner(1);
chart.yPaddingOuter && chart.yPaddingOuter(0.5);

return chart;
};
25 changes: 25 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/flatten.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

export const flattenExtent = array => {
const withUndefined = fn => (a, b) => {
if (a === undefined) return b;
if (b === undefined) return a;
return fn(a, b);
};
return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]);
};

export const flattenArray = array => {
if (Array.isArray(array)) {
return [].concat(...array.map(flattenArray));
} else {
return [array];
}
};
70 changes: 70 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/
import * as d3 from "d3";
import * as fc from "d3fc";
import {flattenArray} from "./flatten";
import {extentLinear as customExtent} from "../d3fc/extent/extentLinear";

export const scale = () => d3.scaleLinear();

export const domain = () => {
const base = customExtent()
.pad([0, 0.1])
.padUnit("percent");

let valueNames = ["crossValue"];

const _domain = data => {
base.accessors(valueNames.map(v => d => parseFloat(d[v])));

return getDataExtent(flattenArray(data));
};

fc.rebindAll(_domain, base);

const getMinimumGap = data => {
const gaps = valueNames.map(valueName =>
data
.map(d => d[valueName])
.sort((a, b) => a - b)
.filter((d, i, a) => i === 0 || d !== a[i - 1])
.reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1])))
);

return Math.min(...gaps);
};

const getDataExtent = data => {
if (base.padUnit() == "domain") {
const dataWidth = getMinimumGap(data);
return base.pad([dataWidth / 2, dataWidth / 2])(data);
} else {
return base(data);
}
};

_domain.valueName = (...args) => {
if (!args.length) {
return valueNames[0];
}
valueNames = [args[0]];
return _domain;
};
_domain.valueNames = (...args) => {
if (!args.length) {
return valueNames;
}
valueNames = args[0];
return _domain;
};

return _domain;
};

export const labelFunction = valueName => d => d[valueName][0];
Loading

0 comments on commit 51cad54

Please sign in to comment.