SalesforceLabs / LightningWebChartJS

Simple yet flexible charting Lightning Web Component using Chart.js for admins & developers
https://sfdc.co/lwcc
MIT License
144 stars 61 forks source link

Options configuration #27

Closed adburke closed 3 years ago

adburke commented 3 years ago

Hey guys,

I didn't see any documentation on using the option object from the standard chart.js library. It looks like Chart.drawChart() (chart.js) uses "options: this._configService.getConfig()" on line 386. ConfigService being a refrence to ChartConfigService. I see ChartConfigService has the necessary object setup in the constructor so what is the intended way of setting this. My use case would be to show percentages on the y-axis. Sample below of chart.js setup.

Full example: https://jsfiddle.net/r71no58a/4/

options: { scales: { yAxes: [{ ticks: { min: 0, max: 25, callback: function(value){return value+ "%"} },
scaleLabel: { display: true, labelString: "Percentage" } }] } }

adburke commented 3 years ago

I did some further digging and noticed that all of this comes in from the c-cartesian-linear-axis data. It looks like only thing that might be missing is the tick styling for the callback function.

https://www.chartjs.org/docs/latest/axes/styling.html

adburke commented 3 years ago

I found my answer but it could use some documentation and possibly a slight update to this method. BaseAxis already has the ticksCallback() created. Update to get this working is below.

Modification to baseAxis.js

set ticksCallback(v) { this._content.ticks = this._content.ticks || {}; this._content.ticks.callback = new Function("value", v); }

Example of html <c-cartesian-linear-axis axis="y" ticks-stepsize="1" position="left" ticks-suggestedmin="0" ticks-callback="return value+ '%'" title-display="false" title-labelstring="Linear axis"></c-cartesian-linear-axis>

victorgz commented 3 years ago

Hi @adburke ,

Yes, the idea behind LWCC is that it is a wrapper on top of Chart.js, so you work with component composition.

For the ticksCallback, you can simple use data binding: the same way you do it for other properties, you can also bind functions to the component template: https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.js_props_getter

Also, the good points of using data binding versus the solution you provided is that the code is going to be always more manageable and easier to control from a JS module than directly inside your markup, you can have potential problems when binding this for the execution context... And the last point is that with your solution, you're totally skipping the two other parameters of the ticksCallback function: index and values.

I hope this is clear for you now, and thanks for using the library !

adburke commented 3 years ago

Makes sense. Thanks for the feedback.

adburke commented 3 years ago

Victor,

One other quick question. If I was to add a chartjs plugin or add an inline plugin function, is there a path to do that already in the library? Any guidance here would be appreciated.

victorgz commented 3 years ago

@adburke that's a good question! There are some platform restrictions so unfortunately the plugins are not supported yet... It is actually our next goal and we hope to be able to include them soon in the future. Stay tunned ! 😄

anicedrop commented 3 years ago

Hey @victorgz ,

Thanks for working on such an awesome component. I've decided to integrate LWCC directly in my component code. The chart code examples were really helpful and it's looking good binding simple properties, but I'm currently out of my depths with how to use the callbacks.

Would it be possible to show the JS data binding example using one of the tooltip callbacks, like title or label?

victorgz commented 3 years ago

Hey @anicedrop , here we go with a tooltip title callback example: image

First, in your component you need to bind your function the same way you bind any other variable: <c-cartesian-category-axis axis="x" ticks-callback={handleTicksCallback}></c-cartesian-category-axis>

And then in the JS side, you need to define a function passing it the arguments of the callback. For the ticksCallback, the argument is an array of Tooltip Items (see the Tooltip Item Interface section to find out the available properties for each item object). Just make any transformation you want and then return the value:

handleCallbackTitle(tooltipItem){
    return `Custom title for ${tooltipItem[0].label} is here!`;
  }

Note that some other callbacks might accept more than one argument. Like the ticksCallback for the axes that accepts three: value, index and values. The way of working with that is exactly the same:

handleTicksCallback(value, index, values){
    return `${index} : ${value}`;
  }

Hope this helps, and thanks for using the library ! 🚀

anicedrop commented 3 years ago

Thanks @victorgz . The tooltip callbackTitle() is working well on bar and line charts where the return item is listed in the Tooltip Item Interface. The difficulty I'm facing is with the bubble chart: bubbleExampleSmall

Here are the values available from the Tooltip Item Interface for the example above: { datasetIndex: 0, index: 0, label: "3", value: "7", x: 260.67265625, xLabel: 3, y: 34.400000000000006, yLabel: 7 }

I was actually hoping to return the data label "John" and also the radius value "10" in the title.

How do I accomplish this?

victorgz commented 3 years ago

@anicedrop actually Chart.js is not exposing that information, so we don't have access to it. It would be nice to have the value of the third dimension but for now it is still not available.

But the good news is that you can still accomplish that by using the datasetIndex and index properties of the Tooltip Items to access directly the information of the dataset (actually it is how Chart.js says to access that information with var label = data.datasets[tooltipItem.datasetIndex].label || '';). In your case it could be done like:

1) Extract your dataset values to a JS variable so you can easily access that info later:

dataItems = [
    {label: 'John', backgroundcolor: 'rgba(82, 183, 216, 1)', detail: [{"x": 3,"y": 7,"r": 10}, {"x": 5,"y": 4,"r": 5}, {"x": 3,"y": 4,"r": 5}]},
    {label: 'Paul', backgroundcolor: 'rgba(225, 96, 50, 1)', detail: [{"x": 2,"y": 2,"r": 2}, {"x": 6,"y": 5,"r": 10}, {"x": 4,"y": 2,"r": 5}]},
    {label: 'Ringo', backgroundcolor: 'rgba(255, 176, 59, 1)', detail: [{"x": 1,"y": 3,"r": 10}, {"x": 3,"y": 3,"r": 10}, {"x": 6,"y": 4,"r": 10}]}
  ]

2) Fill you chart data by using the variable above, instead of with harcoded values:

<c-dataset>
          <template for:each={dataItems} for:item="item">
            <c-data key={item.label}
              label={item.label}
              detail={item.detail}
              backgroundcolor={item.backgroundcolor}
            ></c-data>
          </template> 
 </c-dataset>

3) Inside your callback function, use the properties datasetIndex and index to access the correct value of the dataItem specific detail. So you can get it with:

const value = this.dataItems[tooltipItem[0].datasetIndex].detail[tooltipItem[0].index].r;

4) At this point, you won't be able to access the dataItems value. This is because the scope of thiswill change when the callback is called, so dataItems will be undefined. To fix this you can simply change the definition of the callback function from a function declaration to a function expression using arrow functions. By using the arrow functions, the callback will keep the thisvalue to the component context. More info here:

handleCallbackTitle = (tooltipItem) => {
    const value = this.dataItems[tooltipItem[0].datasetIndex].detail[tooltipItem[0].index].r;
    return `Custom title is here with value: ${value}`;
  }

So with these simple steps, the result will be: image

I hope this helps !

anicedrop commented 3 years ago

Excellent explanation, @victorgz ! I finally feel like I have a good understanding of the callbacks and the data structure. I was able to successfully create a Lightning web component with a bubble chart that displays live Salesforce data from a dev org.

Feel free to take a look here - https://padron-developer-edition.na150.force.com/portfolio/s/

Super helpful. Thanks again!

dimikolovopoulos commented 3 years ago

Hi - I am attempting to render a doughnut chart and have managed to prepare the data in the following way in accordance with @victorgz suggestions in the above comments:

[{"label":"TEST A","detail":2896215,"backgroundcolor":"rgb(110, 64, 170)"},
{"label":"TEST","detail":1440702,"backgroundcolor":"rgb(91, 92, 207)"},
{"label":"TEST C","detail":457358,"backgroundcolor":"rgb(63, 128, 225)"},
{"label":"TEST D","detail":49376,"backgroundcolor":"rgb(37, 168, 218)"},
{"label":"TEST F","detail":39024,"backgroundcolor":"rgb(25, 205, 188)"}]

I am attempting to render the html in the following way but nothing is being shown.

<template>
  <div class="slds-card">
  <c-chart type="doughnut" responsive="true">
  <c-dataset>
     <template for:each={dataItems} for:item="item">
         <c-data 
               key={item.label}
                label={item.label} 
               detail={item.detail}
               backgroundcolor={item.backgroundcolor}
               borderwidth="3" >
          </c-data>
     </template>
  </c-dataset>
     </c-chart>
   </div>
</template>

What am I doing wrong?

anicedrop commented 3 years ago

Hey @dimikolovopoulos, html looks good. Just to get this out of the way, if you test with static data directly in the html, does it render for you? What does your current JS look like, how are you assigning values to dataItems array? Also, in your data, no need to use quotes around label, detail, or backgroundcolor since they're not reserved.

dimikolovopoulos commented 3 years ago

Hi @anicedrop - thank you so much for reaching out. JS is below - and have confirmed, static data directly in the html does not render for me either :(

JS class below - using a renderedCallBack method to load the d3 library which I use later on to access the interpolateCool function. This allows me to dynamically assign a themed colour palette to the chart sections. Once d3 has been initialised I call the method fecthContactData() which calls the imperative apex function getProductSummaries. After the labeland detail values have been set in dataItemsI then populate each element in the array with the loadColours() function.

import { LightningElement, api, wire, track } from 'lwc';
import D3 from '@salesforce/resourceUrl/d3';
import { loadScript, loadStyle } from 'lightning/platformResourceLoader';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import getProductSummaries from '@salesforce/apex/ContactProductSummaries.getProductSummaries';

export default class ContactSalesByProductWebChart extends LightningElement {

    // Object name set in parameters for flow or holdings
    @api objName;
    @api recordId;    
    // Object api name (Contact or Account) set by SF on record page
    @api objectApiName;
    dataItems = [];
    d3Initialized = false;

    renderedCallback() {    
        if (this.d3Initialized) {            
            return;
        }
        Promise.all([
            loadScript(this, D3 + '/d3.v5.min.js'),
            loadStyle(this, D3 + '/style.css')
        ])
        .then(() => {
            this.d3Initialized = true;
            this.fecthContactData();

        })
        .catch((error) => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error loading D3',
                    message: error.message,
                    variant: 'error'
                })
            );
        });
    }

fecthContactData() {
        getProductSummaries({recordId: this.recordId , objName: 'Shareholding__c', objApiName: this.objectApiName})
            .then((result) => {         

                // Some data returned
                var dataParsed = JSON.parse(JSON.stringify(result).split('children').join('_children'));     
               dataParsed.forEach(function (arrayItem) {                    
                   contactData.push({
                        "label":arrayItem["xxName"],
                        "detail": arrayItem["ValuationTotal"],
                        "backgroundcolor": ""
                        });
                });
                this.dataItems = contactData;   
               this.loadColours();

         })
            .catch((error) => {
                this.error = error;
                this.dataItems = undefined;
            });
    }

    loadColours(){       
        /* Create color array */
        var colorScale = d3.interpolateCool;
        var colorRangeInfo = {
            colorStart: 0,
            colorEnd: 0.65,
            useEndAsStart: false,
        };
        var COLORS = this.interpolateColors(this.dataItems.length, colorScale, colorRangeInfo);
        this.contactDataColors = COLORS;
        this.dataItems.forEach((dataItem, index) => {
            const color = COLORS[index];
            this.dataItems[index]["backgroundcolor"] = color;

       });

    }

 /*
    * Automatically Generate Chart Colors with Chart.js & D3's Color Scales
    * DOCUMENTATION: https://codenebula.io/javascript/frontend/dataviz/2019/04/18/automatically-generate-chart-colors-with-chart-js-d3s-color-scales/
    */
    calculatePoint(i, intervalSize, colorRangeInfo) {
        var { colorStart, colorEnd, useEndAsStart } = colorRangeInfo;
        return (useEndAsStart
          ? (colorEnd - (i * intervalSize))
          : (colorStart + (i * intervalSize)));
    }
    /* Must use an interpolated color scale, which has a range of [0, 1] */
    interpolateColors(dataLength, colorScale, colorRangeInfo) {
        var { colorStart, colorEnd } = colorRangeInfo;
        var colorRange = colorEnd - colorStart;
        var intervalSize = colorRange / dataLength;
        var i, colorPoint;
        var colorArray = [];      
        for (i = 0; i < dataLength; i++) {
          colorPoint = this.calculatePoint(i, intervalSize, colorRangeInfo);
          colorArray.push(colorScale(colorPoint));
        }      
        return colorArray;
      }
}