Repository: highcharts-for-python/highcharts-core (language: python)

Issue:
Blank charts after 1.4.0 update when PieSeries is included
After upgrading `highcharts-core` from v1.3.7 to v1.4.0, my charts now load as blank. After some further investigation, I can get the charts to load properly when I disable the pie charts (I have 2 bar charts, 2 pie charts, and a map chart).

Were there some non-backward-compatible changes made to pie charts in v1.4.0? (Still broken on v1.5.0)

Here's the relevant DOM output with only one pie chart enabled:
```
<div id="contracts_category_breakdown" class="dashboard-chart"></div>

<script>
        Highcharts.setOptions({
  colors: ['#66BA9D',
'#FF8D9A',
'#8980FF',
'#FFBC74',
'#5F5E5E',
'#7BB3FF'],
  credits: {
  enabled: false
},
  exporting: {
  buttons: {
  'contextButton': {
  x: 5,
  y: -10
}
}
},
  lang: {
  thousandsSep: ','
},
  navigation: {
  menuItemStyle: {'color': '#5f5e5e', 'fontFamily': 'Roboto', 'fontSize': '12px'}
},
  plotOptions: {
  bar: {
  pointPadding: 0,
  type: 'bar'
},
  column: {
  colorByPoint: true,
  pointPadding: 0,
  type: 'column'
},
  pie: {
  dataLabels: {
  enabled: false
},
  showInLegend: true,
  type: 'pie'
},
  series: {
  dataSorting: {
  enabled: true
},
  type: 'series'
}
},
  title: {
  align: 'left',
  style: {'color': '#000000', 'fontFamily': 'Roboto', 'fontSize': '16px', 'fontWeight': '500'},
  text: 'Chart title'
},
  xAxis: {
  lineWidth: 0,
  labels: {
  style: {'color': '#5f5e5e', 'fontFamily': 'Roboto', 'fontSize': '12px'}
}
},
  yAxis: {
  title: {
  text: null
}
}
});
        document.addEventListener('DOMContentLoaded', function() {
Highcharts.chart('contracts_category_breakdown',
{
  exporting: {
  buttons: {
  'contextButton': {
  menuItems: ['viewFullscreen',
'printChart']
}
}
},
  legend: {
  align: 'right',
  enabled: true,
  itemStyle: {'color': '#5f5e5e', 'fontFamily': 'Roboto', 'fontSize': '12px'},
  labelFormat: '{name} {percentage:.1f}%',
  layout: 'vertical',
  verticalAlign: 'bottom'
},
  plotOptions: {
  pie: {
  allowPointSelect: true,
  cursor: 'pointer',
  dataLabels: {
  enabled: false
},
  showInLegend: true,
  type: 'pie'
},
  series: {
  dataSorting: {
  enabled: false
},
  type: 'series'
}
},
  series: [{
  data: [['Electricity',
154],
['Natural Gas',
101]],
  type: 'pie'
}],
  title: {
  text: 'Contracts Category Breakdown'
},
  tooltip: {
  format: {point.name}: {point.y},
  style: {'color': '#5F5E5E', 'fontFamily': 'Roboto', 'fontSize': '12px', 'fontWeight': '700'}
}
},
);
});
</script>
```


Discussion / hints:
Thanks, @ndrwkim ! I don't recall there being any changes to `PieSeries` introduced, let alone ones that might affect this, but I may be misremembering. Let me take a closer / deeper look, and thanks for sharing the DOM output: That will definitely help me recreate the issue and track down what may be preventing the chart from rendering correctly. I'll hopefully have more info and a resolution on this issue today.
Okay, good news - I've been able to isolate the issue that is causing the chart not to render. It is caused by a bug in the serialization of the `tooltip.format` option, though why this would be serializing strangely (and more specifically, serializing strangely if it's in use within a pie series) I do not yet know. I'm going to track down what the root cause of this bug is, but I can definitely confirm that to be the proximal issue preventing the chart from rendering. Hopefully, I'll have this fixed + patched soon.
Hi @ndrwkim : When you have a moment, can you share:
1. The code snippet or JS template that you're using to configure your `tooltip` (or specifically your `tooltip.format`) option, and
2. The method call (presumably `.to_js_literal()`) that you are using to serialize your chart to JS?

I'm having difficulty regenerating the DOM output that you provided with the incorrectly-serialized `tooltip.format` so seeing how you exactly you are setting that option will help me to create a test case for it and track down the exact bug.
I sent an email with the details you've requested.

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 138570d..761b3d1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,4 +1,13 @@
 
+Release 1.5.1
+=========================================
+
+* **BUGFIX:** Fixed bug in JS literal serialization that would misinterpret strings that 
+  start with ``{``, end with ``}``, and contain a colon (``:``) as an object literal rather
+  than as a string. (#130)
+
+--------------------
+
 Release 1.5.0
 =========================================
 
diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py
index 618528c..1b37c69 100644
--- a/highcharts_core/__version__.py
+++ b/highcharts_core/__version__.py
@@ -1,1 +1,1 @@
-__version__ = '1.5.0'
\ No newline at end of file
+__version__ = '1.5.1'
\ No newline at end of file
diff --git a/highcharts_core/js_literal_functions.py b/highcharts_core/js_literal_functions.py
index 628592e..0ff27d4 100644
--- a/highcharts_core/js_literal_functions.py
+++ b/highcharts_core/js_literal_functions.py
@@ -159,15 +159,25 @@ def is_js_object(as_str, careful_validation = False):
         is_empty = as_str[1:-1].strip() == ''
         if is_empty:
             return True
-        has_colon = ':' in as_str
-        if has_colon:
-            return True
-        if 'new ' in as_str:
+        colon_count = as_str.count(':')
+        open_brace_count = as_str.count('{')
+        close_brace_count = as_str.count('}')
+        brace_set_count = (open_brace_count + close_brace_count) / 2
+        if colon_count > 0:
+            if brace_set_count == 1:
+                return True
+            elif brace_set_count > 1 and colon_count >= brace_set_count:
+                return True
+            else:
+                careful_validation = True
+        elif 'new ' in as_str:
             return True
-        if 'Object.create(' in as_str:
+        elif 'Object.create(' in as_str:
             return True
-        return False
-    else:
+        else:
+            return False
+    
+    if careful_validation:
         expression_item = f'const testName = {as_str}'
         try:
             parsed = esprima.parseScript(expression_item)
@@ -192,6 +202,8 @@ def is_js_object(as_str, careful_validation = False):
             return True
 
         return False
+    
+    return False
 
 
 def attempt_variable_declaration(as_str):


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 3dfdc26..8c87f01 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -796,6 +796,7 @@ def Class_from_js_literal(cls, input_files, filename, as_file, error):
         assert isinstance(result, cls) is True
 
         as_js_literal = result.to_js_literal()
+        print(as_js_literal)
         #print('-----------------')
         #print('RESULT VALIDATION')
         if 'pattern:' in as_js_literal:
diff --git a/tests/options/test_tooltips.py b/tests/options/test_tooltips.py
index f983258..95a3581 100644
--- a/tests/options/test_tooltips.py
+++ b/tests/options/test_tooltips.py
@@ -9,7 +9,7 @@
 from highcharts_core import errors
 from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \
     Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \
-    Class_from_js_literal
+    Class_from_js_literal, compare_js_literals
 
 STANDARD_PARAMS = [
     ({}, None),
@@ -86,6 +86,9 @@
         }""",
         'split': True
      }, None),
+    ({
+        'format': '{point.name} {point.y}'
+      }, None),
 
     ({
       'border_width': 'not-a-number'
@@ -128,3 +131,17 @@ def test_to_dict(kwargs, error):
 ])
 def test_from_js_literal(input_files, filename, as_file, error):
     Class_from_js_literal(cls, input_files, filename, as_file, error)
+
+
+def test_bug130_tooltip_serialization():
+    as_js_literal = """{
+        format: '{point.name}: {point.y}'
+    }"""
+    
+    obj = cls.from_js_literal(as_js_literal)
+    assert obj is not None
+    assert isinstance(obj, cls) is True
+    assert obj.format == '{point.name}: {point.y}'
+    
+    result = obj.to_js_literal()
+    assert "'{point.name}: {point.y}'" in result or '"{point.name}: {point.y}"' in result
\ No newline at end of file
diff --git a/tests/test_js_literal_functions.py b/tests/test_js_literal_functions.py
index 00b52c7..7d39ca9 100644
--- a/tests/test_js_literal_functions.py
+++ b/tests/test_js_literal_functions.py
@@ -190,3 +190,21 @@ def test_convert_js_property_to_python(original_str, override, expected, error):
     else:
         with pytest.raises(error):
             result = js.convert_js_property_to_python(item, original_str)
+
+
+@pytest.mark.parametrize('original_str, expected, error', [
+    ("Object.create({item1:true})", True, None),
+    ("new Date()", True, None),
+    ("test string", False, None),
+    ("{item1: 456}", True, None),
+    ("'{point.name}: {point.y}'", False, None),
+    ("{item: {subitem: 123}}", True, None),
+    ("{item: 123, item2: 456, item3: {subitem: 789}}", True, None),
+])
+def test_is_js_object(original_str, expected, error):
+    if not error:
+        result = js.is_js_object(original_str)
+        assert result is expected
+    else:
+        with pytest.raises(error):
+            result = js.is_js_object(original_str)
\ No newline at end of file