/**
* charts module.
* @module charts
*/
import {
validateMapperDefinition,
validateMapping,
annotateMapping,
default as makeMapper,
} from "./mapping";
import { inferTypes } from "./dataset";
import { RAWError } from "./utils";
import { getOptions, getDefaultOptions } from "./options";
import isObject from "lodash/isObject";
import isFunction from "lodash/isFunction";
import mapValues from "lodash/mapValues";
import get from "lodash/get";
export const baseOptions = {
width: {
type: "number",
default: 500,
},
height: {
type: "number",
default: 500,
},
background: {
type: "color",
default: "#FFFFFF",
},
margins: {
type: "number",
default: 10,
},
};
/**
* @class
*/
class Chart {
/**
* @constructor
* @param {VisualModel} visualModel visual model
* @param {Array.<Object>} data
* @param {Object} dataTypes
* @param {Object} mapping
* @param {Object} visualOptions
*/
constructor(visualModel, data, dataTypes, mapping, visualOptions) {
this._visualModel = visualModel;
this._data = data;
if (
data &&
(!this._dataTypes ||
(typeof this._dataType === "object" &&
Object.keys(this._dataTypes).length))
) {
this._dataTypes = inferTypes(data);
} else {
this._dataTypes = dataTypes;
}
this._mapping = mapping;
this._visualOptions = visualOptions;
}
/**
* @param {Array.<Object>} _data
* @returns {Chart}
*/
data(_data) {
if (!arguments.length) {
return this._data;
}
let dataTypes;
if (
!this._dataTypes ||
(typeof this._dataType === "object" &&
Object.keys(this._dataTypes).length)
) {
dataTypes = inferTypes(_data);
} else {
dataTypes = this.dataTypes;
}
return new RAWChart(
this._visualModel,
_data,
dataTypes,
this._mapping,
this._visualOptions
);
}
/**
* @param {DataTypes} _dataTypes
* @returns {Chart}
*/
dataTypes(_dataTypes) {
if (!arguments.length) {
return this._dataTypes;
}
return new RAWChart(
this._visualModel,
this._data,
_dataTypes,
this._mapping,
this._visualOptions
);
}
/**
* @param {Node} node
* @returns {Node}
*/
getContainer(document) {
//#TODO: this could, in future, depend on visual model
const container = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
container.setAttribute("width", this._visualOptions.width);
container.setAttribute("height", this._visualOptions.height);
container.style["background-color"] = this._visualOptions.background;
return container;
}
mapData() {
let dimensions = this._visualModel.dimensions;
validateMapperDefinition(dimensions);
validateMapping(dimensions, this._mapping, this._dataTypes);
if (isFunction(this._visualModel.mapData)) {
const annotatedMapping = annotateMapping(
dimensions,
this._mapping,
this._dataTypes
);
return this._visualModel.mapData(
this._data,
annotatedMapping,
this._dataTypes,
dimensions
);
} else if (isObject(this._visualModel.mapData)) {
const dimensionsWithOperations = dimensions.map((dim) => {
return {
...dim,
operation: this._visualModel.mapData[dim.id],
};
});
const mapFunction = makeMapper(
dimensionsWithOperations,
this._mapping,
this._dataTypes
);
return mapFunction(this._data);
} else {
throw new RAWError(
"mapData property of visualModel should be a function or an object"
);
}
}
/**
* @param {Node} node
* @returns {DOMChart}
*/
renderToDOM(node) {
if (!this._visualModel) {
throw new RAWError("cannot render: visualModel is not set");
}
const container = this.getContainer(node.ownerDocument);
const vizData = this._visualModel.skipMapping ? this._data : this.mapData();
const dimensions = this._visualModel.dimensions;
const annotatedMapping = annotateMapping(
dimensions,
this._mapping,
this._dataTypes
);
this._visualModel.render(
container,
vizData,
this._visualOptions,
annotatedMapping,
this._data
);
node.innerHTML = "";
node.appendChild(container);
return new DOMChart(
node,
this._visualModel,
this._data,
this._dataTypes,
this._mapping,
this._visualOptions
);
}
/**
* @param {document} document HTML document context (optional if window is available)
* @returns {string}
*/
renderToString(document) {
if (!this._visualModel) {
throw new RAWError("cannot render: visualModel is not set");
}
if (!document && window === undefined) {
throw new RAWError("Document must be passed or window available");
}
const container = this.getContainer(document || window.document);
const vizData = this._visualModel.skipMapping ? this._data : this.mapData();
const dimensions = this._visualModel.dimensions;
const annotatedMapping = annotateMapping(
dimensions,
this._mapping,
this._dataTypes
);
this._visualModel.render(
container,
vizData,
this._visualOptions,
annotatedMapping,
this._data
);
return container.outerHTML;
}
}
class DOMChart extends Chart {
constructor(node, ...args) {
super(...args);
this._node = node;
}
}
/**
* @typedef DataTypes
* @global
* @type {object}
*/
/**
* @typedef Dimension
* @global
* @type {object}
* @property {string} id unique id
* @property {string} name label
* @property {boolean} required
* @property {'get'| 'group'|'groups'|'rollup'|'rollup-leaf'|'rollups'|'groupAggregate'|'groupBy'|'proxy'} operation the operation type
* @property {Object} targets only for proxy operations
* @property {Boolean} [multiple=false] controls if a dimension accept a value with more than one item
* @property {number} [minValues=undefined] min number of items required for the value of the dimension
* @property {number} [maxValues=undefined] max number of items required for the value of the dimension
* @property {Array} validTypes valid data types for the dimension (one or more of 'number', 'string', 'date', 'boolean')
*/
/**
* @typedef MappingDefinition
* @global
* @type {Array.<Dimension>}
*/
/**
* @typedef MappedDimension
* @global
* @type {object}
*/
/**
* @typedef Mapping
* @global
* @type {object}
*/
/**
* @typedef VisualOption
* @global
* @type {object}
*/
/**
* @typedef VisualOptionsDefinition
* @global
* @type {Array.<VisualOption>}
*/
/**
* @typedef VisualOptions
* @global
* @type {object}
*/
/**
* @typedef RenderFunction
* @global
* @type {function}
* @param {Node} node
* @param {any} data the data from mapping
* @param {object} visualOptions the chart visual options
* @param {object} mapping the mapping from column names to
* @param {array} originalData the original tabular dataset
*/
/**
* @typedef VisualModel
* @global
* @type {object}
* @property {RenderFunction} render the render function
* @property {MappingDefinition} dimensions the dimensions configuration (mapping definition)
* @property {VisualOptionsDefinition} options the visual options exposed by the model
* @property {Boolean} [skipMapping=false] if set to true will skip the mapping phase (current mapping is still passed to the render function)
*/
/**
* @typedef RawConfig
* @global
* @type {object}
* @property {Array.<Object>} data - the tabular data to be represented
* @property {DataTypes} dataTypes - object with data types annotations (column name => type name)
* @property {Mapping} mapping - the current mapping of column names to dimensions of the current visual model
* @property {VisualOptions} visualOptions - visual options
*/
/**
* raw factory function
* @description This is the entry point for creating a chart with raw. It will return an instance of the RAWChart class
* @param {VisualModel} visualModel
* @param {RawConfig} config
* @returns {Chart}
*/
function chart(visualModel, config = {}) {
const { data, dataTypes, mapping, visualOptions = {} } = config;
const finalVisualOptions = getOptions(
getDefaultOptions(baseOptions),
visualOptions
);
return new Chart(visualModel, data, dataTypes, mapping, finalVisualOptions);
}
export default chart;