dc.js技术咨询:可缩放Sunburst、非交互图表及多级MenuSelect问题
Hey there! Let's tackle your dc.js questions one by one—happy to help a fellow newbie get up to speed with this flexible library.
Absolutely! dc.js's sunburst chart is built on top of D3.js, so we can extend it with D3's zoom behavior to get that interactive, zoomable effect. Here's a complete example:
First, set up your basic chart structure, then add the zoom logic in a renderlet (which runs after the chart renders):
<div id="sunburst-chart"></div> <script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/dc/4.2.0/dc.min.js"></script>
// Sample hierarchical data (adjust to your structure) const data = { name: 'Root', children: [ { name: 'Bâle', children: [ { name: 'A1', value: 10 }, { name: 'A2', value: 20 } ] }, { name: 'Zurich', children: [ { name: 'B1', value: 15 }, { name: 'B2', value: 25 } ] } ] }; // Convert data to crossfilter-compatible format (flattened) function flattenData(node, parent = '') { let flat = []; const path = parent ? `${parent}.${node.name}` : node.name; if (node.children) { node.children.forEach(child => { flat = flat.concat(flattenData(child, path)); }); } else { flat.push({ path: path, value: node.value }); } return flat; } const flatData = flattenData(data); const cf = crossfilter(flatData); const pathDimension = cf.dimension(d => d.path.split('.')); const pathGroup = pathDimension.group().reduceSum(d => d.value); // Create sunburst chart const sunburstChart = dc.sunburstChart('#sunburst-chart') .width(600) .height(600) .dimension(pathDimension) .group(pathGroup) .valueAccessor(d => d.value); // Add D3 zoom behavior sunburstChart.on('renderlet', function(chart) { const svg = chart.select('svg'); const g = svg.select('g'); // Define zoom function function zoom(event) { g.attr('transform', event.transform); } // Apply zoom to SVG svg.call(d3.zoom() .scaleExtent([1, 8]) .on('zoom', zoom)); }); // Render the chart dc.renderAll();
The key here is using D3's zoom() behavior in the renderlet to enable scaling and panning of the sunburst. Adjust the scaleExtent to control how far users can zoom in/out.
To set up a chart that only reacts to other charts' filters (but doesn't send its own filter events to the rest of the dashboard), you need to override its filterHandler to avoid modifying the Crossfilter dimension when users interact with it. Here's how:
// Example with a bar chart (works for any dc chart type) const passiveChart = dc.barChart('#passive-chart') .dimension(yourDimension) .group(yourGroup) // Override filterHandler to skip applying filters to the dimension .filterHandler(function(dimension, filters) { // We return the filters so the chart updates itself, but don't apply them to the dimension // This means other charts won't get notified of changes from this chart return filters; });
If you want the chart to still highlight filtered values but not propagate the filter, this approach works perfectly. The chart will still update when other charts apply filters (since it's bound to the same dimension/group), but clicks on this chart won't affect the rest of your dashboard.
If you also want to disable click interactions entirely (but keep responsiveness to filters), you can add:
passiveChart.interactive(false);
dc.menuSelect doesn't support nested optgroups out of the box, but we can manually build the dropdown structure using a renderlet to replace the default flat options. Here's a step-by-step example:
First, prepare your data and Crossfilter setup:
// Sample 2D data const data = [ { city: 'Bâle', zone: 'A1', value: 10 }, { city: 'Bâle', zone: 'A2', value: 20 }, { city: 'Zurich', zone: 'B1', value: 15 }, { city: 'Zurich', zone: 'B2', value: 25 }, { city: 'Geneva', zone: 'C1', value: 30 } ]; const cf = crossfilter(data); // Create a dimension that uses both city and zone as a key array const cityZoneDimension = cf.dimension(d => [d.city, d.zone]); const cityZoneGroup = cityZoneDimension.group().reduceSum(d => d.value); // Group the data by city for nested dropdown const groupedOptions = {}; cityZoneGroup.all().forEach(d => { const [city, zone] = d.key; if (!groupedOptions[city]) { groupedOptions[city] = []; } groupedOptions[city].push({ zone: zone, count: d.value }); });
Now build the menu select and customize its options:
const nestedMenu = dc.menuSelect('#nested-menu') .dimension(cityZoneDimension) .group(cityZoneGroup) .on('renderlet', function(chart) { const select = chart.select('select'); // Clear default flat options select.html(''); // Add "All" option select.append('option') .attr('value', '') .text('All'); // Add optgroups for each city, with child options for zones Object.entries(groupedOptions).forEach(([city, zones]) => { const optgroup = select.append('optgroup') .attr('label', city); zones.forEach(zone => { optgroup.append('option') .attr('value', `${city}, ${zone.zone}`) // Match the flat key format .text(`${zone.zone} (${zone.count})`); }); }); }); dc.renderAll();
This will generate a dropdown with <optgroup> elements for each city, containing <option> elements for their respective zones. When a user selects an option, it will still filter the Crossfilter dimension correctly because we're using the same key format ("Bâle, A1") as your original flat setup.
内容的提问来源于stack exchange,提问作者Brice Repond

