import {
ChartView,
convertSelectorToAttribute
} from "../mmviz-common";
import {
LegendComponent,
TemplateComponent
} from "../mmviz-component-dom";
import {
AxisComponentSvg
} from "../mmviz-component-svg";
import {
axisLayouter,
AxisLocationEnum,
getAxisLocationEnum,
getAxisSelector,
getScaleTypeEnum,
LayoutScale
} from "../mmviz-layout";
import {
buildAreaLayer,
buildBarLayer,
buildBarStackLayer,
buildLineLayer,
buildPieLayer,
buildScatterLayer,
buildStackLayer
} from "./layer";
/**
* Get a map of Axis Components based on provided configs
* @param selector - chart selector to associate Axis Componenets with
* @param axisConfigs - array of config objects, one per axis location
*/
function getAxisComponentMap(selector, axisConfigs){
const axisComponentMap: any = {};
let axisConfig,
axisLocationEnum: AxisLocationEnum,
axisSelector: string,
axisComponent: AxisComponentSvg
;
for (const key in axisConfigs) {
if (axisConfigs.hasOwnProperty(key)) {
axisConfig = axisConfigs[key];
axisLocationEnum = getAxisLocationEnum(key);
axisSelector = getAxisSelector(axisLocationEnum);
axisComponent = new AxisComponentSvg(selector, axisSelector, axisConfig.label);
axisComponentMap[key] = axisComponent;
}
}
return axisComponentMap;
}
/**
* Update the Chart View by displaying Legend Components based on provided configs
* @param legendConfigs - array of config objects, one per legend type
* @param layoutScale - contains all scales that map the chart layout
* @param chartView - representation of the chart
*/
function displayLegends(legendConfigs, layoutScale, chartView){
let legendConfig,
legendComponent
;
for (const key in legendConfigs) {
if (legendConfigs.hasOwnProperty(key)) {
legendConfig = legendConfigs[key];
legendComponent = new LegendComponent(".chart-legend-" + key, "chart_legend_" + key);
if (legendConfig.config){
legendComponent.config = legendConfig.config;
}
legendComponent.appendView(chartView.legendsContainer, layoutScale.getScaleModel(key));
// if (legendComponent.isTogglable){
// addLegendItemToggleBehavior(selector, legendComponent.selector);
// }
}
}
if (Object.keys(legendConfigs).length > 0) {
chartView.showLegend();
}
}
/**
* Update the Chart View by displaying the Axis Components in the provided axisComponentMap
* @param axisConfigs
* @param axisComponentMap
* @param layoutScale
* @param chartView
*/
function displayAxies(axisConfigs, axisComponentMap, layoutScale, chartView){
let axisConfig,
axisComponent,
axisLocationEnum: AxisLocationEnum,
axisViewModel
;
for (const key in axisConfigs) {
if (axisConfigs.hasOwnProperty(key)){
axisConfig = axisConfigs[key];
axisComponent = axisComponentMap[key];
axisLocationEnum = getAxisLocationEnum(key);
axisViewModel = axisLayouter(axisLocationEnum, layoutScale, {label: axisConfig.label});
if (axisConfig.configFactory){
axisComponent.config = axisConfig.configFactory(chartView.drawArea.widthSize);
}
axisComponent.updateView(chartView.stage, axisViewModel);
}
}
}
/**
* Build a chart based on provided dataModel and config
* @param dataModel - contains dataArray and functions to map data attributes to chart visuals
* @param config - specifies the features of the chart
*/
export function buildChart(dataModel, config: any){
/*
let dataModel = {
dataArray: dataArray,
keyMap: function(d) { -- uniquely identifies a data point
return d.id;
},
xValueMap: function(d) { -- mapping data to the x-axis for chart types linear, area, scatter
return d.x;
},
yValueMap: function(d) { -- mapping data to the x-axis for chart types linear, area, scatter
return d.y;
},
valueMap: function(d) { -- mapping data to the x-axis for chart types bar, stacked bar
return d.area;
},
areaValueMap: function(d) { -- maps the data to the area of a scatter point
return d.area;
},
colorValueMap: function(d) { -- maps data to color
return d.category;
},
detailsMap: function (d) { -- returns the contents of a details tooltip
var label = d.category,
value = d.x + " - " + d.y;
return `${label}: ${value}`;
}
}
*/
/*
config = {
selector: "#chart", -- selector of element to insert chart into
xScale: "band", -- type scale to use for the chart
(band, linear, sqrt, pow, ordinal, time, threshold, quantile, quantize)
yScale: "linear", -- type of scale to use for the chart
xDomainBuffer: 0.0, -- buffer to add to axis
yDomainBuffer: 0.0, -- buffer to add to axis
axisConfigs: any = {
left: {
label: "Dependant",
configFactory: getLinearAxisConfigY -- function to format the axis labels
},
bottom: {
label: "Independent",
configFactory: getLinearAxisConfigX -- function to format the axis labels
},
...
},
padding: {top: 0, right: 0, bottom: 0, left: 0},
layers: [ -- chart layers
{
type: "area" --type of chart (area, bar, bar-stack, line, pie, scatter)
colorScaleKey: "color" --key to identify the color scale to use
},
...
]
}*/
// start: setup
let axisConfigs: any;
const chartConfig: any = {},
legendConfigs: any = {
color: {}
},
defaultPadding = {top: 41, right: 0, bottom: 48, left: 0}
;
chartConfig.id = convertSelectorToAttribute(config.selector);
chartConfig.selector = config.selector;
chartConfig.title = "";
chartConfig.description = "";
chartConfig.xScale = "linear";
chartConfig.yScale = "linear";
chartConfig.xDomainBuffer = 0;
chartConfig.yDomainBuffer = 0;
chartConfig.aspectRatioPreservation = {alignment: "xMidYMin", scaling: "meet"};
chartConfig.insertChartDom = true;
chartConfig.isFocusable = false;
chartConfig.doUpdateComponents = true;
for (const key in chartConfig) {
if (config.hasOwnProperty(key) && chartConfig.hasOwnProperty(key)){
chartConfig[key] = config[key];
}
}
// populate axis configs
if (config.axisConfigs){
axisConfigs = {
left: {
label: ""
},
bottom: {
label: ""
}
};
let axisConfig;
for (const key in config.axisConfigs) {
if (config.axisConfigs.hasOwnProperty(key)) {
axisConfig = config.axisConfigs[key];
if (!axisConfigs.hasOwnProperty(key)){
axisConfig[key] = {};
}
Object.assign(axisConfigs[key], axisConfig);
}
}
}
// populate legend configs
if (config.legendConfigs){
for (const key in config.legendConfigs) {
if (config.legendConfigs.hasOwnProperty(key)){
let legendConfig = config.legendConfigs[key];
if (!legendConfigs.hasOwnProperty(key)){
legendConfigs[key] = {};
}
Object.assign(legendConfigs[key], legendConfig);
}
}
}
const chartView = new ChartView(config.selector);
chartView.padding = (config.padding) ? config.padding : defaultPadding;
const chartComponent = new TemplateComponent("chart");
const axisComponentMap = getAxisComponentMap(chartConfig.selector, axisConfigs);
// end: setup
// start: create view
const chartViewModel: any = {
id: chartConfig.id,
selector: chartConfig.selector,
title: chartConfig.title,
description: chartConfig.description,
isFocusable: chartConfig.isFocusable
};
if (chartConfig.insertChartDom){
chartComponent.createView(config.selector, chartViewModel);
}
const layoutScale = new LayoutScale(chartView.drawArea),
xScaleTypeEnum = getScaleTypeEnum(chartConfig.xScale),
yScaleTypeEnum = getScaleTypeEnum(chartConfig.yScale)
;
// end: create view
const layerMapperArray = [];
let layerMapper;
for (const layerConfig of config.layers) {
switch (layerConfig.type) {
case "area":
layerMapper = buildAreaLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "bar":
layerMapper = buildBarLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "bar_stack":
layerMapper = buildBarStackLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "line":
layerMapper = buildLineLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "pie":
layerMapper = buildPieLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "scatter":
layerMapper = buildScatterLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
case "stack":
layerMapper = buildStackLayer(layerConfig, chartConfig, layoutScale, chartView);
layerMapperArray.push(layerMapper);
break;
default:
throw "Unknown chart type";
}
}
return () => {
// start: update view
if (!chartView.isViewable){
return;
}
layoutScale.drawArea = chartView.drawArea;
layoutScale
.xScaleSetup(xScaleTypeEnum)
.yScaleSetup(yScaleTypeEnum)
;
chartView.applyResponsiveBehavior();
// map each layer
let layerViewUpdater;
const layerViewUpdaterArray = [];
for (layerMapper of layerMapperArray){
layerViewUpdater = layerMapper(dataModel);
layerViewUpdaterArray.push(layerViewUpdater);
}
// pad the domain to improve visibility
if (chartConfig.xDomainBuffer > 0){
layoutScale.bufferDomainX(chartConfig.xDomainBuffer);
}
if (chartConfig.yDomainBuffer > 0){
layoutScale.bufferDomainY(chartConfig.yDomainBuffer);
}
displayAxies(axisConfigs, axisComponentMap, layoutScale, chartView);
// update the view with each layer
for (layerViewUpdater of layerViewUpdaterArray){
layerViewUpdater();
}
if (chartConfig.doUpdateComponents){
displayLegends(legendConfigs, layoutScale, chartView);
}
};
}