holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.76k stars 517 forks source link

pn.pane.HiPlot #6258

Open avivazran opened 9 months ago

avivazran commented 9 months ago

I would benefit from having a native way to display HiPlot object in my application

given a hiplot experiment exp of type hiplot.experiment.Experiment: I would like to be able to wrap it inside a pn.pane.HiPlot(exp)

I have previously tried to use exp.to_html method to:

  1. use it with pn.pane.HTML - hiplot isn't rendering.
  2. save the pn.pane.HTML into a buffer by: pn.pane.HTML(exp.to_html()).save(BytesIO(), embed=True, resources=bokeh.resources.INLINE,embed_json=True)
avivazran commented 6 months ago

so is it something to that might happen? Because this can be an amazing component in my opinion. achieved something similar with Plotly pane but rendering stutters on big data frame

ahuang11 commented 6 months ago

Is this HiPlot? https://github.com/facebookresearch/hiplot

It seems like it's been archived?

What if you put it in iframe or try wrapping it in ReactiveHTML? https://panel.holoviz.org/explanation/components/reactive_html_components.html

MarcSkovMadsen commented 6 months ago

Please note that even though HiPlot is quite popular the repository has just been "archived" by facebook.

image

There is no explanation why.

MarcSkovMadsen commented 6 months ago

I tried to see if I could make a quick prototype. I see the following challenges

import panel as pn
import param
from panel.reactive import ReactiveHTML

pn.extension()

class HiPlot(ReactiveHTML):
    index = param.Integer(default=0)
    __javascript__=[
            "https://unpkg.com/react@17/umd/react.development.js",
            "https://unpkg.com/react-dom@17/umd/react-dom.development.js",
            "https://unpkg.com/@babel/standalone/babel.min.js",
            "https://cdn.jsdelivr.net/npm/hiplot@0.1.34-rc.200/dist/hiplot.lib.min.js",
        ]

    _template = """
<div id="hiplotContainer" style="background-color: white"><div style="text-align: center">Loading HiPlot...</div>
    <noscript>
    HiPlot needs JavaScript to run
    </noscript>
</div>    
"""

    _scripts = {
        "render": """
console.log(hiplot)

var experimentData = [
    {'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1},
    {'opt': 'adam', 'lr': 0.1, 'dropout': 0.2},
    {'opt': 'adam', 'lr': 1., 'dropout': 0.3},
    {'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4},
];
var experiment = hiplot.Experiment.from_iterable(experimentData);
ReactDOM.render(
    React.createElement(
        hiplot.HiPlot, 
        {experiment: experiment}
    ), 
    hiplotContainer
)
"""
    }

HiPlot(width=500, height=200).servable()

image

See also https://facebookresearch.github.io/hiplot/tuto_javascript.html#

avivazran commented 6 months ago

I’ve also tried make a prototype using ReactiveHTML but got confused with the JavaScript. I think the best approach will be to build the component straight from the JavaScript (or react as it is used in the streamlit component) but I lack the required Js/react knowledge. When pip install hiplot - there are new js files in the site-packages/hiplot lib which are used when exporting the plot to html. Maybe we can leverage that?

I think that the fact it was archived is good because once we will have a working prototype it will be safe from breaking code changes.

I would like to explain my motivation for enabling this component. I am an electrical engineer working in the field of High speed networking. We are collecting huge amounts of data and we need to dig through it, looking for anomalies which might cause errors. my team is unique in the company and have knowledge in python. I’ve been experimenting with Panel for about a year now and I believe that this awesome package can bring a huge change to the teams working methodology. Hiplot has proved itself in helping us quickly visualize the state of the system and quickly find errors. My target is to have my company become a customer of Panel for fast dashboarding for our operations.

Having a hiplot component implemented in my proof of concept will undoubtedly seal the deal.

I know that it might be a lot of work to bring up this component but just so you know that there is a huge benefit in it for HW validation field which in my company, isn’t realized. And we are top in our field. Again, if someone could take this project and bring it up from the Js/Ts code it will open Panel to a whole new field. I am willing to help however Ican to make it happen. Even working closely with your engineers to share knowledge I already have when experimenting with the package and its structure.

please don’t discard this idea.

MarcSkovMadsen commented 5 months ago

The next version of Panel will ship ReactiveESM (or something similar). It should make it easier to create React components.

I tried using what is in the reactive_html_esm branch.

import param
import panel as pn

pn.extension()

class HiPlot(pn.ReactiveESM):
    value = param.List()

    _esm = """
import {default as hip} from "https://esm.sh/hiplot@0.1.33";

console.log(hip);

function App(props) {
    const [value, setValue] = props.state.value;
    console.log(value)
    const experiment = hip.Experiment.from_iterable(value);
    return <hip.HiPlot experiment={experiment} />;
}

export function render({state}) {
    return <App state={state}/>;
}
"""

experiment = [
    {'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1},
    {'opt': 'adam', 'lr': 0.1, 'dropout': 0.2},
    {'opt': 'adam', 'lr': 1., 'dropout': 0.3},
    {'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4},
]
HiPlot(value=experiment).servable()

But for some reason it raises an exception. Whether its on HiPlot side or Panel side I don't know.

image

avivazran commented 4 months ago

Hi, @MarcSkovMadsen so I've managed to come up with some advancement.

the TypeError: Cannot read properties of undefined (reading 'fnOrder') is caused when "TABLE" is enabled in theenabled_displays. image

removing it actually loaded the parallel plot. image

however, we can see that the structure of the rendered html is off. my guess is that it is related either to the unknown div ID or the loading of the CSS files.

so far no ReactiveESM class in panel package so can't test it.

@ahuang11 I've managed to use IFrame to display the graph properly. image

the downside is that I any interaction done by the user with the plot is (changing columns order, coloring by column etc..) is not available to me. in other words, the state of the Hiplot object is not reachable, which is not the desired. I would like to be able to access the plot current state as I want to update the data live while maintaining the adjustments done by the user.

avivazran commented 4 months ago

I really feel like i'm on the brink of solving this. but I get this in the console:

image

I have a feeling that CSS handling is off and this is the last thing to solve before going into adding some logic. @MarcSkovMadsen can you maybe help here please?

from bokeh.io import output_notebook,push_notebook
import panel as pn
import param
from panel.reactive import ReactiveHTML

class HiPlot(ReactiveHTML):
    elem = param.Parameter()
    __javascript__=[
            "https://unpkg.com/react@17/umd/react.development.js",
            "https://unpkg.com/react-dom@17/umd/react-dom.development.js",
            "https://unpkg.com/@babel/standalone/babel.min.js",
            # "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/webpack.config.js"
             #"https://cdn.jsdelivr.net/npm/hiplot@0.1.33/dist/hiplot.lib.min.js"
             "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/dist/hiplot.lib.js"
            # "https://cdn.jsdelivr.net/npm/hiplot@0.1.34-rc.200/dist/hiplot.lib.min.js",
            # "https://cdn.jsdelivr.net/npm/hiplot@0.1.32/dist/hiplot.bundle.js.map",
        # "https://cdn.jsdelivr.net/npm/hiplot@0.1.32/dist/hiplot.bundle.js.map",
        #"https://cdn.jsdelivr.net/npm/hiplot@0.1.32/dist/hiplot.lib.js",

        ]

    _template = """

<div id="hiplotContainer" style="background-color: white"><div style="text-align: center">Loading HiPlot...</div>
    <noscript>
    HiPlot needs JavaScript to run
    </noscript>
</div>    
"""

    __css__=[
        "https://unpkg.com/bootstrap/dist/css/bootstrap.min.css",
        "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/parallel/parallel.scss"
       # "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/style/global.scss",
        #"https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/style/bs-dark.scss",
        #"https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/style/bs-light.scss",
        #"https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/style/bs-light.scss",
        #"https://cdn.jsdelivr.net/npm/hiplot@0.1.33/src/parallel/parallel.scss",

    ]
    _scripts = {
        "render": """

console.log(hiplot)

var experimentData = [
    {'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1},
    {'opt': 'adam', 'lr': 0.1, 'dropout': 0.2},
    {'opt': 'adam', 'lr': 1., 'dropout': 0.3},
    {'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4},
];
var experiment = hiplot.Experiment.from_iterable(experimentData);
let plugins = hiplot.createDefaultPlugins();
delete plugins[hiplot.DefaultPlugins.TABLE];
delete plugins[hiplot.DefaultPlugins.XY];
delete plugins[hiplot.DefaultPlugins.DISTRIBUTION];
console.log(experiment)
var elem = React.createElement(
        hiplot.HiPlot, 
        {experiment: experiment,plugins:plugins,dark:false}

    );

console.log('elem')
console.log(elem.props.plugins)

ReactDOM.render(elem, hiplotContainer);
"""
    }
pn.extension()
HiPlot(width=1000, height=1000).servable()

image

avivazran commented 4 months ago

Think it's related to the bootstrap the package is using. As it doesn't have access to the html inside the shadow dom. Is there a strategy to override this somehow or replace bootstrap with material or something like this?

MarcSkovMadsen commented 4 months ago

scss files need to be compiled and cannot be used directly in the browser to my knowledge. But if you look inside the file it looks like css.

Try including the css inside a <style> tag in your ReactiveHTML _template.

MarcSkovMadsen commented 4 months ago

Ahh. There are several scss files. Try to see if you can find some compiled css files to include. For example I guess the Jupyter widget uses compiled css files.

avivazran commented 3 months ago
from hiplot_style import hiplot_styleheets

class HiPlotComponent(ReactiveHTML):

    colorby = param.String()
    dark_mode = param.Boolean(default=False)
    values = param.List(default=hiplot_default_data)

    _template = """

<div id="hiplotContainer" style="background-color: white"><div style="text-align: center; height:100%;width:100%;">Loading HiPlot...</div>
    <noscript>
    HiPlot needs JavaScript to run
    </noscript>
</div>    
"""

    # _dom_events = {'text-input': ['change']}

    # By declaring an _extension_name the component should be loaded explicitly with pn.extension('material-components')
    # _extension_name = 'material-components'
    _scripts = {
        "render": """
console.log(Object);

var experimentData = data.values

var experiment = hiplot.Experiment.from_iterable(experimentData);
console.log(experiment);
experiment.display_data[hiplot.DefaultPlugins.PARALLEL_PLOT] = {
  'hide': ['uid', 'from_uid'],
};
experiment.colorby = data.colorby;
let plugins = hiplot.createDefaultPlugins();

delete plugins[hiplot.DefaultPlugins.XY];
delete plugins[hiplot.DefaultPlugins.DISTRIBUTION];
delete plugins[hiplot.DefaultPlugins.TABLE];

console.log(data.dark_mode);
// Create React elements without JSX
var hip_elem =     React.createElement(
        hiplot.HiPlot,
        { 
            experiment: experiment,
            plugins: plugins,
            dark: data.dark_mode
        }
    )

console.log(hip_elem);
var elem = React.createElement(
    React.StrictMode,
    null,
    hip_elem
);

console.log(elem);
console.log(state);

// Render the React element
ReactDOM.render(
    elem,
    hiplotContainer // Replace with your actual container ID
);

        """,
        "dark_mode": """self.render();"""
    }

    __javascript__ = [
        # 'https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js',
            "https://unpkg.com/react@17/umd/react.development.js",
            "https://unpkg.com/react-dom@17/umd/react-dom.development.js",
            "https://unpkg.com/@babel/standalone/babel.min.js",
        # "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/dist/hiplot.lib.min.js",
        # "https://cdnjs.cloudflare.com/ajax/libs/datatables.net-responsive-bs4/3.0.2/responsive.bootstrap4.js",
        "https://unpkg.com/hiplot@0.1.33/dist/hiplot.lib.js"
        # "https://cdn.jsdelivr.net/npm/hiplot@0.1.33/dist/hiplot.lib.js",
    ]

    __css__ = [
        # 'https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css'
        # "https://unpkg.com/bootstrap/dist/css/bootstrap.min.css",
    ]

    __javascript_modules__ = [
    ]

    _stylesheets = [
        hiplot_styleheets['1.33']
    ]
    # _dom_events = {'dark_mode': ['change']}
avivazran commented 3 months ago

on different .py file otherwise messes up notebook css


hiplot_styleheets = {

'1.33':"""
._32HeyC7HWVqvVsPvUXaOos svg {
    font-family: Ubuntu, Tahoma, Helvetica, sans-serif;
}

._32HeyC7HWVqvVsPvUXaOos canvas,
._32HeyC7HWVqvVsPvUXaOos svg {
    position: absolute;
    top: 0;
    left: 0;
}

._32HeyC7HWVqvVsPvUXaOos {
    position: relative;
}

._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts {
    fill: rgba(100, 100, 100, 0.15);
    stroke: #fff;
}

._1BHre2Oe6TFO4hTuim52P4:hover rect._1GcXKZcqKY3u_RsHyssxts {
    stroke: #222;
    stroke-dasharray: 5, 5;
}

._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts:hover {
    stroke-dasharray: none;
}

._2jwlhkQibLWzPLqgPawz-2 .label-name {
    transform-origin: bottom left;
}

._3jWoJZMEJsghgskeSu6y2r ._26LxRBU1xQqr-IZixZo4Yr {
    font-size: 16px;
    font-weight: bold;
}

._3jWoJZMEJsghgskeSu6y2r {
    cursor: move;
    font-size: 16px;
}

._3jWoJZMEJsghgskeSu6y2r text {
    fill: #111;
    text-anchor: right;
    font-size: 13px;
    text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}

._3jWoJZMEJsghgskeSu6y2r line,
._3jWoJZMEJsghgskeSu6y2r path {
    fill: none;
    stroke: #777;
    stroke-width: 1;
}

._3jWoJZMEJsghgskeSu6y2r .tick {
    width: 200px;
}

.hip_thm--dark ._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts {
    fill: rgba(100, 100, 100, 0.15);
    stroke: #ddd;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r text {
    fill: #f2f2f2;
    text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r line,
.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r path {
    stroke: #777;
}

._1ZUSF-tbSSCscSmTY4-BEX {
    margin: 0;
    width: 100%;
    height: 100%;
    padding: 0;
}

._1ZUSF-tbSSCscSmTY4-BEX {
    font-family: Ubuntu, Tahoma, Helvetica, sans-serif;
}

.hip_thm--light {
    background: #f7f7f7;
    color: #404040;
}

._1ZUSF-tbSSCscSmTY4-BEX a {
    text-decoration: none;
}

._1AfhwbHQacWQLj0k2eqYPY {
    padding: 0 3.5%;
}

._3ffmvq_BKNEwt5UY9Gc-3z rect {
    fill: none;
}

._1ThJRgZ_-ADTF9DxMm1vO3 {
    fill: none;
}

.Z9LpR6Ss9OVkeNf1RPX89 {
    white-space: nowrap;
}

._3jBnCXb7Wq6d6Nj5qU0tZj,
._3PwMN63swXgYs2V3NaqW4N,
._2fSQp8UpBsNXqy2qBvI5iU {
    float: left;
}

._3jBnCXb7Wq6d6Nj5qU0tZj {
    width: 23%;
    margin: 0 1%;
}

._3PwMN63swXgYs2V3NaqW4N {
    width: 31.3%;
    margin: 0 1%;
}

._2fSQp8UpBsNXqy2qBvI5iU {
    width: 48%;
    margin: 0 1%;
}

._1ZUSF-tbSSCscSmTY4-BEX h3 {
    margin: 12px 0 9px;
}

._1ZUSF-tbSSCscSmTY4-BEX h3 small {
    color: #888;
    font-weight: normal;
}

._1ZUSF-tbSSCscSmTY4-BEX p {
    margin: 0.6em 0;
}

._1ZUSF-tbSSCscSmTY4-BEX small {
    line-height: 1.2em;
}

._3Xzbpi7clTMlgQuSC3TkSJ,
._3jHLkrE3pIpw8R_Qw5_T0S {
    width: 0%;
    font-weight: bold;
    height: 100%;
}

._3Xzbpi7clTMlgQuSC3TkSJ {
    background: #3d9aff;
    border-right: 1px solid #666;
}

._3jHLkrE3pIpw8R_Qw5_T0S {
    background: rgba(171, 171, 171, 0.5);
    border-right: 1px solid #999;
}

._1Mg1NRi7I_xMRF7P6dMc27 {
    height: 2px;
    line-height: 2px;
    width: 100%;
}

._2ZYBXU2M0MFXTysMcWHNaP {
    width: 268px;
    float: left;
}

.-wfDlWaWyKfeKj7tWu_NX {
    float: right;
    height: 24px;
    line-height: 24px;
}

._27zXkTqskweooVZri-rve2 button {
    border-color: #000 !important;
}

._27zXkTqskweooVZri-rve2 button:disabled {
    border: solid 1px transparent !important;
}

::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track {
    background: #ddd;
    border-radius: 12px;
}

::-webkit-scrollbar-thumb {
    background: #b5b5b5;
    border-radius: 12px;
}

._1Bsj-IoFR-3UZmBWt4DK3U .tick line {
    color: #9a9a9a26;
}

._2TQYADEAYb-yeOW_o05tV6 .tick line {
    color: #9a9a9a26;
}

.wYt95QU-sFBMT3IkH8bwU {
    min-height: 100vh;
}

.mRwqXRNbsMNf1DyEbEt7a {
    overflow-x: auto;
}

._37gV_F_HNGkKc8oXAGsUwm {
    height: 10px;
    width: 10px;
    display: inline-block;
}

._2dARw8OX_2i77zjmu1zT9T {
    display: inline-block;
    width: 0px;
}

.QfFdce7wIRJIhiVuXhuDW {
    top: 100%;
    left: 0%;
}

._2dARw8OX_2i77zjmu1zT9T ._32FheJCQOHwmFVYLzJnNY6 {
    visibility: hidden;
    background-color: #000;
    color: #fff;
    text-align: center;
    padding: 5px;
    border-radius: 6px;
}

.Z9LpR6Ss9OVkeNf1RPX89:hover + ._32FheJCQOHwmFVYLzJnNY6 {
    visibility: visible;
}

.Z9LpR6Ss9OVkeNf1RPX89 {
    color: #5e5e5e;
}

.Z9LpR6Ss9OVkeNf1RPX89:hover {
    color: #000;
    text-decoration: underline dotted;
}

.hip_thm--dark .Z9LpR6Ss9OVkeNf1RPX89 {
    color: #b7b7

b7;
}

.hip_thm--dark .Z9LpR6Ss9OVkeNf1RPX89:hover {
    color: #fff;
}

.ZS2MuDjd27slndFC6jn1o line {
    stroke: #000;
    stroke-width: 2;
}

._3kwumxQ6vpcMAxOpEckXUT rect {
    fill: #9467bd;
}

._2jYuBVDUw-byb6rjf5UDty {
    width: 100%;
    height: 50px;
    font-family: monospace;
    font-size: 12pt;
    resize: none;
    overflow: hidden;
}

._1084BZwPDL5dGuJyHsG-Kb {
    height: 25px !important;
}

._27zXkTqskweooVZri-rve2 {
    border-bottom: 1px solid rgba(100, 100, 100, 0.35);
    background: #e2e2e2;
    padding: 6px 24px 4px;
    line-height: 24px;
}

._27zXkTqskweooVZri-rve2 h1 {
    display: inline-block;
    margin: 0px 14px 0 0;
}

._27zXkTqskweooVZri-rve2 button {
    vertical-align: top;
}

._1kTtwhJnygQhCtR86nYUmI {
    margin-left: 5px;
    margin-right: 5px;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r text._2MRseUYIh66-VQsHtH05Kh {
    fill: #ddd;
}

.hip_thm--dark ._27zXkTqskweooVZri-rve2 {
    background: #040404;
    color: #f3f3f3;
}

.hip_thm--dark {
    background: #131313;
    color: #e3e3e3;
}

.hip_thm--dark a {
    color: #5ae;
}

.hip_thm--dark ._1ThJRgZ_-ADTF9DxMm1vO3 {
    fill: none;
}

.hip_thm--dark ::-webkit-scrollbar-track {
    background: #222;
}

.hip_thm--dark ::-webkit-scrollbar-thumb {
    background: #444;
}

.hip_thm--dark ._27zXkTqskweooVZri-rve2 button:enabled {
    border-color: #fff !important;
}

.hip_thm--dark .ZS2MuDjd27slndFC6jn1o line {
    stroke: #fff;
    stroke-width: 2;
}

.hip_thm--dark ._3kwumxQ6vpcMAxOpEckXUT rect {
    fill: #635075;
}
""",

'1.32':"""
._32HeyC7HWVqvVsPvUXaOos svg {
  font-family: Ubuntu, Tahoma, Helvetica, sans-serif;
}

._32HeyC7HWVqvVsPvUXaOos canvas,
._32HeyC7HWVqvVsPvUXaOos svg {
  position: absolute;
  top: 0;
  left: 0;
}

._32HeyC7HWVqvVsPvUXaOos {
  position: relative;
}

._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts {
  fill: rgba(100, 100, 100, 0.15);
  stroke: #fff;
}

._1BHre2Oe6TFO4hTuim52P4:hover rect._1GcXKZcqKY3u_RsHyssxts {
  stroke: #222;
  stroke-dasharray: 5, 5;
}

._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts:hover {
  stroke-dasharray: none;
}

._2jwlhkQibLWzPLqgPawz-2 .label-name {
  transform-origin: bottom left;
}

._3jWoJZMEJsghgskeSu6y2r ._26LxRBU1xQqr-IZixZo4Yr {
  font-size: 16px;
  font-weight: bold;
}

._3jWoJZMEJsghgskeSu6y2r {
  cursor: move;
  font-size: 16px;
}

._3jWoJZMEJsghgskeSu6y2r text {
  fill: #111;
  text-anchor: right;
  font-size: 13px;
  text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}

._3jWoJZMEJsghgskeSu6y2r line,
._3jWoJZMEJsghgskeSu6y2r path {
  fill: none;
  stroke: #777;
  stroke-width: 1;
}

._3jWoJZMEJsghgskeSu6y2r .tick {
  width: 200px;
}

/* Dark theme */

.hip_thm--dark ._1BHre2Oe6TFO4hTuim52P4 rect._1GcXKZcqKY3u_RsHyssxts {
  fill: rgba(100, 100, 100, 0.15);
  stroke: #ddd;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r text {
  fill: #f2f2f2;
  text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r line,
.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r path {
  stroke: #777;
}

/* General styling */

._1ZUSF-tbSSCscSmTY4-BEX {
  margin: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  font-family: Ubuntu, Tahoma, Helvetica, sans-serif;
}

.hip_thm--light {
  background: #f7f7f7;
  color: #404040;
}

._1ZUSF-tbSSCscSmTY4-BEX a {
  text-decoration: none;
}

._1AfhwbHQacWQLj0k2eqYPY {
  padding: 0 3.5%;
}

._3ffmvq_BKNEwt5UY9Gc-3z rect {
  fill: none;
}

._1ThJRgZ_-ADTF9DxMm1vO3 {
  fill: none;
}

.Z9LpR6Ss9OVkeNf1RPX89 {
  white-space: nowrap;
}

._3jBnCXb7Wq6d6Nj5qU0tZj,
._3PwMN63swXgYs2V3NaqW4N,
._2fSQp8UpBsNXqy2qBvI5iU {
  float: left;
}

._3jBnCXb7Wq6d6Nj5qU0tZj {
  width: 23%;
  margin: 0 1%;
}

._3PwMN63swXgYs2V3NaqW4N {
  width: 31.3%;
  margin: 0 1%;
}

._2fSQp8UpBsNXqy2qBvI5iU {
  width: 48%;
  margin: 0 1%;
}

._1ZUSF-tbSSCscSmTY4-BEX h3 {
  margin: 12px 0 9px;
}

._1ZUSF-tbSSCscSmTY4-BEX h3 small {
  color: #888;
  font-weight: normal;
}

._1ZUSF-tbSSCscSmTY4-BEX p {
  margin: 0.6em 0;
}

._1ZUSF-tbSSCscSmTY4-BEX small {
  line-height: 1.2em;
}

._3Xzbpi7clTMlgQuSC3TkSJ,
._3jHLkrE3pIpw8R_Qw5_T0S {
  width: 0%;
  font-weight: bold;
  height: 100%;
}

._3Xzbpi7clTMlgQuSC3TkSJ {
  background: #3d9aff;
  border-right: 1px solid #666;
}

._3jHLkrE3pIpw8R_Qw5_T0S {
  background: rgba(171, 171, 171, 0.5);
  border-right: 1px solid #999;
}

._1Mg1NRi7I_xMRF7P6dMc27 {
  height: 2px;
  line-height: 2px;
  width: 100%;
}

._2ZYBXU2M0MFXTysMcWHNaP {
  width: 268px;
  float: left;
}

.-wfDlWaWyKfeKj7tWu_NX {
  float: right;
  height: 24px;
  line-height: 24px;
}

._27zXkTqskweooVZri-rve2 button {
  border-color: #000 !important;
}

._27zXkTqskweooVZri-rve2 button:disabled {
  border: solid 1px transparent !important;
}

._1Bsj-IoFR-3UZmBWt4DK3U .tick line {
  color: #9a9a9a26;
}

._2TQYADEAYb-yeOW_o05tV6 .tick line {
  color: #9a9a9a26;
}

.wYt95QU-sFBMT3IkH8bwU {
  min-height: 100vh;
}

.mRwqXRNbsMNf1DyEbEt7a {
  overflow-x: auto;
}

._37gV_F_HNGkKc8oXAGsUwm {
  height: 10px;
  width: 10px;
  display: inline-block;
}

._2dARw8OX_2i77zjmu1zT9T {
  display: inline-block;
  width: 0px;
}

.QfFdce7wIRJIhiVuXhuDW {
  top: 100%;
  left: 0%;
}

._2dARw8OX_2i77zjmu1zT9T ._32FheJCQOHwmFVYLzJnNY6 {
  visibility: hidden;
  background-color: #000;
  color: #fff;
  text-align: center;
  padding: 5px;
  border-radius: 6px;
}

/* Show the tooltip text when you mouse over the tooltip container */

.Z9LpR6Ss9OVkeNf1RPX89:hover + ._32FheJCQOHwmFVYLzJnNY6 {
  visibility: visible;
}

.Z9LpR6Ss9OVkeNf1RPX89 {
  color: #5e5e5e;
}

.Z9LpR6Ss9OVkeNf1RPX89:hover {
  color: #000;
  text-decoration: underline dotted;
}

/* Dark theme adjustments */

.hip_thm--dark .Z9LpR6Ss9OVkeNf1RPX89 {
  color: #b7b7b7;
}

.hip_thm--dark .Z9LpR6Ss9OVkeNf1RPX89:hover {
  color: #fff;
}

.ZS2MuDjd27slndFC6jn1o line {
  stroke: #000;
  stroke-width: 2;
}

._3kwumxQ6vpcMAxOpEckXUT rect {
  fill: #9467bd;
}

._2jYuBVDUw-byb6rjf5UDty {
  width: 100%;
  height: 50px;
  font-family: monospace;
  font-size: 12pt;
  resize: none;
  overflow: hidden;
}

._1084BZwPDL5dGuJyHsG-Kb {
  height: 25px !important;
}

._27zXkTqskweooVZri-rve2 {
  border-bottom: 1px solid rgba(100, 100, 100, 0.35);
  background: #e2e2e2;
  padding: 6px 24px 4px;
  line-height: 24px;
}

._27zXkTqskweooVZri-rve2 h1 {
  display: inline-block;
  margin: 0px 14px 0 0;
}

._27zXkTqskweooVZri-rve2 button {
  vertical-align: top;
}

._1kTtwhJnygQhCtR86nYUmI {
  margin-left: 5px;
  margin-right: 5px;
}

.hip_thm--dark ._3jWoJZMEJsghgskeSu6y2r text._2MRseUYIh66-VQsHtH05Kh {
  fill: #ddd;
}

.hip_thm--dark ._27zXkTqskweooVZri-rve2 {
  background: #040404;
  color: #f3f3f3;
}

.hip_thm--dark {
  background: #131313;
  color: #e3e3e3;
}

.hip_thm--dark a {
  color: #5ae;
}

.hip_thm--dark ._1ThJRgZ_-ADTF9DxMm1vO3 {
  fill: none;
}

.hip_thm--dark ::-webkit-scrollbar-track {
  background: #222;
}

.hip_thm--dark ::-webkit-scrollbar-thumb {
  background: #444;
}

.hip_thm--dark ._27zXkTqskweooVZri-rve2 button:enabled {
  border-color: #fff !important;
}

.hip_thm--dark .ZS2MuDjd27slndFC6jn1o line {
  stroke: #fff;
  stroke-width: 2;
}

.hip_thm--dark ._3kwumxQ6vpcMAxOpEckXUT rect {
  fill: #635075;
}
""",

}