Viewing entries in
Web Development

Extjs: Centering Sprites in Donut (Pie) Charts

Comment

Extjs: Centering Sprites in Donut (Pie) Charts

Recently I wanted to center a sprite (image or text) inside an Extjs donut chart (Sencha Charts, Extjs 5.1+).  There's not an out-of-the-box way to do this in Extjs and I couldn't find much online, so I figured out a solution and now I'm sharing it.  I was using this to display a project logo image in the center of the donut, however the same principal applies to display any sprite to include textual data.

You'll need to do two things, which require knowing the image size, the chart's container size, and some simple math.

  1. Draw the sprite and center it inside the donut using the chart's boxready listener.
  2. Move the sprite to the donut's center using the chart's resizeHandler.

Please note you'll need to account for things like insetPadding and legend height/width depending where/if you have a legend displayed.  Please see the code snippet below for implementation details on the boxready listener and resizeHandler.


Ext.define('My.pie.chart', {
  extend: 'Ext.panel.Panel',

  initComponent: function() {
    this.items: [{
      xtype: 'polar',
      reference: 'chart',
      width: '100%',
      height: '100%',
      insetPadding: 50,
      // ... other chart config here
      legend: {
        field: 'someField',
        position: 'bottom'
      },
      series: [{
        type: 'pie',
        donut: 50,
        angleField: 'sliceSize',
        // ... other pie config here
      ]},
      resizeHandler: function(size) {
        this.scheduleLayout();

        var surface = this.getSurface();
        // we're only dealing with a single sprite here, but there are better ways to find a particular sprite on the surface if need be.
        var sprite = surface.getItems()[0];
        if (sprite) {
          // Note: when moving the sprite, we need to account for insetPadding, imageSize, and legend height.  This assumes the legend is on the top or bottom.  If the legend is on the side, use the inset padding from left/right instead
          var imageSize = 150;  // this could be a property on the sprite
          var insetPadding = this.insetPadding.top;
          var legendHeight = 0;
          if (this.getLegend()) {
            legendHeight = this.getLegend().getHeight();
          }
          sprite.setAttributes({
            // account for insetPadding when finding hte center width
            x: ((size.width/2 - insetPadding) - imageSize/2),
            // account for legend height and insetPadding when finding the center height
            y: (((size.height - legendHeight)/2 - insetPadding) - imageSize/2)
          });
        }
      },
      listeners: {
        boxready: function(me) {
          // Defer by the same amount as the chart animation (if there is any) so we know the chart is in it's final resting location
          Ext.defer(function() {
            var imageSize = 150; // this could be a property on the sprite
            var surface = this.getSurface();
            var sprite = surface.add({
            type: 'image',
            itemId; 'mySpriteImage',
            src: 'mySprite.png',
            width: imageSize,
            height: imageSize,
            // subtract half the image width from half the chart width to get the center
            x: (surface.getRect()[2]/2 - imageSize/2),
            // subtract half the image height from half the chart height to get the center
            y: (surface.getRect()[3]/2 - imageSize/2),
            surface: surface
          }, animationDelay);
        }
      }
    }];
    this.callParent(arguments);
  }
});

Comment

Extjs Stateful Web Desktop

Comment

Extjs Stateful Web Desktop

Extjs has a pretty cool Web Desktop. Though out of the box the Ext.ux.desktop.Desktop component is not stateful.  Meaning it won't remember what windows you last had open, their sizes, or their positions.  Fortunately it's relatively simple to get this working.  The following steps will work if you have an Extjs Web Desktop similar to the example linked above.  You'll also need a standard Extjs State Manager/Provider configured within your project.

Step 1: Make your Windows stateful

The Ext.ux.desktop.Desktop component uses Ext.window.Window components to display data.  Ext.window.Window components inherit the Ext.state.Stateful mixin allowing the Extjs State Manager to remember their size, location, and z-index relative to other Windows.  All you'll need to do with your Windows is add the "stateful: true" flag and assign them a stateId.

Step 2: Track which Windows are open

Making the Desktop stateful isn't as easy as simply setting the stateful and stateId properties, but it's not much harder.  As the Windows are already stateful and can track themselves, you only need to track which Windows were last open on the Desktop to determine it's last/complete state.

First you'll need to assign a stateId to the Desktop.  This will be used to get/set the Desktop's state through the Extjs State Manager.  Next you can override the Desktop's onWindowClose and createWindow methods to determine which Windows have opened and closed.  All this is doing is saving an array of strings (the Window's stateIds) to your Extjs State Manager/Provider.  When you open a new Window, add it's stateId to the Desktop's state array.  When you close a Window, remove it's stateId from the Desktop's state array.


Ext.define('MyProject.ux.desktop.Desktop', {
  extend: 'Ext.ux.desktop.Desktop',
  alias: 'widget.myproject-desktop',
  stateId: 'myproject-desktop',

  // ... other Desktop code here not relevant to this example would be here ...

  onWindowClose: function(win) {
    var state = Ext.state.Manager.get(this.stateId);
    var newState = { openWindows: Ext.Array.remove(state.openWindows, win.id) };
    Ext.state.Manager.set(this.stateId, newState);

    this.callParent(arguments);
  },

  createWindow: function(config) {
    var state = Ext.state.Manager.get(this.stateId) || {};
    var openWindows = state.openWindows || [];
    if (openWindows && !Ext.Array.contais(openWindows, config.id)) {
        var newState = { openWindows: Ext.Array.push(openWindows, win.id) };
        Ext.state.Manager.set(this.stateId, newState); 
    }

    return this.callParent(arguments);
  }
});

Step 3: Upon reload/refresh, create last open Windows (Profit!)

The last step is opening your previously opened Windows after a reload/refresh.  I handled this within the App, which houses the Desktop component.


Ext.define('MyProject.ui.App', {

  init: function() {
    // ... code not relevant to this example would be here ...
    
    this.desktop = new MyProject.ux.desktop.Desktop(desktopCfg);
    this._loadInitialModules();
  },

  _loadInitialModules: function() {
    var desktopState = Ext.state.Manager.get(this.desktop.stateId);

    var openWindows = desktopState.openWindows || [];
    Ext.Array.each(openWindows, function(windowId) {
      this.createWindow(this.getModule(windowId));
    }, this);
  }
});

That's it!  Users will love coming back to the Desktop in the state they last left it.

Comment

Extjs Dynamic Stateful Grid

Comment

Extjs Dynamic Stateful Grid

Do you have a javascript MVC framework for your existing web application?  Tired of spending development cycles updating your application views every time your data provider sends something new?  Yeah, me too.  Thankfully there are a good number javascript MVC frameworks that can be used to display dynamic data AND save the display state of said data.

Recently I wanted to build such a web application that would display dynamic data key/value pairs as well as saving the user's view state of said data.  I’ve used the Extjs framework to build various web applications and I’ve been impressed with the Ext.grid.Panel’s features as well as the ease of creating ‘stateful’ components with the Extjs framework. While there isn’t anything directly out of the box that will allow you to create a dynamic stateful grid panel, the Extjs framework does give you the tools to build your own stateful grid component without modifying any of their source code.

My requirements consisted of:

  • Dynamically display json-based messages in a table (without knowing the json nested structure or key/value pairs of the data).
  • Allow for user to customize the table (sorting, grouping, columns displayed, column order, column width).
  • Save state of the user-customized table and reload state upon user return to web application.

Blog post assumptions:

  • You are somewhat familiar with Extjs.
  • You know how to setup an Extjs State Provider.
  • You are using a ‘standard’ Extjs store (I’m using an ajax proxy with my Extjs store).

Server Side Steps

Step 1: Flatten your result set.  The Dynamic Stateful Grid described in this post worked well with a flattened array of json objects.

Step 2: Define the 'dynamic columns' in your response to the client.

An Extjs grid will allow the grid columns to be defined dynamically.  To do this, the response to the Grid’s store must include a ‘metaData’ element which defines the fields and columns based on the data being served.  The ‘metaData’ element looks like this example:

{
  metaData: {
    columns: [
      {‘header’: key1, ‘dataIndex’: key1, ‘stateId’: key1},
      {‘header’: key2, ‘dataIndex’: key2, ‘stateId’: key2}
    ],
    fields: [
      {‘name’: key1, ‘type’: ‘string’},
      {‘name’: key2, ‘type’: ‘string’}
    ]
  },
  items: [
    {‘field1’: value1,  ‘field2’: value2, ‘field3’: value3},
    {‘field4’: value4,  ‘field5’: value5, ‘field6’: value6}
  ]
}

Client Side Steps

Step 1: Define a Dynamic Stateful Grid Panel (code below works with Extjs 4, 5, and 6)

When the Extjs grid sees the ‘metaData’ element in it’s store results, the ‘metachange’ event is fired.  We will need to override both the ‘metachange’ and ‘beforestatestave’ events to keep track of the columns shown/hidden, the column order, and the column widths.

Ext.define('Ext.grid.DynamicStatefulGridPanel', {
extend: 'Ext.grid.Panel',
alias: 'widget.dynamicstatefulgrid',
stateful: true,

initComponent: function() {
  var _this = this;
  this.on({
    beforestatesave: function(cmp, state, eOpts) {
      var savedState = Ext.state.Manager.getProvider().state[_this.stateId];

      // don't save a state of 0 columns shown
      if (state && state.columns.length === 0 && savedState && savedState.columns.length === 0) {
        return false;
      }

      // if the current state has 0 columns shown, replace it with the old state and 
      // return false to prevent saving an empty state
      if (state.columns.length === 0 && savedState && savedState.columns.length > 0) {
        state = savedState;
        return false;
      }

      // if a metachange event was fired previously, reset the metachangefired flag 
      // to prevent saving the incorrect state
      if (_this.store.metachangefired) {
        _this.store.metachangefired = false;
        return false;
      }

      // if new state and old state are present, update the column hidden and width attributes
      if (state && state.columns.length > 0 && savedState && savedState.columns.length > 0) {
        $.each(savedState.columns, function(index, savedColumn) {
          $.each.(state.columns, function(stateIndex, newColumn) {
            if (savedColumn['id'] === newColumn['id']) {
              if (savedColumn['hidden'] && newColumn['hidden'] != false) {
                newColumn['hidden'] = savedColumn['hidden'];
              }
              if (!newColumn['width'] && savedColumn['width']) {
                newColumn['width'] = savedColumn['width'];
              }
            }
          });
        });
      }
      _this.metachangefired = false;
    },
    beforereconfigure: function(cmp, store, columns, oldStore, oldColumns, eOpts) {
      _this.store.metachangefired = false;
    }
  });
  this.callParent(arguments);
},

bindStore: function(store) {
  var _this = this;
  //initialize the metachangefired flag on the store
  store.metachangefired = false;
  this.callParent(arguments);
  store.mon(store, 'metachange', function(store, meta) {
      store.metachangefired = true;

      // get the columns passed in on the metachange event. 
      // Note: these columns don't have the savedState applied yet
      var metaDataColumns = meta.columns;

      // initialze array to track saved state column order
      var reorderedMetaColumns = [];

      var provider = Ext.state.Manager.getProvider();
      var state = provider.state;

      // if a state is present for this grid (_this.stateId), update the columns 
      // with the saved state from the state provider
      if (state[_this.stateId]) {
        $.each(metaDataColumns, function(index, metaDataColumn) {
          $.each(state[_this.stateId]['columns'], function(stateIndex, stateColumn) {
            if (metaDataColumn['dataIndex'] === stateColumn['id']) {
              if (stateColumn['hidden']) {
                metaDataColumn['hidden'] = stateColumn['hidden'];
              }
              if (stateColumn['width']) {
                metaDataColumn['width'] = stateColumn['width'];

              }
            }
          });
        });
      }

      if (reorderedMetaColumns.length === 0) {
        reorderedMetaColumns = metaDataColumns;
      }

      // reconfigure the grid with the saved state applied to 
      // the dynamic metaData columns
      _this.reconfigure(store, reorderedMetaColumns);
    }
  }
});

Step 2: Instantiate your Stateful Grid Panel

var _this = this;
_this.store = Ext.create(‘My.ext.RecordStore’, {});
_this.grid = Ext.create(‘Ext.grid.DynamicStatefulGridPanel’, {
	store: _this.store,
	features: [{ftype: ‘grouping’}];
	columns: [],
	stateId: ‘my-dynamic-stateful-grid’
});

Step 3: Profit!

That's it!  Once your Extjs data Store is hooked up to a data source and your results are transformed into a flattened list structure, you'll be able to display whatever key/value pairs are present in the results set as well as save the user's state of columns displayed, column widths, column order, sorting and grouping.

 

Comment