/** * Created by rburson on 4/27/16. */ import * as React from 'react' import { CvState, CvProps, CvBaseMixin, CvNavigationResult, CvEvent, CvContext, CvStateChangeResult, CvQueryPaneCallback, CvQueryPane, CvValueProvider, CvValueAdapter, CvValueListener, CvActionHandlerParams, CvProp, CvAction, CvActionCallback, CvActionFiredResult, ColorUtil } from 'catreact' import { FormContext, GraphContext, GraphDef, GraphDataPointDef, ObjUtil, EntityRec, ArrayUtil, Log, Prop, PropDef, PropFormatter } from 'catavolt-sdk' import { LineChart, BarChart, PieChart, ScatterChart, Line, Bar, Pie, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, Tooltip, Legend, Dot, ResponsiveContainer, Cell, ReferenceLine } from 'recharts' const CV_GRADIENT_COLORS = [ // From Flex ['#A1AECF', '#47447A'], // Blue ['#EF7651', '#994C34'], // Red ['#E9C836', '#AA9127'], // Yellow ['#6FB35F', '#497B54'], // Green ['#BA9886', '#AE775B'], // Dingy Red ['#8B8989', '#8B6969'], // Snow Gry ['#9400D3', '#4B0082'], // DarkViolet Blu ['#FFC1C1', '#EEB4B4'], // RosyBrown Red ['#FFFF00', '#EEEE00'], // Yellow Ylw ['#7FFFD4', '#76EEC6'], // Aquamarine Grn ['#CAE1FF', '#BCD2EE'], // LtSteelBl Blu ['#FF6A6A', '#EE6363'], // IndianRed Red ['#FFD700', '#EEC900'], // Gold Gld ['#00FF00', '#32CD32'], // Lime Grn ['#1C86EE', '#104E8B'], // DodgerBlu Blu ['#FF3030', '#EE3B3B'], // FireBrick Red ['#EEE685', '#BDB76B'], // Khaki Brn ['#ADFF2F', '#A2CD5A'], // YellowGrn Grn ['#EE00EE', '#CD00CD'], // Magenta Prp ['#00F5FF', '#00E5EE'], // Turquoise Trq ['#FFFACD', '#EEE8AA'], // LemonChifn Brn ['#54FF9F', '#2E8B57'], // SeaGreen Grn ['#4876FF', '#436EEE'], // RoyalBlue Blu ['#FFAEB9', '#CD8C95'], // LightPink Pnk ['#EEB422', '#CD9B1D'], // Goldenrod Gld ['#7FFF00', '#76EE00'], // Chartreuse Grn ['#6959CD', '#473C8B'], // SlateBlue Blu ['#FFBBFF', '#EEAEEE'], // Plum Prp ['#EEEE00', '#CDCD00'], // Yellow Ylw ['#00FF00', '#32CD32'], // Lime Grn ['#AB82FF', '#9F79EE'], // MedPurple Blu ['#EE7AE9', '#DA70D6'], // Orchid Prp ['#FFF68F', '#CDC673'], // Khaki Brn ['#76EEC6', '#66CDAA'], // SpringGrn Grn ['#0000FF', '#0000EE'], // Blue Blu ['#EE82EE', '#CD96CD'], // Violet Prp ['#FFE7BA', '#F5DEB3'], // Wheat Gry ['#008B8B', '#008080']]; // DarkCyan Trq // const CV_GRAPH_DEFAULT_COLORS = ['#1E6FF2', '#F29500', '#15BFA2', '#6F5F5A']; const CV_GRAPH_DEFAULT_COLORS:string[] = CV_GRADIENT_COLORS.map((g:string[])=>{ return g[1]}); // Convert from gradient to flat /* Base mixin for Charts */ // http://recharts.org/api/#/en-US/api // https://www.w3.org/TR/SVG/styling.html For styling export const CvChart = { axisType: function(graphContext:GraphContext, propName:string, data:Array) { const propDef:PropDef = graphContext.propDefAtName(propName); let numeric = true; if(propDef) { numeric = propDef.isNumericType; } else { //fallback to a sample value numeric = typeof this._getSampleValue(data, propName) === 'number'; } return numeric ? 'number' : 'category'; }, colorForDataPointDef: function (dataPointDef:GraphDataPointDef, i:number) { return this._zeroFill(dataPointDef.seriesColor) || this.props.defaultSeriesColors[i % this.props.defaultSeriesColors.length]; }, getMetaValues: function(graphDef:GraphDef, data:Array) { const minX = graphDef.xAxisRangeFrom ? graphDef.xAxisRangeFrom : 0; const maxX = graphDef.xAxisRangeTo ? graphDef.xAxisRangeTo : 'auto'; const minY = graphDef.yAxisRangeFrom ? graphDef.yAxisRangeFrom : 0; const maxY = graphDef.yAxisRangeTo ? graphDef.yAxisRangeTo : 'auto'; const xAxisLabel = graphDef.xAxisLabel; const yAxisLabel = graphDef.yAxisLabel; return {minX:minX, maxX:maxX, minY:minY, maxY:maxY, xAxisLabel:xAxisLabel, yAxisLabel:yAxisLabel}; }, handleClick(id) { if(this.props.clickHandler) { this.props.clickHandler(id); } }, _applyAlpha: function(c, a:number):string { return ColorUtil.toColorFromNumWithAlpha(Number("0x" + c.substr(1)), a); }, _getSampleValue(data:Array, name:string):any { if(data && data.length > 0) { return data[0][name]; } }, _zeroFill(color:string):string { // charts fail with a color like: #ff so convert to #0000ff; if (color && color.length > 1) { color = color.charAt(0) === '#' ? color.substring(1) : color; while (color.length < 6) { color = '0' + color } return '#' + color; } return null; } } /* Base props for Charts */ export interface CvChartProps extends CvProps { graphContext:GraphContext; data:Array; identPropName:string; defaultSeriesColors:Array; clickHandler:(id:string)=>void; } /* Cartesian style line chart */ export const CvLineChart = React.createClass({ mixins: [CvChart], render: function () { const graphContext = this.props.graphContext; const graphDef = graphContext.graphDef; const {minX, maxX, minY, maxY, xAxisLabel, yAxisLabel} = this.getMetaValues(graphDef, this.props.data); const yPropDef:PropDef = graphContext.propDefAtName(graphDef.dataPointDefs[0].name); // First will do const xType = this.axisType(this.props.graphContext, graphDef.identityDataPointDef.name, this.props.data); const yType = this.axisType(this.props.graphContext, graphDef.dataPointDefs[0].name, this.props.data); const yAxisFormat = (value) => { return PropFormatter.formatValueForRead(value, yPropDef); }; return (
{graphDef.dataPointDefs.map((dataPointDef:GraphDataPointDef, i:number)=>{ const seriesColor = this.colorForDataPointDef(dataPointDef, i); const legendKey = dataPointDef.legendKey || dataPointDef.name; return {this.handleClick(data.id)}}/> })}
); }, }); /* Cartesian style bar chart */ export interface CvBarChartProps extends CvChartProps { stack?:boolean; } export const CvBarChart = React.createClass({ mixins: [CvChart], render: function () { const graphContext = this.props.graphContext; const graphDef = graphContext.graphDef; const {minX, maxX, minY, maxY, xAxisLabel, yAxisLabel} = this.getMetaValues(graphDef, this.props.data); const yPropDef:PropDef = graphContext.propDefAtName(graphDef.dataPointDefs[0].name); // First will do const xType = this.axisType(this.props.graphContext, graphDef.identityDataPointDef.name, this.props.data); const yType = this.axisType(this.props.graphContext, graphDef.dataPointDefs[0].name, this.props.data); const yAxisFormat = (value) => { return PropFormatter.formatValueForRead(value, yPropDef); }; return (
{graphDef.dataPointDefs.map((dataPointDef:GraphDataPointDef, i1:number)=>{ const legendKey = dataPointDef.legendKey || dataPointDef.name; const seriesColor = this.colorForDataPointDef(dataPointDef, i1); const props = { dataKey:dataPointDef.name, name:legendKey, fill:seriesColor, onClick:(data)=>{this.handleClick(data.id)}, key:i1 }; if(this.props.stack) props['stackId'] = 'a'; return { this.props.data.map((data, i2) => { // Check for annotation color const c = data.annoColor ? data.annoColor : seriesColor; return }) } })}
); } }); /* Cartesian style Scatter chart */ export interface CvScatterChartProps extends CvChartProps { bubble?:boolean; } export const CvScatterChart = React.createClass({ mixins: [CvChart], render: function () { const graphDef = this.props.graphContext.graphDef; const {minX, maxX, minY, maxY, xAxisLabel, yAxisLabel} = this.getMetaValues(graphDef, this.props.data); //note: to support multiple 'scatter series' the data set has to be broken in corresponding discrete sets //for now, we're taking the first dataPointDef only const dataPointDef = graphDef.dataPointDefs[0]; const xType = this.axisType(this.props.graphContext, dataPointDef.xAxisName, this.props.data); const yType = this.axisType(this.props.graphContext, dataPointDef.name, this.props.data); if(dataPointDef) { const legendKey = dataPointDef.legendKey || dataPointDef.name; if (graphDef.displayQuadrantLines) { var xRef = ; var yRef = ; } return (
{ xRef } { yRef } {(()=>{ if(this.props.bubble) { if(dataPointDef.bubbleRadiusName) { return } else { return } } else { return } })()} {this.handleClick(data.id)}}> { this.props.data.map((data, i2) => { // Check for annotation color let f = data.annoColor ? data.annoColor : this.colorForDataPointDef(dataPointDef, 0); if (dataPointDef.bubbleRadiusName) { f = this._applyAlpha(f, .4) }; return }) }
); } else { return null; } }, _labelFormatterFunction: function(a,b,c,d) { return "here"; }, _formatterFunction: function(a,b,c,d) { return "here"; } }); /* Pie chart */ export interface CvPieChartProps extends CvChartProps { } export const CvPieChart = React.createClass({ mixins: [CvChart], render: function () { const graphContext = this.props.graphContext; const graphDef = graphContext.graphDef; //note: to support multiple 'pie levels' the data set has to be broken in corresponding discrete sets //for now, we're taking the first dataPointDef only const dataPointDef = graphDef.dataPointDefs[0]; if(dataPointDef) { const propDef:PropDef = graphContext.propDefAtName(dataPointDef.name); const legendKey = dataPointDef.legendKey || dataPointDef.name; return (
{this.handleClick(data.id)}}> {return PropFormatter.formatValueForRead(v.value, propDef)}}> {this.props.data.map((data, i:number)=>{ // Check for annotation color const color = data.annoColor ? data.annoColor : this.colorForDataPointDef(dataPointDef, i); return })}
); } else { return null; } } }); export interface CvGraphPanelState extends CvState { } export interface CvGraphPanelProps extends CvProps { paneRef?:number; formContext?:FormContext; graphContext:GraphContext; navigationListeners?:Array<(event:CvEvent)=>void>; selectionListener?:CvValueListener>; actionListeners?:Array<(event:CvEvent)=>void> stateChangeListeners?:Array<(event:CvEvent)=>void>; navTarget?:string; defaultSeriesColors?:Array; actionProvider?:CvValueProvider; } /* *************************************************** * Render a Graph *************************************************** */ export const CvGraphPanel = React.createClass({ mixins: [CvBaseMixin], componentDidMount: function () { }, getDefaultProps: function () { return { paneRef: null, formContext: null, graphContext: null, navigationListeners: [], selectionListener: null, stateChangeListeners: [], actionListeners: [], defaultSeriesColors: CV_GRAPH_DEFAULT_COLORS, navTarget: null, actionProvider: null } }, getInitialState: function () { return {} }, render: function () { const {} = this.state; const graphPaneProps = { paneRef: this.props.paneRef, formContext: this.props.formContext, queryContext: this.props.graphContext, stateChangeListeners: this.props.stateChangeListeners, actionListeners: this.props.actionListeners, actionProvider: this.props.actionProvider } return ( { const graphContext:GraphContext = cvContext.scopeCtx.scopeObj; return }}/> ); }, }); export interface CvGraphState extends CvState { } export interface CvGraphProps extends CvProps { graphContext:GraphContext; lastRefreshTime:Date, navigationListeners?:Array<(event:CvEvent)=>void>; selectionListener?:CvValueListener>; actionListeners?:Array<(event:CvEvent)=>void> stateChangeListeners?:Array<(event:CvEvent)=>void>; navTarget?:string; defaultSeriesColors?:Array; } export const CvGraph = React.createClass({ mixins: [CvBaseMixin], componentDidMount: function () { }, getDefaultProps: function () { return { graphContext: null, lastRefreshTime: null, navigationListeners: [], selectionListener: null, stateChangeListeners: [], actionListeners: [], defaultSeriesColors: CV_GRAPH_DEFAULT_COLORS, navTarget: null, } }, getInitialState: function () { return {} }, render: function () { const graphContext:GraphContext = this.props.graphContext; const selectionAdapter:CvValueAdapter> = new CvValueAdapter>(); const selectionListener:CvValueListener> = selectionAdapter.createValueListener(); return { const clickHandler = (id:string) => { selectionListener([id]); // Because the selectionListener is setting state that needs to be read by the callback, // don't fire the callback until the state is set. Accomplished by setting some dummy // state. this.setState({}, ()=>{callback.fireAction()}); } const graphDef:GraphDef = graphContext.graphDef; const records:Array = ArrayUtil.copy(graphContext.scroller.buffer); const points:Array = graphDef.graphType === GraphDef.GRAPH_TYPE_PIE ? this._generateDataForPieChart(graphContext, records) : this._generateData(graphContext, records); const identPropName = graphDef.identityDataPointDef.name; const plotTypes = graphDef.dataPointDefs.map((dataPointDef:GraphDataPointDef)=>{ return dataPointDef.plotType }).filter((v:string)=>{ return !!v; }); if(graphDef.graphType === GraphDef.GRAPH_TYPE_CARTESIAN) { if (plotTypes.length === 0 || plotTypes.indexOf(GraphDef.PLOT_TYPE_BAR) > -1){ return } else if(plotTypes.indexOf(GraphDef.PLOT_TYPE_LINE) > -1) { return } else if(plotTypes.indexOf(GraphDef.PLOT_TYPE_STACKED) > -1) { return } else if(plotTypes.indexOf(GraphDef.PLOT_TYPE_SCATTER) > -1) { return }else if(plotTypes.indexOf(GraphDef.PLOT_TYPE_BUBBLE) > -1 ) { return } else { return null; } } else if(graphDef.graphType === GraphDef.GRAPH_TYPE_PIE) { return } else { return null; } }}/> }, shouldComponentUpdate(nextProps, nextState) { /* This is an 'inclusive' change list - updates DO NOT happen by default Desired updates should be explicitly added here */ //update if we have new list data if (nextProps.lastRefreshTime) { if (this.props.lastRefreshTime) { if (nextProps.lastRefreshTime.getTime() > this.props.lastRefreshTime.getTime()) { return true; } } else { return true; } } return false; }, _formatProp: function(prop:Prop, propDef:PropDef) { if(propDef && !propDef.isNumericType) { return PropFormatter.formatValueForRead(prop.value, propDef); } else if(typeof prop.value !== 'number') { return PropFormatter.formatValueForRead(prop.value, null); } else { return Number(prop.value); } }, _generateData: function(graphContext:GraphContext, records:Array):Array { const graphDef:GraphDef = graphContext.graphDef; //prop for 1 axis (or dimension) const identPropName = graphDef.identityDataPointDef.name; const points = records.map((record:EntityRec)=>{ const item = {} item['id'] = record.objectId; item[identPropName] = this._formatProp(record.propAtName(identPropName), graphContext.propDefAtName(identPropName)); item['annoColor'] = record.foregroundColor; //possible props for other axes (or dimensions) graphDef.dataPointDefs.forEach((dataPointDef:GraphDataPointDef)=>{ const propName = dataPointDef.name; if(propName) item[propName] = this._formatProp(record.propAtName(propName), graphContext.propDefAtName(propName)); const xAxisName = dataPointDef.xAxisName; if(xAxisName) item[xAxisName] = this._formatProp(record.propAtName(xAxisName), graphContext.propDefAtName(xAxisName)); const radiusName = dataPointDef.bubbleRadiusName; if(radiusName) item[radiusName] = Number(record.propAtName(radiusName).value); }); return item; }); return points; }, _generateDataForPieChart: function(graphContext:GraphContext, records:Array) { /* The pie chart requires the primary series to be numeric */ const graphDef:GraphDef = graphContext.graphDef; //prop for 1 axis (or dimension) const identPropName = graphDef.identityDataPointDef.name; const points = records.map((record:EntityRec)=>{ const item = {} item['id'] = record.objectId; item[identPropName] = this._formatProp(record.propAtName(identPropName), graphContext.propDefAtName(identPropName)); item['annoColor'] = record.foregroundColor; graphDef.dataPointDefs.forEach((dataPointDef:GraphDataPointDef)=>{ const propName = dataPointDef.name; //force numeric typing if(propName) item[propName] = Number(record.propAtName(propName).value); }); return item; }); return points; }, });