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); } }; }