778 lines
21 KiB
Markdown
778 lines
21 KiB
Markdown
# Visual Spending Visualization - Developer Guide
|
|
|
|
This document provides details on how to customize and extend the Visual Spending sunburst chart. The visualization is built using Apache ECharts and displays spending data in a hierarchical, interactive format.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Project Structure](#project-structure)
|
|
2. [Chart Configuration](#chart-configuration)
|
|
3. [Label Customization](#label-customization)
|
|
4. [Hover Effects](#hover-effects)
|
|
5. [Details Box](#details-box)
|
|
6. [Color Management](#color-management)
|
|
7. [Drilling Down & Interactivity](#drilling-down--interactivity)
|
|
8. [Data Structure](#data-structure)
|
|
9. [Layout & Positioning](#layout--positioning)
|
|
10. [Advanced Configuration](#advanced-configuration)
|
|
|
|
## Project Structure
|
|
|
|
The project consists of four main files:
|
|
|
|
- **index.html**: Contains the basic HTML structure and imports libraries
|
|
- **styles.css**: Handles styling for the visualization and details box
|
|
- **app.js**: Contains the core functionality and visualization logic
|
|
- **server.js**: A simple Node.js server to serve the application
|
|
|
|
## Chart Configuration
|
|
|
|
The chart is created and configured in the `renderChart` function in `app.js`. The main configuration is defined in the `option` object:
|
|
|
|
```javascript
|
|
option = {
|
|
backgroundColor: '#fff',
|
|
grid: {
|
|
left: '10%',
|
|
containLabel: true
|
|
},
|
|
series: {
|
|
type: 'sunburst',
|
|
radius: [0, '95%'],
|
|
center: ['40%', '50%'],
|
|
nodeClick: 'rootToNode',
|
|
// ... other options
|
|
},
|
|
// ... graphic elements, etc.
|
|
};
|
|
```
|
|
|
|
### Key Configuration Properties
|
|
|
|
- `type: 'sunburst'`: Specifies the chart type
|
|
- `radius`: Controls the inner and outer radius of the chart
|
|
- `center`: Positions the chart in the container [x, y]
|
|
- `nodeClick`: Determines behavior when clicking nodes ('rootToNode', 'link', false)
|
|
- `gap`: Space between segments (default: 0)
|
|
- `sort`: How to sort data (null, 'asc', 'desc', or function)
|
|
|
|
## Label Customization
|
|
|
|
Labels are configured at multiple levels:
|
|
|
|
### Global Label Configuration
|
|
|
|
```javascript
|
|
label: {
|
|
show: true,
|
|
formatter: function(param) {
|
|
// Logic to format labels
|
|
if (param.depth === 0) {
|
|
if (param.name.length > 10) {
|
|
return param.name.replace(/(.{1,10})(?: |$)/g, "$1\n").trim();
|
|
}
|
|
return param.name;
|
|
} else {
|
|
return '';
|
|
}
|
|
},
|
|
minAngle: 5, // Only show labels in segments covering at least 5 degrees
|
|
align: 'center', // Horizontal alignment: 'left', 'center', 'right'
|
|
verticalAlign: 'middle', // Vertical alignment: 'top', 'middle', 'bottom'
|
|
position: 'inside' // Position: 'inside', 'outside'
|
|
}
|
|
```
|
|
|
|
### Per-Level Label Configuration
|
|
|
|
Each level in the sunburst can have its own label configuration:
|
|
|
|
```javascript
|
|
levels: [
|
|
{}, // Level 0 (uses defaults)
|
|
{
|
|
// Level 1 (Categories)
|
|
r0: '15%', // Inner radius
|
|
r: '35%', // Outer radius
|
|
label: {
|
|
show: true,
|
|
rotate: 'tangential', // 'radial', 'tangential', or 0-360 degrees
|
|
fontSize: 12,
|
|
lineHeight: 15,
|
|
align: 'center',
|
|
verticalAlign: 'middle',
|
|
position: 'inside'
|
|
}
|
|
},
|
|
// ... other levels
|
|
]
|
|
```
|
|
|
|
### Label Rotation Options
|
|
|
|
- `rotate: 'tangential'`: Text follows the curve of the segment
|
|
- `rotate: 'radial'`: Text radiates outward from center
|
|
- `rotate: 30`: Rotate by a specific angle (in degrees)
|
|
|
|
### Showing/Hiding Labels
|
|
|
|
You can use a function to conditionally show/hide labels based on any criteria:
|
|
|
|
```javascript
|
|
label: {
|
|
show: function(param) {
|
|
// Only show for segments larger than 5% of total
|
|
return (param.value / totalValue) > 0.05;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Hover Effects
|
|
|
|
Hover effects are defined in the `emphasis` property:
|
|
|
|
```javascript
|
|
emphasis: {
|
|
focus: 'ancestor', // 'self', 'series', 'ancestor', 'descendant'
|
|
label: {
|
|
show: function(param) {
|
|
// Show labels for specific depths on hover
|
|
return param.depth === 3 || (param.depth === 2 && !param.data.children);
|
|
},
|
|
position: function(param) {
|
|
// Position labels differently based on depth
|
|
if (param.depth === 3 || (param.depth === 2 && !param.data.children)) {
|
|
return 'outside';
|
|
}
|
|
return 'inside';
|
|
},
|
|
formatter: function(param) {
|
|
// Format hover labels
|
|
if (param.depth > 0) {
|
|
if (param.name.length > 15) {
|
|
return param.name.slice(0, 12) + '...';
|
|
}
|
|
return param.name;
|
|
}
|
|
return param.name;
|
|
},
|
|
fontSize: 11,
|
|
fontWeight: 'bold',
|
|
backgroundColor: 'rgba(255,255,255,0.85)', // Label background
|
|
borderRadius: 4, // Rounded corners
|
|
padding: [3, 5], // [vertical, horizontal] padding
|
|
align: 'left',
|
|
verticalAlign: 'middle',
|
|
distance: 10, // Distance from segment (for 'outside')
|
|
borderColor: '#fff',
|
|
borderWidth: 1,
|
|
shadowBlur: 3,
|
|
shadowColor: 'rgba(0,0,0,0.2)'
|
|
},
|
|
itemStyle: {
|
|
shadowBlur: 10, // Highlight shadow
|
|
shadowOffsetX: 0,
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
}
|
|
}
|
|
```
|
|
|
|
### Focus Types
|
|
|
|
The `focus` property controls what gets highlighted:
|
|
|
|
- `'self'`: Only highlight the hovered segment
|
|
- `'ancestor'`: Highlight the segment and its ancestors
|
|
- `'descendant'`: Highlight the segment and its descendants
|
|
- `'series'`: Highlight the entire series
|
|
|
|
### Hover Label Animation
|
|
|
|
To adjust how labels animate on hover:
|
|
|
|
- For smoother transitions, use CSS transitions on the label elements
|
|
- Adjust `distance` to control how far labels move
|
|
- Set `animation: true` and `animationDuration: 300` (milliseconds) for animated transitions
|
|
|
|
## Details Box
|
|
|
|
The details box is updated via event handlers in the `setupHoverEvents` function:
|
|
|
|
```javascript
|
|
function setupHoverEvents() {
|
|
const hoverNameElement = document.getElementById('hover-name');
|
|
const hoverAmountElement = document.getElementById('hover-amount');
|
|
const topItemsElement = document.getElementById('top-items');
|
|
|
|
myChart.on('mouseover', function(params) {
|
|
if (params.data) {
|
|
// Update details box content
|
|
hoverNameElement.textContent = params.name;
|
|
hoverAmountElement.textContent = params.value.toLocaleString() + ' RUB';
|
|
|
|
// Show top items/subcategories
|
|
topItemsElement.innerHTML = '';
|
|
if (params.data.children && params.data.children.length > 0) {
|
|
// Sort and display children
|
|
// ...
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
### Customizing the Details Box
|
|
|
|
#### HTML Structure (index.html)
|
|
|
|
```html
|
|
<div id="details-box">
|
|
<h3>Details</h3>
|
|
<div id="hover-name">Hover over a segment to see details</div>
|
|
<div id="hover-amount"></div>
|
|
<h4>Top Items:</h4>
|
|
<div id="top-items"></div>
|
|
</div>
|
|
```
|
|
|
|
#### CSS Styling (styles.css)
|
|
|
|
```css
|
|
#details-box {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 20px;
|
|
width: 280px;
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.15);
|
|
padding: 15px;
|
|
z-index: 10;
|
|
min-height: 200px;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
opacity: 0.95;
|
|
border: 1px solid #eee;
|
|
}
|
|
|
|
/* Additional styling for elements inside the box */
|
|
```
|
|
|
|
#### Modifying Top Items Display
|
|
|
|
To change how the top items are displayed:
|
|
|
|
```javascript
|
|
// Inside mouseover event handler
|
|
if (params.data.children && params.data.children.length > 0) {
|
|
// Sort children by value (or any other criteria)
|
|
const sortedChildren = [...params.data.children].sort((a, b) => b.value - a.value);
|
|
|
|
// Limit to top N (e.g., top 10)
|
|
const topChildren = sortedChildren.slice(0, 10);
|
|
|
|
// Create elements for each item
|
|
topChildren.forEach(child => {
|
|
const itemDiv = document.createElement('div');
|
|
itemDiv.className = 'top-item';
|
|
|
|
// Customize what data is displayed
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = 'top-item-name';
|
|
nameSpan.textContent = child.name;
|
|
|
|
const amountSpan = document.createElement('span');
|
|
amountSpan.className = 'top-item-amount';
|
|
|
|
// Format values as needed
|
|
const percentage = ((child.value / params.value) * 100).toFixed(1);
|
|
amountSpan.textContent = `${child.value.toLocaleString()} ₽ (${percentage}%)`;
|
|
|
|
itemDiv.appendChild(nameSpan);
|
|
itemDiv.appendChild(amountSpan);
|
|
topItemsElement.appendChild(itemDiv);
|
|
});
|
|
}
|
|
```
|
|
|
|
## Color Management
|
|
|
|
### Default Colors
|
|
|
|
The initial colors are defined in the `transformToSunburst` function:
|
|
|
|
```javascript
|
|
const colors = [
|
|
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
|
|
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72',
|
|
'#d56358', '#82b1ff', '#f19143', '#addf84', '#6f7787'
|
|
];
|
|
```
|
|
|
|
### Color Assignment on Click
|
|
|
|
When clicking on a segment, colors are reassigned to children:
|
|
|
|
```javascript
|
|
myChart.on('click', function(params) {
|
|
if (params.depth >= 0) {
|
|
const colorPalette = [
|
|
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
|
|
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#4cae72'
|
|
];
|
|
|
|
// ... find target data ...
|
|
|
|
// Recolor children
|
|
targetData.children.forEach((child, i) => {
|
|
const color = colorPalette[i % colorPalette.length];
|
|
child.itemStyle = {
|
|
color: color
|
|
};
|
|
|
|
// Handle grandchildren with color gradients
|
|
if (child.children && child.children.length > 0) {
|
|
const microColors = generateColorGradient(color, child.children.length);
|
|
child.children.forEach((micro, j) => {
|
|
micro.itemStyle = {
|
|
color: microColors[j]
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
// Update data
|
|
// ...
|
|
}
|
|
});
|
|
```
|
|
|
|
### Color Gradient Generation
|
|
|
|
The `generateColorGradient` function creates variations of a base color:
|
|
|
|
```javascript
|
|
function generateColorGradient(baseColor, steps) {
|
|
const result = [];
|
|
const base = tinycolor(baseColor);
|
|
|
|
for (let i = 0; i < steps; i++) {
|
|
let color;
|
|
if (i % 3 === 0) {
|
|
color = base.clone().lighten(15); // Lighter version
|
|
} else if (i % 3 === 1) {
|
|
color = base.clone().darken(10); // Darker version
|
|
} else {
|
|
color = base.clone().saturate(20); // More saturated
|
|
}
|
|
result.push(color.toString());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
```
|
|
|
|
### Custom Color Schemes
|
|
|
|
To use a different color scheme:
|
|
|
|
1. Replace the color arrays with your preferred colors
|
|
2. For thematic colors, consider using ColorBrewer schemes or other predefined palettes
|
|
3. Use TinyColor functions to generate related colors:
|
|
- `lighten()` / `darken()`
|
|
- `saturate()` / `desaturate()`
|
|
- `spin()` (shift hue)
|
|
- `complement()` (get complementary color)
|
|
|
|
## Drilling Down & Interactivity
|
|
|
|
### Click Behavior
|
|
|
|
The click behavior is controlled by the `nodeClick` option and event handlers:
|
|
|
|
```javascript
|
|
// In the chart options
|
|
nodeClick: 'rootToNode', // 'rootToNode', 'link', or false
|
|
|
|
// Event handler
|
|
myChart.on('click', function(params) {
|
|
// Handle the click event
|
|
});
|
|
```
|
|
|
|
Options for `nodeClick`:
|
|
- `'rootToNode'`: Drill down to show the clicked node as root
|
|
- `'link'`: Use the link option of nodes (if defined)
|
|
- `false`: Disable click interaction
|
|
|
|
### Custom Drill Down
|
|
|
|
For full control over drill down behavior, set `nodeClick: false` and handle clicks manually:
|
|
|
|
```javascript
|
|
myChart.on('click', function(params) {
|
|
// Update the chart based on what was clicked
|
|
// You can use a data structure to track the current view state
|
|
// And update the displayed data accordingly
|
|
});
|
|
```
|
|
|
|
### Adding Click-to-Reset
|
|
|
|
To add a "click center to reset" feature:
|
|
|
|
```javascript
|
|
// Add a click event listener to the center area
|
|
myChart.getZr().on('click', function(event) {
|
|
// Get clicked position
|
|
const x = event.offsetX;
|
|
const y = event.offsetY;
|
|
|
|
// Check if click is in the center area
|
|
const centerX = myChart.getWidth() * 0.4; // Match your 'center' setting
|
|
const centerY = myChart.getHeight() * 0.5;
|
|
const innerRadius = myChart.getWidth() * 0.15; // Match your inner radius
|
|
|
|
const distance = Math.sqrt(
|
|
Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)
|
|
);
|
|
|
|
if (distance < innerRadius) {
|
|
// Reset the chart to its initial state
|
|
resetChart();
|
|
}
|
|
});
|
|
|
|
function resetChart() {
|
|
// Reset the chart data to initial state
|
|
option.series.data = JSON.parse(JSON.stringify(originalData));
|
|
myChart.setOption(option, { replaceMerge: ['series'] });
|
|
}
|
|
```
|
|
|
|
## Data Structure
|
|
|
|
### CSV Data Format
|
|
|
|
The app reads data from CSV files with the following structure:
|
|
|
|
```
|
|
transaction_date,processing_date,transaction_description,comment,mcc,card_info,account,merchant_name,location,info_source,amount_original,currency_original,amount_rub,category,subcategory,microcategory,simple_name
|
|
```
|
|
|
|
Key fields used in the visualization:
|
|
- `category`: First level of the hierarchy
|
|
- `subcategory`: Second level of the hierarchy
|
|
- `microcategory`: Third level of the hierarchy
|
|
- `amount_rub`: The amount spent in Russian rubles
|
|
|
|
### Transformed Data Structure
|
|
|
|
The data is transformed into a hierarchical structure by `transformToSunburst`:
|
|
|
|
```javascript
|
|
// Sample output
|
|
{
|
|
total: 426571.09, // Total spending
|
|
data: [
|
|
{
|
|
name: "Food",
|
|
value: 120000,
|
|
children: [
|
|
{
|
|
name: "Groceries",
|
|
value: 80000,
|
|
children: [
|
|
{ name: "Fruits", value: 20000 },
|
|
{ name: "Vegetables", value: 15000 },
|
|
// ...
|
|
]
|
|
},
|
|
// More subcategories...
|
|
]
|
|
},
|
|
// More categories...
|
|
]
|
|
}
|
|
```
|
|
|
|
### Adding Custom Data
|
|
|
|
To add metadata or custom properties:
|
|
|
|
```javascript
|
|
// In the transformation function
|
|
const categoryNode = {
|
|
name: categoryObj.name,
|
|
value: categoryObj.value,
|
|
children: [],
|
|
// Custom properties
|
|
metadata: {
|
|
avgTransaction: categoryObj.value / transactionCount,
|
|
lastTransaction: lastTransactionDate,
|
|
// ...any other custom data
|
|
}
|
|
};
|
|
```
|
|
|
|
## Layout & Positioning
|
|
|
|
### Chart Size and Position
|
|
|
|
```javascript
|
|
series: {
|
|
type: 'sunburst',
|
|
radius: [0, '95%'], // [innerRadius, outerRadius]
|
|
center: ['40%', '50%'], // [x, y] as percentages
|
|
}
|
|
```
|
|
|
|
### Ring Size Adjustment
|
|
|
|
The rings are sized using the `levels` configuration:
|
|
|
|
```javascript
|
|
levels: [
|
|
{}, // Level 0 (uses defaults)
|
|
{
|
|
r0: '15%', // Inner radius of this level
|
|
r: '35%', // Outer radius of this level
|
|
// ...
|
|
},
|
|
{
|
|
r0: '35%', // Should match the outer radius of previous level
|
|
r: '65%',
|
|
// ...
|
|
},
|
|
{
|
|
r0: '65%',
|
|
r: '72%', // Just 7% width for the outermost ring
|
|
// ...
|
|
}
|
|
]
|
|
```
|
|
|
|
### Central Text Positioning
|
|
|
|
The text in the center is positioned using graphic elements:
|
|
|
|
```javascript
|
|
graphic: [
|
|
{
|
|
type: 'text',
|
|
left: '40%', // Should match center[0]
|
|
top: '50%', // Vertical position
|
|
style: {
|
|
text: totalAmount.toLocaleString(),
|
|
// ... styling
|
|
},
|
|
z: 100 // Higher values appear on top
|
|
},
|
|
{
|
|
type: 'text',
|
|
left: '40%',
|
|
top: '58%',
|
|
style: {
|
|
text: 'RUB',
|
|
// ... styling
|
|
},
|
|
z: 100
|
|
}
|
|
]
|
|
```
|
|
|
|
## Advanced Configuration
|
|
|
|
### Custom Tooltips
|
|
|
|
```javascript
|
|
tooltip: {
|
|
trigger: 'item', // 'item' or 'axis'
|
|
formatter: function(info) {
|
|
// Customize tooltip content
|
|
const value = info.value.toLocaleString();
|
|
const name = info.name;
|
|
const percentage = ((info.value / totalValue) * 100).toFixed(1);
|
|
|
|
// Can return string or HTML
|
|
return `
|
|
<div style="font-weight:bold">${name}</div>
|
|
<div>Amount: ${value} RUB</div>
|
|
<div>Percentage: ${percentage}%</div>
|
|
`;
|
|
},
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
borderColor: '#ccc',
|
|
borderWidth: 1,
|
|
padding: 10,
|
|
textStyle: {
|
|
color: '#333'
|
|
}
|
|
}
|
|
```
|
|
|
|
### Animation Settings
|
|
|
|
```javascript
|
|
animation: true,
|
|
animationThreshold: 1000, // Don't animate if too many data points
|
|
animationDuration: 1000,
|
|
animationEasing: 'cubicOut', // 'linear', 'quadraticIn/Out', 'cubicIn/Out', etc.
|
|
animationDelay: 0,
|
|
```
|
|
|
|
### Custom Sorting
|
|
|
|
```javascript
|
|
// Sort segments by value (descending)
|
|
sort: function(a, b) {
|
|
return b.value - a.value;
|
|
}
|
|
|
|
// Sort alphabetically
|
|
sort: function(a, b) {
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
|
|
// Or use predefined options:
|
|
sort: 'desc' // 'desc', 'asc', or null (for data order)
|
|
```
|
|
|
|
### Adding Event Listeners
|
|
|
|
```javascript
|
|
// After setting up the chart
|
|
myChart.on('mouseover', function(params) { /* ... */ });
|
|
myChart.on('mouseout', function(params) { /* ... */ });
|
|
myChart.on('click', function(params) { /* ... */ });
|
|
|
|
// For lower-level events
|
|
const zr = myChart.getZr();
|
|
zr.on('click', function(event) { /* ... */ });
|
|
zr.on('mousemove', function(event) { /* ... */ });
|
|
```
|
|
|
|
### Responsiveness
|
|
|
|
```javascript
|
|
// Handle window resize
|
|
window.addEventListener('resize', function() {
|
|
myChart.resize();
|
|
});
|
|
|
|
// For more complex responsive behavior
|
|
window.addEventListener('resize', function() {
|
|
const width = window.innerWidth;
|
|
|
|
if (width < 768) {
|
|
// Mobile view
|
|
option.series.center = ['50%', '50%'];
|
|
option.series.radius = [0, '90%'];
|
|
// Update other settings for small screens
|
|
} else {
|
|
// Desktop view
|
|
option.series.center = ['40%', '50%'];
|
|
option.series.radius = [0, '95%'];
|
|
}
|
|
|
|
myChart.setOption(option);
|
|
myChart.resize();
|
|
});
|
|
```
|
|
|
|
### Adding Custom Legend
|
|
|
|
```javascript
|
|
// In chart options
|
|
legend: {
|
|
type: 'scroll',
|
|
orient: 'vertical',
|
|
right: 10,
|
|
top: 20,
|
|
bottom: 20,
|
|
data: categoryNames // Array of category names
|
|
}
|
|
|
|
// Or create a custom legend outside the chart
|
|
function createCustomLegend() {
|
|
const legendContainer = document.createElement('div');
|
|
legendContainer.className = 'custom-legend';
|
|
|
|
// Add legend items
|
|
sunburstData.data.forEach((category, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'legend-item';
|
|
|
|
const colorBox = document.createElement('span');
|
|
colorBox.className = 'color-box';
|
|
colorBox.style.backgroundColor = colors[index % colors.length];
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = `${category.name} (${category.value.toLocaleString()} RUB)`;
|
|
|
|
item.appendChild(colorBox);
|
|
item.appendChild(label);
|
|
legendContainer.appendChild(item);
|
|
|
|
// Add click handler to highlight corresponding segment
|
|
item.addEventListener('click', function() {
|
|
// Find and highlight the corresponding segment
|
|
});
|
|
});
|
|
|
|
document.querySelector('.content-wrapper').appendChild(legendContainer);
|
|
}
|
|
```
|
|
|
|
### Performance Optimization
|
|
|
|
For large datasets:
|
|
|
|
```javascript
|
|
// Limit data size
|
|
function simplifyData(data, threshold) {
|
|
// If there are too many small items, group them as "Other"
|
|
const result = [];
|
|
const other = { name: 'Other', value: 0, children: [] };
|
|
|
|
data.forEach(item => {
|
|
if (item.value / totalValue > threshold) {
|
|
result.push(item);
|
|
} else {
|
|
other.value += item.value;
|
|
other.children.push(item);
|
|
}
|
|
});
|
|
|
|
if (other.value > 0) {
|
|
result.push(other);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Usage
|
|
const simplifiedData = simplifyData(originalData, 0.01); // Group items < 1%
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
1. **Labels not appearing**: Check `minAngle` setting - labels only show on segments larger than the minimum angle
|
|
2. **Click not working**: Ensure `nodeClick` is set correctly and event handlers are properly configured
|
|
3. **Incorrect colors**: Check that color assignments in the transformation and click handlers are working
|
|
4. **Chart not updating**: Make sure to use `replaceMerge` option when updating data: `myChart.setOption(option, { replaceMerge: ['series'] })`
|
|
|
|
### Debugging
|
|
|
|
For debugging, you can add console logging in key functions:
|
|
|
|
```javascript
|
|
myChart.on('click', function(params) {
|
|
console.log('Click params:', params);
|
|
// Continue with normal handling
|
|
});
|
|
|
|
// Or inspect the current options
|
|
console.log('Current options:', myChart.getOption());
|
|
``` |