I've finally solved this for my purposes so here's the solution I came up with if it helps anyone:
Write a python script (mine at end of this) which essentially does this:
- identify the unique categories in the point layer field of interest
- for each category, select all matching points and establish the extent of this set
- for each extent generate a new polygon in a blank atlas coverage layer with a key attribute "CategoryName"
This gave me the atlas coverage layer with one polygon for each category of interest looking like this:

Configure the atlas and print composer as per normal - leaving only the issue of turning off and on features.
For this it's a little bit of trial and error to work out the exact set of options:
The below expression lets you get the value currently held in the CategoryName field for the current atlas feature
attribute ($atlasfeature, 'CategoryName')
Use this to create rule based styling for the point layer along the lines of
attribute ($atlasfeature, 'CategoryName') = PointCategory AND PointCategory = "RedDots"
I also had a rule to guarantee all others became transparent
attribute ($atlasfeature, 'CategoryName') IS NOT PointCategory

Testing this with the atlas works really well. Finally just use the same approach to manipulate labels shown, make labels dynamic and filter tables appropriately. Ticking the 'filter legend by map content' is also very effective if you don't want all legend items on all maps.
Final atlas set:

Edit - as it was asked for, here's my script:
from PyQt4.QtCore import *
#main script----------------------------------------------
#set up the layer references - you will need to change this
targetlayer=QgsMapLayerRegistry.instance().mapLayer("AtlasExtents20150727154732521")
eylayer = QgsMapLayerRegistry.instance().mapLayer("Early_Years_Providers20150727152919862")
#establish the unique categories
names = getUniqueAttributes(eylayer, 'Mapping_La')
#get a set of boxes
boxset = getBoundings(eylayer, names)
#ensure layer is emptied, then add bounding boxes
deleteBoxes(targetlayer)
createBoxes(targetlayer, boxset)
#end main script----------------------------------------------
#------functions-------#
#gets unique set of attributes - returns a set()
def getUniqueAttributes(layer, fieldname):
values = set()
for feature in layer.getFeatures():
values.add(feature[fieldname])
return values
#quickly selects all points on a layer, given a query
def selectionQuick(layer, queryitem):
layer.removeSelection ()
#hardcoded field name
expr = QgsExpression( "\"Mapping_La\" = '" + queryitem +"'")
it = layer.getFeatures( QgsFeatureRequest( expr ) )
ids = [i.id() for i in it]
layer.setSelectedFeatures( ids )
#for a set of unique items, get bounding boxes
def getBoundings(layer, itemset):
bboxes = {}
for itemname in itemset:
selectionQuick(layer,itemname)
box = layer.boundingBoxOfSelected()
bboxes[itemname] = box
return bboxes
#for a layer create a bunch of boxes
def createBoxes(layer, boxes):
id=0
for boxkey in boxes:
id = id +1
box=boxes[boxkey]
feat = QgsFeature(layer.pendingFields())
geom = QgsGeometry.fromRect(box)
feat.setAttribute('id', id)
#hardcoded field name
feat.setAttribute('CareType', boxkey)
feat.setGeometry(geom)
(res, outFeats) = layer.dataProvider().addFeatures([feat])
def deleteBoxes(layer):
ids = [f.id() for f in layer.getFeatures()]
layer.dataProvider().deleteFeatures( ids )