If you run the solution shown in this chapter, you will notice that the Chart controls on the pages entitled Historical Charts and Current Call Levels blend in well with the colors and fonts used on the rest of the page. You might think that this was just custom formatting implemented in code, but actually it was done in a much more generic way (described momentarily) that you can use to format any Chart control to match the containing page's style information.
Linked and embedded style sheets allow web developers to separate formatting information from their content and layout information in a web page. Keeping colors and fonts consistent throughout a site is often difficult when you embed formatting information into the HTML document using <font> tags. However, you can use style sheets to define what formatting the browser should use when rendering specified HTML elements. This feature is known as cascading style sheets (CSS) and is documented in the MSDN libraries. I use linked style sheets (meaning the style sheet exists as a separate file and is linked into each page) in all this book's examples and solutions so that the pages look consistent.
Style sheets allow you to specify formatting for any HTML element, but unfortunately they have no control over the content displayed within a COM control on a web page. Because the Office Web Components support all the HTML color and font names Microsoft Internet Explorer supports, you can write code to automatically format your components (in this case the Chart component) to match the styles in your page. Let's take a look at how I did this in the Helpdesk Reporting solution.
I implemented the control formatting code as a generic set of functions that you can include in any HTML page. It resides in a file called FormatControls.scp in the Scripts directory on the companion CD. To include this in the HistoricalCharts.htm page, I inserted the following HTML fragment:
<!-- Common routines for formatting the Office Web Components based on styles --> <script language=vbscript src="../Scripts/FormatControls.scp"> </script> |
In the HTML <script> tag, you can add an optional src attribute. The value for this attribute is the URL of another file containing the contents for the script block. In this case, I've used a relative URL to the FormatControls.scp file in the Scripts directory. This script file implements a number of functions, but the only one needed in this example is the FormatChartFromStyles function, used in the BindChart function:
' Format the chart FormatChartFromStyles cspace |
This function takes a reference to the entire Chart control, and it performs the rest of the formatting work inside the function.
Let's look at the beginning of the FormatChartFromStyles function:
Sub FormatChartFromStyles(cspace) ' Local variables Dim cht ' Temporary WCChart object reference Dim ax ' Temporary WCAxis object reference Dim ser ' Temporary WCSeries object reference Dim dls ' Temporary WCDataLabels object reference Dim vStyleVal ' Temporary style value Dim ct ' Loop counter ' Format the various elements of the ChartSpace ' ChartSpace background vStyleVal = FindStyleValue(Array("ChartSpace","TH"), _ "background-color") If Not(IsEmpty(vStyleVal)) Then cspace.Interior.Color = vStyleVal ' ChartSpace title If cspace.HasChartspaceTitle Then vStyleVal = FindStyleValue(Array("ChartspaceTitle","H2","H1"), _ "background-color") If Not(IsEmpty(vStyleVal)) Then _ cspace.ChartspaceTitle.Interior.Color = vStyleVal SetFontInfo cspace.ChartspaceTitle, _ Array("ChartspaceTitle","H2","H1") End If 'Has a title ' ChartSpace legend If cspace.HasChartspaceLegend Then vStyleVal = FindStyleValue(Array("Legend","Body"), _ "background-color") If Not(IsEmpty(vStyleVal)) Then _ cspace.ChartspaceLegend.Interior.Color = vStyleVal SetFontInfo cspace.ChartspaceLegend, Array("Legend", "P") End If 'Has a legend |
This function cycles through all those elements in the Chart control that can be formatted with color or fonts. Then it cycles through each chart, each series of each chart, and each axis of each chart, formatting each of those elements using the page's style information. This function uses two helper functions in the same script file: FindStyleValue and SetFontInfo. The FindStyleValue function finds style values for the requested selector and style attribute; SetFontInfo is a wrapper function that sets a number of font properties based on the specified selector.
Before I describe how to obtain style information, I want to define a few terms used in style sheets. To help define these terms, let's look at part of the style sheet used for this chapter's code (from Styles.css on the companion CD):
body { background-color: Whitesmoke; font-family: Tahoma; color: Black;} p { color: Black; font-family: Verdana; font-size: 10pt; } h1 { font-family: Tahoma; font-size: 24pt; font-weight: bold; color: Indigo; } Legend { font-family: Tahoma; font-size: 8pt; } Axis { font-family: Tahoma; font-size: 8pt; color: Black; } PlotArea { background-color: Whitesmoke; } ValueAxis { font-family: Tahoma; font-size: 8pt; number-format: #,##0; color: Maroon; } |
A style sheet is essentially a listing of formatting rules. Each rule starts with a logical element name called a selector, and within each rule, you can define a set of style attributes. A selector is often an HTML tag name such as <p> or <h1> or a class name such as td.button. However, you can also include additional names that are not known tags—for example, Legend. Internet Explorer will retain these selectors and expose them in the Document Object Model (DOM), but it will not know how to apply them automatically since it has no idea what Legend is. Style attributes are formatting definitions such as color: Indigo, which states that any element matching the selector should have the color indigo.
All the information from the style sheet is parsed and loaded into objects that you can use from scripts in the page. Given the name of a selector, you can find its style rule and ask for any of its style attributes such as color, background-color, or font-family. The FindStyleValue and GetStyleValue functions use these objects to extract the style value and return it, enabling the FormatChartFromStyles function to apply the same formatting value to the particular chart element. Since the Office Web Components support the same named colors as Internet Explorer, you can assign color values directly from the returned style attribute value to the chart element. Let's look at the GetStyleValue function first:
Function GetStyleValue(sSelector, sAttributeName) ' Local variables Dim ctStyleSheet ' Style sheet loop counter Dim ctRule ' Rule loop counter Dim ssCur ' Current styleSheet object reference ' Check to see that we have at least one styleSheet If document.styleSheets.length = 0 Then Exit Function ' Loop over all styleSheets backward ' (to get the ones with highest precedence first) For ctStyleSheet = (document.styleSheets.length - 1) To 0 Step -1 ' Grab a reference to the current style sheet Set ssCur = document.styleSheets(ctStyleSheet) ' Make sure the style sheet is enabled If Not(ssCur.disabled) Then ' Loop over all rules in the style sheet For ctRule = 0 To ssCur.Rules.Length - 1 ' If the selectorText = the selector we're looking for, ' get the value for the specified attribute If LCase(ssCur.Rules(ctRule).selectorText) = _ LCase(sSelector) Then GetStyleValue = GetAttributeValue( _ ssCur.Rules(ctRule), _ sAttributeName) ' Since we found the selector and ' GetAttributeValue will get the attribute ' if it exists, it's OK to exit the function ' now and return what we have Exit Function End If 'Element we're looking for Next 'ctRule End If 'styleSheet is not disabled Next 'ctStylesheet End Function 'GetStyleValue() |
GetStyleValue takes parameters indicating what selector and attribute name you want to get. The function loops over all style sheets in the current document in reverse order. (You might have both a linked style sheet and an embedded style block.) This ensures that you get the last style definition for the given selector because that will usually be the one that overrides all previous definitions. The code also checks that the style sheet is enabled before attempting to find a selector within it.
Unfortunately, this code must loop over the style rules in the style sheet, looking for one that has a selector property value equal to the requested value. You can define multiple rules that use the same selector, defining some style attributes in one rule and others in different rules. This code does not catch this condition and will simply attempt to use the first rule it finds involving the desired selector. This makes the function a bit faster because I can short-circuit the loop when I find the selector instead of always looping over all rules. Note that I use the LCase function from VBScript to make sure I compare the selector text in a case-insensitive way. You can also use StrComp to perform a case-insensitive comparison.
Once I find a rule involving the desired selector, I use another helper function called GetAttributeValue. This function contains a simple Select Case block that uses the appropriate style object property based on the desired attribute name:
Function GetAttributeValue(rule, sAttributeName) ' Local variables Dim sCssText ' Holder for CSS text Dim nPosStart ' Temporary start position pointer Dim nPosEnd ' Temporary end position pointer ' Switch on the desired attribute name Select Case LCase(sAttributeName) Case "backgroundcolor", "background-color" GetAttributeValue = rule.style.backgroundcolor Case "color" GetAttributeValue = rule.style.color Case "fontfamily", "font-family" GetAttributeValue = rule.style.fontFamily Case "fontsize", "font-size" GetAttributeValue = rule.style.fontSize Case "fontweight", "font-weight" GetAttributeValue = rule.style.fontWeight Case Else ' Custom style attribute ' See whether we can find it in the cssText property sCssText = rule.style.cssText nPosStart = InStr(sCssText, sAttributeName) If nPosStart > 0 Then ' Found it; now extract it nPosStart = nPosStart + Len(sAttributeName) + 1 nPosEnd = InStr(nPosStart, sCssText, ";") If nPosEnd <= 0 Then nPosEnd = Len(sCssText) + 1 GetAttributeValue = Trim(Mid(sCssText, nPosStart, _ nPosEnd - nPosStart)) End If 'Found attribute in cssText End Select 'sAttributeName End Function 'GetAttributeValue() |
Because no method for looking up a style value by string name exists, this function determines the appropriate property of the style object based on the specified style attribute name. It also has a long Case Else block that attempts to find attributes that are not part of the CSS standard but might be in the style sheet. A great example of this is the style information for the ValueAxis selector I showed you earlier. I defined an attribute called number-format, which is not part of the CSS standard but should be defined for a chart's value axis. The GetAttributeValue code parses the value for non-CSS attributes from the style object's cssText property. This property returns the original text from the section of the style sheet in which the current rule exists.
Now that you have a way to get any style attribute value for any selector and attribute name, you need a way to find values given an escalating list of selectors. Suppose you want to allow developers to specify a rule in the style sheet for the chart title. However, if they do not make this specification, you want to use the rule defined for H3. If H3 is not defined, you might want to use the rule for H2 instead. This escalation is done using the FindStyleValue function.
Function FindStyleValue(asSelectors, sAttributeName) ' Local variables Dim ct ' Loop counter ' Loop over all the selectors until GetStyleValue returns ' something other than Empty For ct = LBound(asSelectors) To UBound(asSelectors) FindStyleValue = GetStyleValue(asSelectors(ct), sAttributeName) If Not(IsEmpty(FindStyleValue)) Then Exit Function Next 'ct End Function 'FindStyleValue |
This method simply loops over the array of selector names passed in the asSelectors argument, handing each one to the GetStyleValue function and determining whether it found a value. If it did, the function short-circuits and returns. If it did not, it continues with the next selector. If no value is found, the function simply returns Empty and the FormatChartFromStyles function skips formatting the chart element, leaving it set to its default values.
The last routine that I want to mention is the SetFontInfo function. Because the Chart control uses the same programming interface for any font information on any chart element, you can encapsulate the setting of font formatting information on a chart element in one function and reuse it. Here is what the function looks like:
Sub SetFontInfo(obj, asSelectors) ' Local variables Dim vStyleVal ' Temporary style value vStyleVal = FindStyleValue(asSelectors, "font-family") If Not(IsEmpty(vStyleVal)) Then obj.Font.Name = vStyleVal vStyleVal = FindStyleValue(asSelectors, "font-size") If Not(IsEmpty(vStyleVal)) Then obj.Font.Size = _ Left(vStyleVal, Len(vStyleVal) - 2) vStyleVal = FindStyleValue(asSelectors, "font-weight") If Not(IsEmpty(vStyleVal)) Then obj.Font.Bold = _ (LCase(vStyleVal) = "bold") vStyleVal = FindStyleValue(asSelectors, "color") If Not(IsEmpty(vStyleVal)) Then obj.Font.Color = vStyleVal vStyleVal = FindStyleValue(asSelectors, "number-format") If Not(IsEmpty(vStyleVal)) Then obj.NumberFormat = vStyleVal End Sub 'SetFontInfo() |
This code is just like the code in FormatChartFromStyles, but it operates on a generic object that has a Font property that exposes the Name, Size, Bold, and Color properties. It also applies the NumberFormat information if found.
You can reuse this code in your own solutions and modify it to include any additional functionality you need. To reuse this code in your solution, simply perform the following steps:
<!--Common routines for formatting the Office Web Components based on styles --> <script language=vbscript src="../Scripts/FormatControls.scp"> </script> |
FormatChartFromStyles MyChartSpace |
In the above fragment, MyChartSpace is the name of your Chart control.