An important aspect when working on Opportunities (or anything with a process
that a record progresses through) is to knowing how far through the Sales
Process a particular Opportunity is. While some users may be familiar
enough with the Sales Process to be able to figure this out immediately from the
Stage Name, a visual indicator always helps.
Something I've been working on for a while now is using a line chart for just
this purpose. The key elements of this are:
-
List all of the Stages for the Opportunity - if record types are in use,
this should be the Stages applicable to that record type
-
Plot a green line and markers for Stages that have been completed (or
skipped, if the Opportunity has jumped into the process at an advanced
staged
- Plot a red line and markers for Stages still to come
The first point had always been a sticking point, but I managed to solve this
using back in January by allowing Visualforce to render the specific picklist
for the record, and then sending this back to the controller prior to
processing the record, as detailed in
this post.
It will come as no surprise to regular readers of this blog that I chose to
handle the second and third points using the Dojo Charting framework.
I've finally completed the charting code - below is a screenshot
showing the chart embedded into the standard Opportunity record view page:
The controller simply walks the Stage Names and adds them to a complete list
until it encounters the current state. All Stage Names after this go
into a todo list - note that both Closed-Won and Closed-Lost appear, and also
the default '-- None-- entry - probably not what is desired and left as an
exercise for the avid student. The page is as follows:
<apex:page standardController="Opportunity" extensions="OpportunityStatusChartController" showheader="false" standardstylesheets="false" sidebar="false">
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js"
djConfig="parseOnLoad: true,
modulePaths: {
'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo',
'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit',
'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox'
}
">
</script>
<link rel="stylesheet"
href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css" />
</head>
<body class="claro">
<apex:form id="frm">
<apex:actionFunction name="reloadWithStages" action="{!reload}" />
<div id="test1" style="width: 100%; height: 150px;"></div>
<apex:outputPanel layout="block" id="vals" style="display:none">
<apex:inputField value="{!Opportunity.StageName}" required="false" id="stages"/>
<apex:inputText value="{!valsText}" required="false" id="back"/>
</apex:outputPanel>
</apex:form>
<apex:outputField value="{!Opportunity.StageName}" rendered="false"/>
<script>
function reload()
{
var ele=document.getElementById('{!$Component.frm.stages}');
var idx=0;
var valText='';
for (idx=0; idx<ele.length; idx++)
{
valText+=ele.options[idx].text + ':';
}
var backele=document.getElementById('{!$Component.frm.back}');
backele.value=valText;
reloadWithStages();
}
<apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
dojo.require("dojox.charting.Chart2D");
dojo.require("dojox.charting.axis2d.Default");
dojo.require("dojox.charting.plot2d.Default");
dojo.require("dojox.charting.plot2d.StackedLines");
dojo.require("dojox.charting.plot2d.Columns");
dojo.require("dojox.charting.plot2d.Bars");
dojo.require("dojox.charting.plot2d.ClusteredBars");
dojo.require("dojox.charting.plot2d.StackedBars");
dojo.require("dojox.charting.plot2d.Bubble");
dojo.require("dojox.charting.plot2d.Grid");
dojo.require("dojox.charting.plot2d.Pie");
dojo.require("dojox.charting.themes.PlotKit.green");
dojo.require("dojox.charting.action2d.Highlight");
dojo.require("dojox.charting.action2d.Magnify");
dojo.require("dojox.charting.action2d.MoveSlice");
dojo.require("dojox.charting.action2d.Shake");
dojo.require("dojox.charting.action2d.Tooltip");
dojo.require("dojox.charting.widget.Legend");
dojo.require("dojo.colors");
dojo.require("dojo.fx.easing");
dojo.require("dojo.date.stamp");
dojo.require("dojo.date.locale");
makeCharts = function(){
var myMap={
<apex:repeat value="{!labels}" var="label">
'{!label.idx}':'{!label.text}'<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
</apex:repeat>
};
var chart1 = new dojox.charting.Chart2D("test1");
chart1.setTheme(dojox.charting.themes.PlotKit.green);
chart1.addPlot("default", {type: "Default", lines: true, markers: true, tension:2});
chart1.addAxis("x",
{
majorTick: {stroke: "black", length: 3},
majorTickStep:1,
minorTicks: false,
microTicks: false,
min: 0,
max: {!labelCount},
rotation:30,
font: "6pt Tahoma",
labels: [
<apex:repeat value="{!labels}" var="label">
{value: {!label.idx}, text:'{!label.text}'}<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
</apex:repeat>
]
});
chart1.addSeries("cleared",
[
<apex:repeat value="{!doneStageNumbers}" var="stage">
{x: {!stage}, y: 2},
</apex:repeat>
]);
chart1.addSeries("todo",
[
<apex:repeat value="{!todoStageNumbers}" var="stage">
{x: {!stage}, y: 2},
</apex:repeat>
],
{plot: "default", stroke: {color:"#FE2E2E"}}
);
var myMap={
<apex:repeat value="{!tooltips}" var="tooltip">
"{!tooltip.idx}": "{!tooltip.text}",
</apex:repeat>
};
var anim1a = new dojox.charting.action2d.Magnify(chart1, "default");
var anim1b = new dojox.charting.action2d.Tooltip(chart1, "default",
{
text : function(o) {
return (myMap[o.x])
}
});
chart1.render();
};
</apex:outputPanel>
dojo.addOnLoad(
<apex:outputPanel layout="none" rendered="{!loadOnce}">
reload
</apex:outputPanel>
<apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
makeCharts
</apex:outputPanel>
);
</script>
</body>
</apex:page>
Essentially this page has two distinct functions - the first to render a
hidden form containing an input field for the StageName field, and then submit
the values back via javascript, while the second is to actually render the
chart based on the values. The completed stages are plotted as a
separate series to those that remain, to allow that part of the chart to be
rendered in a different colour and markers. I've also thrown in a tooltip on
each of my markers so that I can display some help/explanation text about each
of the stages.
The controller is quite a simple one:
public class OpportunityStatusChartController
{
public List<Tuple> labels {get; set;}
public List<Tuple> tooltips {get; set;}
public List<Integer> doneStageNumbers {get; set;}
public List<Integer> todoStageNumbers {get; set;}
public Integer labelCount {get; set;}
public Boolean loadOnce {get; set;}
public String valsText {get; set;}
private Opportunity opp;
public OpportunityStatusChartController(ApexPages.StandardController std)
{
opp=(Opportunity) std.getRecord();
loadOnce=true;
}
public PageReference reload()
{
init();
loadOnce=false;
return null;
}
public void init()
{
labels=new List<Tuple>();
tooltips=new List<Tuple>();
doneStageNumbers=new List<Integer>();
todoStageNumbers=new List<Integer>();
labelCount=0;
Boolean done=false;
labels.add(new Tuple(labelCount++, '.'));
for (String val : valsText.split(':'))
{
if (!done)
{
doneStageNumbers.add(labelCount);
}
else
{
todoStageNumbers.add(labelCount);
}
if (val==opp.StageName)
{
done=true;
todoStageNumbers.add(labelCount);
}
labels.add(new Tuple(labelCount, val));
toolTips.add(new Tuple(labelCount, 'Help for ' + val + ' stage'));
labelCount++;
}
labels.add(new Tuple(labelCount, '.'));
}
public class Tuple
{
public Integer idx {get; set;}
public String text {get; set;}
public Tuple(Integer inIdx, String inText)
{
idx=inIdx;
text=inText;
}
}
}
The labels are returned in a containing Tuple class along with an index, which
is the x-axis position of the ploy. Everything is plotted at the same
level on the y-axis, as I want a straight line chart.
I can then change the record type of the Opportunity, save it and the chart
updates accordingly:
You can see a live example of this on my
demo site - simply click on one of the Opportunities to see the progress chart.