• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

docinfosci / canvasxpress-python / 5f9854e2-3ace-4db8-8620-070e901a70eb

04 Apr 2025 03:41AM UTC coverage: 80.946% (+6.1%) from 74.814%
5f9854e2-3ace-4db8-8620-070e901a70eb

push

circleci

web-flow
JS local sources; Better Jupyter; Top level configs; R/JS attribute mapping

1.  Support local / custom CanvasXpress JS and CSS sources.
2.  Support top-level configuration declarations.
3.  Support mapping R/JS configuration key names to Pythonic editions.  For example, renderTo --> render_to.
4.  Improved Jupyter Notebook detection and pre-loading of CanvasXpress JS and CSS.

93 of 117 new or added lines in 10 files covered. (79.49%)

7 existing lines in 3 files now uncovered.

1933 of 2388 relevant lines covered (80.95%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.18
/canvasxpress/canvas.py
1
import json
1✔
2
import uuid
1✔
3
from copy import deepcopy
1✔
4
from typing import Union, List, Any
1✔
5
from warnings import warn
1✔
6

7
from pandas import DataFrame
1✔
8

9
from canvasxpress.config.collection import CXConfigs
1✔
10
from canvasxpress.config.type import CXConfig
1✔
11
from canvasxpress.data.base import CXData
1✔
12
from canvasxpress.data.convert import CXHtmlConvertable
1✔
13
from canvasxpress.data.keypair import CXDictData
1✔
14
from canvasxpress.data.matrix import CXDataframeData, merge_dataframes_into_xyz_object
1✔
15
from canvasxpress.data.text import CXTextData
1✔
16
from canvasxpress.js.collection import CXEvents
1✔
17
from canvasxpress.js.function import CXEvent
1✔
18
from canvasxpress.util.template import render_from_template
1✔
19

20
# from deprecated import deprecated
21

22
_DEFAULT_JS_URL: str = "https://www.canvasxpress.org/dist/canvasXpress.min.js"
1✔
23
_DEFAULT_VERSIONED_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/canvasXpress/@cx_version@/canvasXpress.min.js"
1✔
24

25
_DEFAULT_CSS_URL: str = "https://www.canvasxpress.org/dist/canvasXpress.css"
1✔
26
_DEFAULT_VERSIONED_CSS_URL = (
1✔
27
    "https://cdnjs.cloudflare.com/ajax/libs/canvasXpress/@cx_version@/canvasXpress.css"
28
)
29

30
_CX_JS_TEMPLATE = "var cX@cx_target_id@ = new CanvasXpress(@cx_json@); @cx_functions@"
1✔
31
"""
1✔
32
The template for declaring a CanvasXpress Javascript object using data
33
from the Python edition.
34
"""
35

36
_CX_LICENSE_TEMPLATE = "<script src='@cx_license@' type='text/javascript'></script>"
1✔
37
"""
1✔
38
The template for declaring a CanvasXpress license file so that charts do 
39
not create_element with unlicensed watermarks.
40
"""
41

42
_CX_REPR_TEMPLATE = """CanvasXpress(
1✔
43
    render_to="@render_to@",
44
    data=@data@,
45
    config=@config@,
46
    width=@width@,
47
    height=@height@,
48
    events=@events@,
49
    after_render=@after_render@,
50
    other_init_params=@other_init_params@
51
)
52
"""
53

54

55
class CanvasXpress(CXHtmlConvertable):
1✔
56
    """
57
    CanvasXpress acts as a proxy to the Javascript CanvasXpress object, and in
58
    general use remains similar to its Javascript counterpart.
59

60
    Assuming a flask function that returns a rendered page using the data from
61
    a CanvasXpress object:
62

63
    ```python
64
    @app.route('/pythonexample')
65
    def get_simple_chart() -> str:
66
        chart: CanvasXpress = CanvasXpress(
67
            render_to="example_chart",
68
            data=CXDictData(
69
                {
70
                    "y": {
71
                    "vars": ["Gene1"],
72
                    "smps": ["Smp1", "Smp2", "Smp3"],
73
                    "data": [[10, 35, 88]],
74
                }
75
            ),
76
            config=CXConfigs(
77
                CXGraphType(CXGraphTypeOptions.Bar)
78
            )
79
        )
80

81
        html_parts = chart.render_to_html_parts()
82

83
        return render_template(
84
            "bar.html",
85
            canvas_element=html_parts["cx_canvas"],
86
            bar_graph=html_parts["cx_js"]
87
        )
88
    ```
89
    """
90

91
    __target_id: Union[str, None] = None
1✔
92
    """
1✔
93
    __target_id is used by the JS renderTo param.  None indicates that the object is anonymous.
94
    """
95

96
    @property
1✔
97
    def anonymous(self) -> bool:
1✔
98
        """
99
        Indicates whether the object is anonymous.  If `True`, then `render_to()` will result in a one-time random
100
        ID string being returned for use in contexts such as rapidly changing React environments.  If `False`, then
101
        the ID string returned is the one set for the object by the developer.
102
        :returns: `bool` `True` if the object is anonymous; otherwise `False`.
103
        """
104
        return self.__target_id is None
1✔
105

106
    @property
1✔
107
    def render_to(self) -> Union[str, None]:
1✔
108
        """
109
        The ID of the CanvasXpress object's associated HTML components, such as
110
        the create_element canvas element.  Sets the `id` attribute of the `<canvas>`
111
        element.
112
        :returns: `str` The ID, if configured; `None` if anonymous.
113
        """
114
        return (
1✔
115
            str(uuid.uuid4()).replace("-", "") if self.anonymous else self.__target_id
116
        )
117

118
    @render_to.setter
1✔
119
    def render_to(self, value: Union[str, None]) -> None:
1✔
120
        """
121
        Sets the render_to of the CanvasXpress instance.  Sets the `id`
122
        attribute of the `<canvas>` element.
123
        :param value:
124
            `str` The ID to be associated.  Must be alphanumeric.  Non-alphanumeric characters will
125
            be removed, except for `_`, and if the remaining string is empty then a UUID4 will be
126
            substituted.  This is to preserve JS compatibility during rendering. `None` can also
127
            be provided to indicate that this object should be anonymous, such as for use
128
            in rapidly changing React interfaces.
129
        """
130
        if not isinstance(value, str) and value is not None:
1✔
131
            raise TypeError("value must be of type str or None")
1✔
132

133
        elif value is not None:
1✔
134
            candidate = ""
1✔
135
            for c in value:
1✔
136
                if c.isalnum() or c == "_":
1✔
137
                    candidate += c
1✔
138
            if candidate == "":
1✔
139
                candidate = None
×
140

141
        else:
142
            candidate = value
1✔
143

144
        self.__target_id = candidate
1✔
145

146
    __license_url: Union[str, None] = None
1✔
147
    """
1✔
148
    Location of the CanvasXpressLicense.js file for use in rendering.
149
    """
150

151
    @property
1✔
152
    def license_available(self) -> bool:
1✔
153
        """
154
        Indicates if a license is associated with the CanvasXpress object.
155
        :returns: `True` if a license file URL has been set.
156
        """
157
        return self.__license_url is not None
1✔
158

159
    @property
1✔
160
    def license_url(self) -> str:
1✔
161
        """
162
        Returns the location of the license file associated with the
163
        CanvasXpress object.
164
        :returns: `str` URL of the file or `None` if no file is associated.
165
        """
166
        return self.__license_url
1✔
167

168
    @license_url.setter
1✔
169
    def license_url(self, value: str) -> None:
1✔
170
        """
171
        Sets the location of the license file to be associated with the
172
        CanvasXpress object.
173
        :param value:
174
            `str` The path to the license file or `None` if a previously set URL
175
            is no longer valid.
176
        """
177
        if value is None:
1✔
178
            self.__license_url = None
1✔
179

180
        else:
181
            candidate = str(value)
1✔
182
            if "CanvasXpressLicense.js" not in candidate:
1✔
183
                raise ValueError(
1✔
184
                    "CanvasXpress license files must be named "
185
                    "'CanvasXpressLicense.js'"
186
                )
187

188
            else:
189
                self.__license_url = candidate
1✔
190

191
    __cdn_edition: Union[str, None] = None
1✔
192
    """
1✔
193
    The edition of CanvasXpress to use.  None indicates that the latest edition available shall be used.
194
    Used as a class variable.
195
    """
196

197
    @classmethod
1✔
198
    def cdn_edition(cls) -> Union[str, None]:
1✔
199
        """
200
        Indicates the version of CanvasXpress being used.
201

202
        :returns: `Union[str, None]`: The Javascript CDN version used or None if the latest.
203
        """
204
        return cls.__cdn_edition
1✔
205

206
    @classmethod
1✔
207
    def set_cdn_edition(cls, value) -> None:
1✔
208
        """
209
        Sets the version of the Javascript CDN to use when generating HTML or React components.  See thr CDN page
210
        for CanvasXpress to identify the versions available: https://cdnjs.com/libraries/canvasXpress.
211

212
        :param value:
213
            `Union[str, None]` The CDN version, such as `38.1`, or None if the latest version is preferred.
214
        """
215
        cls.__cdn_edition = None if value is None else str(value)
1✔
216

217
    __js_url: str = _DEFAULT_JS_URL
1✔
218
    """
1✔
219
    The preferred JS library URL.  By default the standard URL.
220
    """
221

222
    @classmethod
1✔
223
    def js_library_url(cls) -> Union[str, None]:
1✔
224
        """
225
        Indicates the version of CanvasXpress being used.  This can be either a version number, such as for cdnjs, or
226
        a fully qualified URL to the CanvasXpress library.
227

228
        :returns: `str`: The Javascript CDN version used.
229
        """
230
        candidate_url = cls.__js_url
1✔
231
        if cls.__cdn_edition is not None:
1✔
NEW
232
            candidate_url = _DEFAULT_VERSIONED_JS_URL.replace(
×
233
                "@cx_version@", cls.__cdn_edition
234
            )
235

236
        return candidate_url
1✔
237

238
    @classmethod
1✔
239
    def set_js_library_url(cls, value) -> None:
1✔
240
        """
241
        Sets the non-versioned URL to use for the CanvasXpress library.
242

243
        :param value:
244
            The HTTP(S) URL providing the JS library.
245
        """
246
        cls.__js_url = _DEFAULT_JS_URL if value is None else str(value)
1✔
247

248
    __css_url: str = _DEFAULT_CSS_URL
1✔
249
    """
1✔
250
    The preferred CSS library URL.  By default the standard URL.
251
    """
252

253
    @classmethod
1✔
254
    def css_library_url(cls) -> str:
1✔
255
        """
256
        Indicates the version of CanvasXpress being used.  This can be either a version number, such as for cdncss, or
257
        a fully qualified URL to the CanvasXpress library.
258

259
        :returns: `str`: The Javascript CDN version used.
260
        """
261
        candidate_url = cls.__css_url
1✔
262
        if cls.__cdn_edition is not None:
1✔
NEW
263
            candidate_url = _DEFAULT_VERSIONED_CSS_URL.replace(
×
264
                "@cx_version@", cls.__cdn_edition
265
            )
266

267
        return candidate_url
1✔
268

269
    @classmethod
1✔
270
    def set_css_library_url(cls, value) -> None:
1✔
271
        """
272
        Sets the non-versioned URL to use for the CanvasXpress library.
273

274
        :param value:
275
            The HTTP(S) URL providing the JS library.
276
        """
277
        cls.__css_url = _DEFAULT_CSS_URL if value is None else str(value)
1✔
278

279
    CHART_WIDTH_DEFAULT: int = 500
1✔
280
    """
1✔
281
    Default width of the chart when rendered, such as into HTML.
282
    """
283

284
    __chart_width: int = CHART_WIDTH_DEFAULT
1✔
285
    """
1✔
286
    Preferred width of the chart when rendered, such as into HTML.
287
    """
288

289
    @property
1✔
290
    def width(self) -> int:
1✔
291
        """
292
        Indicates the preferred <canvas> Web element width when rendered.  This
293
        property is used to facilitate integration with Web containers such
294
        as Jupyter notebooks. Added to the `<canvas>` element, and also
295
        influences create_element containers for contexts such as Jupyter Notebooks.
296
        :returns: `int` The width
297
        """
298
        return self.__chart_width
1✔
299

300
    @width.setter
1✔
301
    def width(self, value: int):
1✔
302
        """
303
        Sets the preferred Web element width when rendered. Added to the
304
        `<canvas>` element, and also influences create_element containers for contexts
305
        such as Jupyter Notebooks.
306
        :param value: `int`
307
            The pixel count.  Cannot be `None` or less than `1`.
308
        """
309
        if value is None:
1✔
310
            raise ValueError("element_width cannot be None")
1✔
311

312
        elif not isinstance(value, int):
1✔
313
            raise TypeError("element_width must be an int.")
1✔
314

315
        elif value < 1:
1✔
316
            raise ValueError("element_width cannot be less than 1 pixel")
1✔
317

318
        else:
319
            self.__chart_width = value
1✔
320

321
    CHART_HEIGHT_DEFAULT: int = 500
1✔
322
    """
1✔
323
    Default height of the chart in pixels when rendered, such as into HTML.
324
    """
325

326
    __chart_height: int = CHART_HEIGHT_DEFAULT
1✔
327
    """
1✔
328
    Preferred height of the chart in pixels when rendered, such as into HTML.
329
    """
330

331
    @property
1✔
332
    def height(self) -> int:
1✔
333
        """
334
        Indicates the preferred Web element height when rendered.  This
335
        property is used to facilitate integration with Web containers such
336
        as Jupyter notebooks.  Added to the `<canvas>` element, and also
337
        influences create_element containers for contexts such as Jupyter Notebooks.
338
        :returns: `int` The pixel count
339
        """
340
        return self.__chart_height
1✔
341

342
    @height.setter
1✔
343
    def height(self, value: int):
1✔
344
        """
345
        Sets the preferred Web element height when rendered.  Added to the
346
        `<canvas>` element, and also influences create_element containers for contexts
347
        such as Jupyter Notebooks.
348
        :param value: `int`
349
        """
350
        if value is None:
1✔
351
            raise ValueError("element_height cannot be None")
1✔
352

353
        elif not isinstance(value, int):
1✔
354
            raise TypeError("element_height must be an int.")
1✔
355

356
        elif value < 1:
1✔
357
            raise ValueError("element_height cannot be less than 1 pixel")
1✔
358

359
        else:
360
            self.__chart_height = value
1✔
361

362
    __data: CXData = None
1✔
363
    """
1✔
364
    _data is used by the JS data param, and it can be of numerous forms.  This
365
    class provides mapping mechanisms to translate Python objects into 
366
    structures usable by CanvasXpress.
367
    """
368

369
    @property
1✔
370
    def data(self) -> CXData:
1✔
371
        """
372
        Provides access to the CXData associated with this CanvasXpress chart.
373
        :returns: `CXData` The data to be associated with the chart.
374
        """
375
        return self.__data
1✔
376

377
    @data.setter
1✔
378
    def data(self, value: Union[CXData, dict, DataFrame, str, None]) -> None:
1✔
379
        """
380
        Sets the CXData associated with this CanvasXpress chart.
381
        :param value: `Union[CXData, dict, DataFrame, str, None]`
382
            An object translatable into a CXData type. If the object is an
383
            instance of CXData then it will be tracked by the CanvasXpress
384
            object; otherwise, a new CXData object will be created to manage
385
            the content.
386
        """
387
        if value is None or isinstance(value, bool):
1✔
388
            self.__data = CXDictData()
1✔
389

390
        elif isinstance(value, CXData):
1✔
391
            self.__data = value
1✔
392

393
        elif isinstance(value, dict):
1✔
394
            self.__data = CXDictData(value)
1✔
395

396
        elif isinstance(value, DataFrame):
1✔
397
            self.__data = CXDataframeData(value)
×
398

399
        elif isinstance(value, str):
1✔
400
            self.__data = CXTextData(value)
×
401

402
        else:
403
            raise TypeError(
1✔
404
                "data must be of type CXData, dict, DataFrame, str, bool, or None"
405
            )
406

407
    __sample_annotation: CXData = None
1✔
408
    """
1✔
409
    Provides convenience support when working with DataFrames similar to R.
410
    """
411

412
    @property
1✔
413
    def sample_annotation(self) -> CXData:
1✔
414
        """
415
        Provides access to the CXData associated with this CanvasXpress chart.
416
        :returns: `CXData` The data to be associated with the chart.
417
        """
418
        return self.__sample_annotation
×
419

420
    @sample_annotation.setter
1✔
421
    def sample_annotation(self, value: Union[CXDataframeData, DataFrame, None]) -> None:
1✔
422
        """
423
        Sets the CXData associated with this CanvasXpress chart.
424
        :param value: `Union[CXData, dict, DataFrame, str, None]`
425
            An object translatable into a CXData type. If the object is an
426
            instance of CXData then it will be tracked by the CanvasXpress
427
            object; otherwise, a new CXData object will be created to manage
428
            the content.
429
        """
430
        if value is not None and not isinstance(self.data, CXDataframeData):
1✔
431
            raise ValueError(
×
432
                "The data property must be a DataFrame before this property can be set."
433
            )
434

435
        elif value is None or isinstance(value, bool):
1✔
436
            self.__sample_annotation = None
1✔
437

438
        elif isinstance(value, DataFrame):
×
439
            self.__sample_annotation = CXDataframeData(value)
×
440

441
        elif isinstance(value, CXDataframeData):
×
442
            self.__sample_annotation = CXDataframeData(value)
×
443

444
        else:
445
            raise TypeError("data must be of type CXDataframeData,  DataFrame, or None")
×
446

447
    __variable_annotation: CXData = None
1✔
448
    """
1✔
449
    Provides convenience support when working with DataFrames similar to R.
450
    """
451

452
    @property
1✔
453
    def variable_annotation(self) -> CXData:
1✔
454
        """
455
        Provides access to the CXData associated with this CanvasXpress chart.
456
        :returns: `CXData` The data to be associated with the chart.
457
        """
458
        return self.__variable_annotation
×
459

460
    @variable_annotation.setter
1✔
461
    def variable_annotation(
1✔
462
        self, value: Union[CXDataframeData, DataFrame, None]
463
    ) -> None:
464
        """
465
        Sets the CXData associated with this CanvasXpress chart.
466
        :param value: `Union[CXData, dict, DataFrame, str, None]`
467
            An object translatable into a CXData type. If the object is an
468
            instance of CXData then it will be tracked by the CanvasXpress
469
            object; otherwise, a new CXData object will be created to manage
470
            the content.
471
        """
472
        if value is not None and not isinstance(self.data, CXDataframeData):
1✔
473
            raise ValueError(
×
474
                "The data property must be a DataFrame before this property can be set."
475
            )
476

477
        elif value is None or isinstance(value, bool):
1✔
478
            self.__variable_annotation = None
1✔
479

480
        elif isinstance(value, DataFrame):
×
481
            self.__variable_annotation = CXDataframeData(value)
×
482

483
        elif isinstance(value, CXDataframeData):
×
484
            self.__variable_annotation = CXDataframeData(value)
×
485

486
        else:
487
            raise TypeError("data must be of type CXDataframeData,  DataFrame, or None")
×
488

489
    __events: CXEvents = None
1✔
490
    """
1✔
491
    __events is used by the JS events param.
492
    """
493

494
    @property
1✔
495
    def events(self) -> CXEvents:
1✔
496
        """
497
        Provides access to the CXEvents associated with this CanvasXpress chart.
498
        :returns: `CXEvents` The events to be associated with the chart.
499
        """
500
        return self.__events
1✔
501

502
    @events.setter
1✔
503
    def events(self, events: Union[CXEvents, List[CXEvent], None]) -> None:
1✔
504
        """
505
        Sets the CXEvents associated with this CanvasXpress chart.
506
        :param events:
507
            `CXEvents, List[CXEvent], None` An object translatable into a
508
            CXEvents type.  If the object is an instance of CXEvents then it
509
            will be tracked by the CanvasXpress object; otherwise, a new
510
            CXEvents object will be created to manage the content.
511
        """
512
        if not events:
1✔
513
            self.__events = CXEvents()
1✔
514

515
        elif isinstance(events, CXEvents):
1✔
516
            self.__events = events
1✔
517

518
        elif isinstance(events, list):
1✔
519
            self.__events = CXEvents(*events)
1✔
520

521
        else:
522
            raise TypeError("value must be List[CXEvent] or CXEvents")
1✔
523

524
    __config: CXConfigs = None
1✔
525
    """
1✔
526
    __config is used to compose the config settings for the CanvasXpress object.
527
    """
528

529
    @property
1✔
530
    def config(self) -> CXConfigs:
1✔
531
        """
532
        Provides access to the CXConfigs associated with this CanvasXpress chart.
533
        :returns: `CXConfigs` The config to be associated with the chart.
534
        """
535
        return self.__config
1✔
536

537
    @config.setter
1✔
538
    def config(
1✔
539
        self, value: Union[List[CXConfig], List[tuple], List[list], dict, CXConfigs]
540
    ):
541
        """
542
        Sets the CXConfigs associated with this CanvasXpress chart.
543
        :param value: `Union[
544
                List[CXConfig],
545
                List[tuple],
546
                List[list],
547
                dict,
548
                CXConfigs
549
            ]`
550
            An object translatable into a CXConfigs type.  If the object is an
551
            instance of CXConfigs then it will be
552
            tracked by the CanvasXpress object; otherwise, a new CXConfigs
553
            object will be created to manage the content.
554
        """
555
        if value is None:
1✔
556
            self.__config = CXConfigs()
1✔
557

558
        elif isinstance(value, list):
1✔
559
            self.__config = CXConfigs(*value)
1✔
560

561
        elif isinstance(value, dict):
1✔
UNCOV
562
            self.__config = CXConfigs(value)
×
563

564
        elif isinstance(value, CXConfigs):
1✔
565
            self.__config = value
1✔
566

567
        else:
568
            raise TypeError(
1✔
569
                "value must be one of Union[List[CXConfig], List[tuple], "
570
                "dict, CXConfigs, CXConfigs]"
571
            )
572

573
    __after_render: CXConfigs = None
1✔
574
    """
1✔
575
    __after_render is used to compose the afterRender settings for the 
576
    CanvasXpress object.
577
    """
578

579
    @property
1✔
580
    def after_render(self) -> CXConfigs:
1✔
581
        """
582
        Provides access to the CXConfigs associated with this CanvasXpress
583
        chart's afterRender property.
584
        :returns: `CXConfigs`
585
            The after_render configuration to be associated with the chart.
586
        """
587
        return self.__after_render
1✔
588

589
    @after_render.setter
1✔
590
    def after_render(
1✔
591
        self, value: Union[List[CXConfig], List[tuple], List[list], dict, CXConfigs]
592
    ):
593
        """
594
        Sets the CXConfigs associated with this CanvasXpress chart's afterRender
595
        property.
596
        :param value: `Union[List[CXConfig], List[tuple], dict, CXConfigs,
597
            CXConfigs]`
598
            An object translatable into a CXConfigs
599
            type.  If the object is an instance of CXConfigs then it will be
600
            tracked by the CanvasXpress object; otherwise, a new CXConfigs
601
            object will be created to manage the content.
602
        """
603
        if value is None:
1✔
604
            self.__after_render = CXConfigs()
1✔
605

606
        elif isinstance(value, list):
1✔
607
            self.__after_render = CXConfigs(*value)
1✔
608

609
        elif isinstance(value, dict):
1✔
610
            self.__after_render = CXConfigs(value)
×
611

612
        elif isinstance(value, CXConfigs):
1✔
613
            self.__after_render = value
1✔
614

615
        else:
616
            raise TypeError(
1✔
617
                "value must be one of Union[List[CXConfig], List[tuple], "
618
                "dict, CXConfigs, CXConfigs]"
619
            )
620

621
    __canvas_attributes = CXConfigs()
1✔
622
    """
1✔
623
    __canvas_attributes modify the <canvas> element generated by this object.
624
    """
625

626
    @property
1✔
627
    def other_init_params(self) -> CXConfigs:
1✔
628
        """
629
        Provides access to additional parameters that will be used with the
630
        `CanvasXpress` for Javascript constructor, such as `afterRencderInit`.
631

632
        :returns: `CXConfigs`
633
            `CXConfig` objects to be provided as additional Javascript
634
            constructor parameter values.
635
        """
636
        return self.__canvas_attributes
1✔
637

638
    @other_init_params.setter
1✔
639
    def other_init_params(
1✔
640
        self, value: Union[List[CXConfig], List[tuple], List[list], dict, CXConfigs]
641
    ):
642
        """
643
        Set the additional parameters to be used with the `CanvasXpress` for
644
        Javascript constructor, such as `afterRencderInit`. The following
645
        parameters will be ignored and the properties for this class should
646
        be used instead due to Python tier functionality useful to the
647
        developer:
648

649
        * `renderTo`: Use `CanvasXpress.render_to`
650
        * `data`: Use `CanvasXpress.data`
651
        * `config`: Use `CanvasXpress.config`
652
        * `afterRender`: Use `CanvasXpress.after_render`
653
        * `width`: Use `CanvasXpress.width`
654
        * `height`: Use `CanvasXpress.height`
655

656
        :param value: `Union[List[CXConfig], List[tuple], List[list], dict,
657
            CXConfigs]`
658
            An object translatable into a CXConfigs type.  If the object is an
659
            instance of CXConfigs then it will be tracked by the CanvasXpress
660
            object; otherwise, a new CXConfigs object will be created to manage
661
            the content.
662
        """
663
        if value is None:
1✔
664
            self.__canvas_attributes = CXConfigs()
1✔
665

666
        elif isinstance(value, list):
1✔
667
            self.__canvas_attributes = CXConfigs(*value)
1✔
668

669
        elif isinstance(value, dict):
1✔
670
            self.__canvas_attributes = CXConfigs(value)
1✔
671

672
        elif isinstance(value, CXConfigs):
×
673
            self.__canvas_attributes = value
×
674

675
        else:
676
            raise TypeError(
×
677
                "value must be one of List[CXConfig], List[tuple], List[list], "
678
                "dict, CXConfigs"
679
            )
680

681
        # Clean override properties
682
        disallowed_attributes = [
1✔
683
            "id",
684
            "width",
685
            "height",
686
            "data",
687
            "config",
688
            "afterRender",
689
            "renderTo",
690
        ]
691
        for attribute in disallowed_attributes:
1✔
692
            self.other_init_params.remove(attribute)
1✔
693

694
    @classmethod
1✔
695
    def from_reproducible_json(
1✔
696
        cls,
697
        cx_json: str,
698
        include_factory: bool = False,
699
        include_system: bool = False,
700
    ) -> "CanvasXpress":
701
        """
702
        Initializes a new `CanvasXpress` object using a reproducable research
703
        JSON saved from a CanvasXpress chart rendered in a Web browser.
704

705
        :param cx_json: `str`
706
            A valid reproducable research JSON typical of those created by
707
            CanvasXpress when running a Web browser.
708

709
        :param include_factory: `bool`
710
            Default `False`.  If `False` remove the `factory` attribute.
711

712
        :param include_system: `bool`
713
            Default `False`.  If `False` remove the `system` attribute.
714

715
        :returns: `CanvasXpress`
716
            Returns a new `CanvasXpress` object will all properties filled using
717
            the information provided by the reproducable research JSON.
718
        """
719
        try:
1✔
720
            cx_json_dict = json.loads(cx_json)
1✔
721

722
            if not include_factory:
1✔
723
                if cx_json_dict.get("factory"):
1✔
724
                    del cx_json_dict["factory"]
1✔
725

726
            if not include_system:
1✔
727
                if cx_json_dict.get("system"):
1✔
728
                    del cx_json_dict["system"]
1✔
729

730
            cx_render_to = cx_json_dict.get("renderTo")
1✔
731
            cx_data = cx_json_dict.get("data")
1✔
732
            cx_config = cx_json_dict.get("config")
1✔
733
            cx_after_render = cx_json_dict.get("afterRender")
1✔
734
            cx_width = cx_json_dict.get("width", 500)
1✔
735
            cx_height = cx_json_dict.get("height", 500)
1✔
736
            cx_other_init_params = [
1✔
737
                (str(param), cx_json_dict[param])
738
                for param in cx_json_dict.keys()
739
                if param
740
                not in [
741
                    "id",
742
                    "width",
743
                    "height",
744
                    "data",
745
                    "config",
746
                    "afterRender",
747
                    "renderTo",
748
                ]
749
            ]
750

751
            # Capure and remove setDimensions width, height
752
            setDimensions_indexes = list()
1✔
753
            for instruction_index, instruction in enumerate(cx_after_render):
1✔
754
                if isinstance(instruction, list):
1✔
755
                    if len(instruction) >= 2:
1✔
756
                        if instruction[0] == "setDimensions":
1✔
757
                            dimensions = instruction[1]
1✔
758
                            if isinstance(dimensions, list):
1✔
759
                                if len(dimensions) >= 2:
1✔
760
                                    if isinstance(dimensions[0], int):
1✔
761
                                        cx_width = dimensions[0]
1✔
762
                                    if isinstance(dimensions[1], int):
1✔
763
                                        cx_height = dimensions[1]
1✔
764
                            setDimensions_indexes.append(instruction_index)
1✔
765

766
            for index in reversed(setDimensions_indexes):
1✔
767
                del cx_after_render[index]
1✔
768

769
            candidate = CanvasXpress(
1✔
770
                render_to=cx_render_to,
771
                data=cx_data,
772
                config=cx_config,
773
                after_render=cx_after_render,
774
                other_init_params=cx_other_init_params,
775
                width=int(cx_width),
776
                height=int(cx_height),
777
            )
778

779
            return candidate
1✔
780

781
        except Exception as e:
×
782
            raise ValueError(
×
783
                "cx_json must be a valid CanvasXpress reproducable" " research JSON"
784
            )
785

786
    def __init__(
1✔
787
        self,
788
        render_to: str = None,
789
        data: Union[CXData, dict, DataFrame, str, None] = None,
790
        sample_annotation: Union[CXDataframeData, DataFrame, None] = None,
791
        variable_annotation: Union[CXDataframeData, DataFrame, None] = None,
792
        events: Union[List[CXEvent], CXEvents] = None,
793
        config: Union[List[CXConfig], List[tuple], dict, CXConfigs] = None,
794
        after_render: Union[List[CXConfig], List[tuple], dict, CXConfigs] = None,
795
        other_init_params: Union[List[CXConfig], List[tuple], dict, CXConfigs] = None,
796
        width: int = CHART_WIDTH_DEFAULT,
797
        height: int = CHART_HEIGHT_DEFAULT,
798
        **kwargs: Any,
799
    ) -> None:
800
        """
801
        Initializes a new CanvasXpress object.  Default values are provided for
802
        all parameters if values are not specified; otherwise the arguments are
803
        treated as if an appropriate setter were used.
804
        :param render_to: See `render_to` property, except that on default
805
            initialization the object will be assigned an UUID4 value.
806
        :param data: See the `data` property
807
        :param events: See the `events` property
808
        :param config: See the `config` property
809
        :param after_render: See the `after_render` property
810
        :param other_init_params: See the 'other_init_params` property
811
        :param width: See the `width` property
812
        :param height: See the `height` property
813
        :param kwargs: `Any`
814
            Additional keyword arguments for CanvasXpress. Secondary keywords are
815
            added to the `config` property, overriding any existing items of the
816
            same name. Primary keywords `renderTo` or `afterRender` are mapped to
817
            the corresponding property (`render_to` or `after_render`).
818
        """
819

820
        super().__init__()
1✔
821

822
        config_updated = CXConfigs()
1✔
823

824
        if isinstance(config, list):
1✔
825
            config_updated = deepcopy(CXConfigs(*config))
1✔
826

827
        elif isinstance(config, dict):
1✔
828
            config_updated = deepcopy(CXConfigs(config))
1✔
829

830
        elif isinstance(config, CXConfigs):
1✔
831
            config_updated = deepcopy(config)
1✔
832

833
        elif config is not None:
1✔
NEW
834
            raise TypeError(
×
835
                "config must be one of Union[List[CXConfig], List[tuple], "
836
                "dict, CXConfigs]"
837
            )
838

839
        existing_config_labels = [item.label for item in config_updated.configs]
1✔
840

841
        for key, value in kwargs.items():
1✔
842
            if key == "renderTo":
1✔
843
                if render_to is not None:
1✔
844
                    warn("`render_to` argument has been overridden by `renderTo`")
1✔
845

846
                render_to = value
1✔
847

848
            elif key == "afterRender":
1✔
849
                if after_render is not None:
1✔
850
                    warn("`after_render` argument has been overridden by `afterRender`")
1✔
851

852
                after_render = value
1✔
853

854
            else:
855
                if key in existing_config_labels:
1✔
856
                    config_updated.remove(key)
1✔
857

858
                config_updated.set_param(label=key, value=value)
1✔
859

860
        self.render_to = render_to
1✔
861
        self.data = data
1✔
862
        self.sample_annotation = sample_annotation
1✔
863
        self.variable_annotation = variable_annotation
1✔
864
        self.events = events
1✔
865
        self.config = config_updated
1✔
866
        self.after_render = after_render
1✔
867
        self.other_init_params = other_init_params
1✔
868
        self.width = width
1✔
869
        self.height = height
1✔
870

871
    def provide_data_object(self) -> CXData:
1✔
872
        if isinstance(self.data, CXDataframeData):
1✔
873
            return CXDictData(
×
874
                merge_dataframes_into_xyz_object(
875
                    self.data,
876
                    self.sample_annotation,
877
                    self.variable_annotation,
878
                )
879
            )
880

881
        else:
882
            return self.data
1✔
883

884
    def prepare_html_element_parts(self) -> dict:
1✔
885
        """
886
        Converts the CanvasXpress object into CanvasXpress element components in
887
        anticipation of further use in renderable objects or conversion into HTML.
888

889
        If the associated `CXData` is a type of `CXProfiledData` and a profile
890
        has yet to be assigned then a profile can be assigned according to the
891
        `CXConfig` labelled `graphType`.  If a profile is assigned but is not
892
        `CXRawProfile` then the `graphType` can be reassessed, and if
893
        appropriate a new profile better aligned to the data can be provided.
894

895
        :returns: `dict` A map of values in anticipation of further conversion
896
            into html or a renderable.
897
        """
898
        cx_element_params = {
1✔
899
            "renderTo": self.render_to,
900
            "data": self.provide_data_object().render_to_dict(),
901
            "config": self.config.render_to_dict(),
902
            "afterRender": self.after_render.render_to_list(),
903
            "otherParams": self.other_init_params.render_to_dict(),
904
            "events": "js_events",
905
            "width": self.width,
906
            "height": self.height,
907
        }
908

909
        # Support unique data without JSON data structure
910
        if cx_element_params["data"].get("raw"):
1✔
911
            cx_element_params["data"] = cx_element_params["data"]["raw"]
×
912

913
        cx_element_params["events"] = self.events.render_to_js()
1✔
914

915
        return cx_element_params
1✔
916

917
    def render_to_html_parts(self, fix_missing_profile: bool = True) -> dict:
1✔
918
        """
919
        Converts the CanvasXpress object into HTML5 complant script.
920

921
        If the associated `CXData` is a type of `CXProfiledData` and a profile
922
        has yet to be assigned then a profile can be assigned according to the
923
        `CXConfig` labelled `graphType`.  If a profile is assigned but is not
924
        `CXRawProfile` then the `graphType` can be reassessed, and if
925
        appropriate a new profile better aligned to the data can be provided.
926

927
        :returns: `dict` A map of values appropriate for use in HTML, such as
928
            via a jinja template typical of flask apps.
929

930
        Assuming a jinja template such as this:
931
        ```html
932
        <html>
933
            <head>
934
                <meta charset="UTF-8">
935
                <title>Flask CanvasXpress Example</title>
936
            </head>
937
            <body>
938
                <!-- 1. DOM element where the visualization will be appear -->
939
                {{canvas_element|safe}}
940

941
                <!-- 2. Include the CanvasXpress library -->
942
                <link
943
                    href='https://www.canvasxpress.org/dist/canvasXpress.css'
944
                    rel='stylesheet' type='text/css'/>
945
                <script
946
                    src='https://www.canvasxpress.org/dist/canvasXpress.min.js'
947
                    type='text/javascript'>
948
                </script>
949

950
                <!-- 3. Include script to initialize object -->
951
                <script type="text/javascript">
952
                    onReady(function () {
953
                        {{bar_graph|safe}}
954
                    })
955
                </script>
956
            </body>
957
        </html>
958
        ```
959

960
        A flask function could create_element a page with a chart such as:
961
        ```python
962
        @app.route('/pythonexample')
963
        def get_simple_chart() -> str:
964
            chart: CanvasXpress = CanvasXpress(
965
                render_to="example_chart",
966
                data=CXDictData(
967
                    {
968
                        "y": {
969
                        "vars": ["Gene1"],
970
                        "smps": ["Smp1", "Smp2", "Smp3"],
971
                        "data": [[10, 35, 88]],
972
                    }
973
                ),
974
                config=CXConfigs(
975
                    CXGraphType(CXGraphTypeOptions.Bar)
976
                )
977
            )
978

979
            html_parts = chart.render_to_html_parts()
980

981
            return render_template(
982
                "bar.html",
983
                canvas_element=html_parts["cx_canvas"],
984
                bar_graph=html_parts["cx_js"]
985
            )
986
        ```
987

988
        :param fix_missing_profile: `bool`
989
            Defaults to `True`.  If `True` then CXData used for the chart will
990
            be provided with a data profile appropriate to the `graphType`
991
            (or CXStandardProfile if no graphType is provided).  If `False`
992
            then no profile will be applied to those data objects without
993
            profiles.
994

995
        :param match_profile_to_graphtype: `bool`
996
            Defaults to `True`.  If `True` then the `graphType` will be
997
            inspected and an appropriate data profile will be applied to
998
            the data object.  If a profile of an appropriate type is already
999
            associated then nothing is changed.  If a CXRawProfile is associated
1000
            then no change is made regardless of the paranmeter value.
1001
            Missing profiles are ignored unless fix_missing_profile is also
1002
            `True`.  If `False` then no change to the data profile will be made
1003
            if a profile is already associated with the data object.
1004
        """
1005
        #  Capture the ID once to avoid anonymous object calls producing different IDs.
1006
        render_id = self.render_to
1✔
1007

1008
        primary_params = {
1✔
1009
            "renderTo": render_id,
1010
            "data": self.provide_data_object().render_to_dict(),
1011
            "config": self.config.render_to_dict(),
1012
            "events": "js_events",
1013
        }
1014
        secondary_params = self.other_init_params.render_to_dict()
1✔
1015
        canvasxpress = {**primary_params, **secondary_params}
1✔
1016
        after_render_functions = []
1✔
1017
        for fx in self.after_render.render_to_list():
1✔
1018
            params = [json.dumps(p) for p in fx[1]]
1✔
1019
            after_render_functions.append(
1✔
1020
                f"CanvasXpress.$('{render_id}').{fx[0]}({', '.join(params)})"
1021
            )
1022

1023
        # Support unique data without JSON data structure
1024
        if canvasxpress["data"].get("raw"):
1✔
1025
            canvasxpress["data"] = str(canvasxpress["data"]["raw"])
1✔
1026

1027
        cx_js = render_from_template(
1✔
1028
            _CX_JS_TEMPLATE,
1029
            {
1030
                "cx_target_id": render_id,
1031
                "cx_json": json.dumps(canvasxpress),
1032
                "cx_functions": "\n" + "; ".join(after_render_functions) + ";\n",
1033
            },
1034
        )
1035

1036
        cx_js = cx_js.replace(
1✔
1037
            '"js_events"',
1038
            self.events.render_to_js(),
1039
        )
1040

1041
        canvas_configs = CXConfigs(
1✔
1042
            {
1043
                "id": render_id,
1044
                "width": self.width,
1045
                "height": self.height,
1046
            }
1047
        )
1048
        cx_canvas = (
1✔
1049
            "<canvas "
1050
            + " ".join(
1051
                [
1052
                    f"{str(config.label)}={json.dumps(config.value)}"
1053
                    for config in canvas_configs.configs
1054
                ]
1055
            )
1056
            + "></canvas>"
1057
        )
1058

1059
        cx_license = render_from_template(
1✔
1060
            _CX_LICENSE_TEMPLATE, {"cx_license": self.license_url}
1061
        )
1062

1063
        cx_web_data = dict()
1✔
1064
        cx_web_data["cx_js"] = cx_js
1✔
1065
        cx_web_data["cx_canvas"] = cx_canvas
1✔
1066
        if self.license_available:
1✔
1067
            cx_web_data["cx_license"] = cx_license
1✔
1068

1069
        return cx_web_data
1✔
1070

1071
    def __str__(self) -> str:
1✔
1072
        """
1073
        *str* function.  Converts the `CanvasXpress` object into a JSON
1074
         representation.
1075
        :returns" `str`
1076
            JSON form of the `CanvasXpress` object.
1077
        """
1078
        data = str(type(self.data)).split(".")[-1][:-2] if self.data else "None"
×
1079

1080
        return (
×
1081
            f"CanvasXpress ({hex(id(self))}):"
1082
            f" render_to '{self.render_to}';"
1083
            f" data <{data}>;"
1084
            f" config {len(self.config.configs)} item(s);"
1085
            f" after_render {len(self.after_render.configs)} item(s));"
1086
            f" other_init_params"
1087
            f" {len(self.other_init_params.configs)} item(s);"
1088
            f" events {len(self.events.events)} function(s)."
1089
        )
1090

1091
    def __repr__(self) -> str:
1✔
1092
        """
1093
        *repr* function.  Converts the `CanvasXpress` object into a pickle
1094
        string that can be used with `eval` to establish a copy of the object.
1095
        :returns: `str` An evaluatable representation of the object.
1096
        """
1097

1098
        normalized_data = self.data.render_to_dict(config=self.config)
1✔
1099
        if normalized_data.get("raw"):
1✔
1100
            normalized_data = normalized_data["raw"]
×
1101
        str_data = json.dumps(normalized_data)
1✔
1102
        str_data_parts = str_data.split("\n")
1✔
1103
        str_data = "\n".join(["    " + line for line in str_data_parts])[4:]
1✔
1104

1105
        str_config = json.dumps(self.config.render_to_dict())
1✔
1106
        str_config_parts = str_config.split("\n")
1✔
1107
        str_config = "\n".join(["    " + line for line in str_config_parts])[4:]
1✔
1108

1109
        str_after_render = json.dumps(self.after_render.render_to_list())
1✔
1110
        str_after_render_parts = str_after_render.split("\n")
1✔
1111
        str_after_render = "\n".join(
1✔
1112
            ["    " + line for line in str_after_render_parts]
1113
        )[4:]
1114

1115
        str_other_init_params = json.dumps(
1✔
1116
            self.other_init_params.render_to_dict(), indent=4
1117
        )
1118
        str_other_init_params_parts = str_other_init_params.split("\n")
1✔
1119
        str_other_init_params = "\n".join(
1✔
1120
            ["    " + line for line in str_other_init_params_parts]
1121
        )[4:]
1122

1123
        repr_str = (
1✔
1124
            _CX_REPR_TEMPLATE.replace("@render_to@", self.render_to)
1125
            .replace("@data@", str_data)
1126
            .replace("@config@", str_config)
1127
            .replace("@width@", json.dumps(self.width))
1128
            .replace("@height@", json.dumps(self.height))
1129
            .replace("@events@", repr(self.events))
1130
            .replace("@after_render@", str_after_render)
1131
            .replace("@other_init_params@", str_other_init_params)
1132
            .replace("true", "True")
1133
            .replace("false", "False")
1134
            .replace("null", "None")
1135
        )
1136

1137
        return repr_str
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc