<commit_before>import classNames from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
import { FormattedDate, FormattedTime } from 'react-intl'
import { injectState, provideState } from '@julien-f/freactal'
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'

import _ from './intl'
import Button from './button'
import Component from './base-component'
import TimezonePicker from './timezone-picker'
import Tooltip from './tooltip'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
import { getXoServerTimezone } from './xo'
import { Range, Toggle } from './form'

// ===================================================================

const CLICKABLE = { cursor: 'pointer' }
const PREVIEW_SLIDER_STYLE = { width: '400px' }

// ===================================================================

const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']

const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20

const MONTHS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]

const DAYS = (() => {
  const days = []

  for (let i = 0; i < 4; i++) {
    days[i] = []

    for (let j = 1; j < 8; j++) {
      days[i].push(7 * i + j)
    }
  }

  days.push([29, 30, 31])

  return days
})()

const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]

const HOURS = (() => {
  const hours = []

  for (let i = 0; i < 4; i++) {
    hours[i] = []

    for (let j = 0; j < 6; j++) {
      hours[i].push(6 * i + j)
    }
  }

  return hours
})()

const MINS = (() => {
  const minutes = []

  for (let i = 0; i < 6; i++) {
    minutes[i] = []

    for (let j = 0; j < 10; j++) {
      minutes[i].push(10 * i + j)
    }
  }

  return minutes
})()

const PICKTIME_TO_ID = {
  minute: 0,
  hour: 1,
  monthDay: 2,
  month: 3,
  weekDay: 4,
}

const TIME_FORMAT = {
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
}

// ===================================================================

// monthNum: [ 0 : 11 ]
const getMonthName = monthNum => (
  <FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
)

// dayNum: [ 0 : 6 ]
const getDayName = dayNum => (
  // January, 1970, 5th => Monday
  <FormattedDate
    value={Date.UTC(1970, 0, 4 + dayNum)}
    weekday='long'
    timeZone='UTC'
  />
)

// ===================================================================

export class SchedulePreview extends Component {
  static propTypes = {
    cronPattern: PropTypes.string.isRequired,
    timezone: PropTypes.string,
  }

  componentDidMount () {
    getXoServerTimezone.then(serverTimezone => {
      this.setState({
        defaultTimezone: serverTimezone,
      })
    })
  }

  render () {
    const { defaultTimezone, value } = this.state
    const { cronPattern, timezone = defaultTimezone } = this.props
    const dates = createSchedule(cronPattern, timezone).next(value)

    return (

          {_('cronPattern')} {cronPattern}

          <Range
            min={MIN_PREVIEWS}
            max={MAX_PREVIEWS}
            onChange={this.linkState('value')}
            value={value && +value}
            required
          />

          {map(dates, (date, id) => (

              <FormattedTime value={date} {...TIME_FORMAT} />

          ))}

...

    )
  }
}

// ===================================================================

class ToggleTd extends Component {
  static propTypes = {
    children: PropTypes.any.isRequired,
    onChange: PropTypes.func.isRequired,
    tdId: PropTypes.number.isRequired,
    value: PropTypes.bool.isRequired,
  }

  _onClick = () => {
    const { props } = this
    props.onChange(props.tdId, !props.value)
  }

  render () {
    const { props } = this
    return (

        {props.children}

    )
  }
}

// ===================================================================

const TableSelect = [
  provideState({
    effects: {
      onChange: (_, tdId, add) => (_, { value, onChange }) => {
        const newValue = [...value]
        const index = sortedIndex(newValue, tdId)
        if (add) {
          newValue[index] !== tdId && newValue.splice(index, 0, tdId)
        } else {
          newValue[index] === tdId && newValue.splice(index, 1)
        }
        onChange(newValue)
      },
      selectAll: () => ({ optionsValues }, { onChange }) => {
        onChange(optionsValues)
      },
    },
    computed: {
      optionsValues: (_, { options }) => flatten(options),
    },
  }),
  injectState,
  ({ state, effects, labelId, options, optionRenderer = identity, value }) => {
    let k = 0
    return (

            {map(options, (line, i) => (

                {map(line, tdOption => (
                  <ToggleTd
                    children={optionRenderer(tdOption)}
                    tdId={tdOption}
                    key={tdOption}
                    onChange={effects.onChange}
                    value={
                      k < value.length && value[k] === tdOption && (++k, true)
                    }
                  />
                ))}

            ))}

        <Button className='pull-right' onClick={effects.selectAll}>
          {_(`selectTableAll${labelId}`)}
        </Button>

    )
  },
].reduceRight((value, decorator) => decorator(value))

TableSelect.propTypes = {
  labelId: PropTypes.string.isRequired,
  options: PropTypes.array.isRequired,
  optionRenderer: PropTypes.func,
  onChange: PropTypes.func.isRequired,
  value: PropTypes.array.isRequired,
}

// ===================================================================

const TimePicker = [
  provideState({
    effects: {
      onChange: (_, value) => ({ optionsValues }, { onChange }) => {
        if (isArray(value)) {
          value = value.length === optionsValues.length ? '*' : value.join(',')
        } else {
          value = `*/${value}`
        }

        onChange(value)
      },
    },
    computed: {
      maxStep: ({ optionsValues }) => Math.floor(optionsValues.length / 2),
      optionsValues: (_, { options }) => flatten(options),

      // '*' or '*/1' => all values
      // '2,7' => [2,7]
      // '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]
      tableValue: ({ optionsValues, step }, { value }) =>
        step === 1
          ? optionsValues
          : step !== undefined
            ? optionsValues.filter((_, i) => i % step === 0)
            : value.split(',').map(Number),

      // '*' => 1
      // '*/2' => 2
      // otherwise => undefined
      step: (_, { value }) =>
        value === '*'
          ? 1
          : value.indexOf('/') === 1
            ? +value.split('/')[1]
            : undefined,
    },
  }),
  injectState,
  ({ state, effects, ...props }) => (
    <Card>
      <CardHeader>
        {_(`scheduling${props.labelId}`)}
        {props.headerAddon}
      </CardHeader>
      <CardBlock>
        <TableSelect
          labelId={props.labelId}
          onChange={effects.onChange}
          optionRenderer={props.optionRenderer}
          options={props.options}
          value={state.tableValue}
        />
        <Range
          max={state.maxStep}
          min={1}
          onChange={effects.onChange}
          value={state.step}
        />
      </CardBlock>
    </Card>
  ),
].reduceRight((value, decorator) => decorator(value))

TimePicker.propTypes = {
  headerAddon: PropTypes.node,
  labelId: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  optionRenderer: PropTypes.func,
  options: PropTypes.array.isRequired,
  value: PropTypes.string.isRequired,
}

const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
  if (monthDayPattern === '*' && weekDayPattern === '*') {
    return
  }

  return weekDayPattern !== '*'
}

class DayPicker extends Component {
  static propTypes = {
    monthDayPattern: PropTypes.string.isRequired,
    weekDayPattern: PropTypes.string.isRequired,
  }

  state = {
    weekDayMode: isWeekDayMode(this.props),
  }

  componentWillReceiveProps (props) {
    const weekDayMode = isWeekDayMode(props)

    if (weekDayMode !== undefined) {
      this.setState({ weekDayMode })
    }
  }

  _setWeekDayMode = weekDayMode => {
    this.props.onChange(['*', '*'])
    this.setState({ weekDayMode })
  }

  _onChange = cron => {
    this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])
  }

  render () {
    const { monthDayPattern, weekDayPattern } = this.props
    const { weekDayMode } = this.state

    const dayModeToggle = (
      <Tooltip
        content={_(
          weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode'
        )}
      >

          <Toggle
            onChange={this._setWeekDayMode}
            iconSize={1}
            value={weekDayMode}
          />

      </Tooltip>
    )

    return (
      <TimePicker
        headerAddon={dayModeToggle}
        key={weekDayMode ? 'week' : 'month'}
        labelId='Day'
        onChange={this._onChange}
        optionRenderer={weekDayMode ? getDayName : undefined}
        options={weekDayMode ? WEEK_DAYS : DAYS}
        value={weekDayMode ? weekDayPattern : monthDayPattern}
      />
    )
  }
}

// ===================================================================

export default class Scheduler extends Component {
  static propTypes = {
    cronPattern: PropTypes.string,
    onChange: PropTypes.func,
    timezone: PropTypes.string,
    value: PropTypes.shape({
      cronPattern: PropTypes.string.isRequired,
      timezone: PropTypes.string,
    }),
  }

  constructor (props) {
    super(props)

    this._onCronChange = newCrons => {
      const cronPattern = this._getCronPattern().split(' ')
      forEach(newCrons, (cron, unit) => {
        cronPattern[PICKTIME_TO_ID[unit]] = cron
      })

      this.props.onChange({
        cronPattern: cronPattern.join(' '),
        timezone: this._getTimezone(),
      })
    }

    forEach(UNITS, unit => {
      this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
    })
    this._dayChange = ([monthDay, weekDay]) =>
      this._onCronChange({ monthDay, weekDay })
  }

  _onTimezoneChange = timezone => {
    this.props.onChange({
      cronPattern: this._getCronPattern(),
      timezone,
    })
  }

  _getCronPattern = () => {
    const { value, cronPattern = value.cronPattern } = this.props
    return cronPattern
  }

  _getTimezone = () => {
    const { value, timezone = value && value.timezone } = this.props
    return timezone
  }

  render () {
    const cronPatternArr = this._getCronPattern().split(' ')
    const timezone = this._getTimezone()

    return (

        <Row>
          <Col largeSize={6}>
            <TimePicker
              labelId='Month'
              optionRenderer={getMonthName}
              options={MONTHS}
              onChange={this._monthChange}
              value={cronPatternArr[PICKTIME_TO_ID['month']]}
            />
          </Col>
          <Col largeSize={6}>
            <DayPicker
              onChange={this._dayChange}
              monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
              weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
            />
          </Col>
        </Row>
        <Row>
          <Col largeSize={6}>
            <TimePicker
              labelId='Hour'
              options={HOURS}
              onChange={this._hourChange}
              value={cronPatternArr[PICKTIME_TO_ID['hour']]}
            />
          </Col>
          <Col largeSize={6}>
            <TimePicker
              labelId='Minute'
              options={MINS}
              onChange={this._minuteChange}
              value={cronPatternArr[PICKTIME_TO_ID['minute']]}
            />
          </Col>
        </Row>
        <Row>
          <Col>

            <TimezonePicker
              value={timezone}
              onChange={this._onTimezoneChange}
            />
          </Col>
        </Row>

    )
  }
}
<commit_msg>fix(xo-web/scheduling): fix unselecting single value (#3577)<commit_after>import classNames from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
import { FormattedDate, FormattedTime } from 'react-intl'
import { injectState, provideState } from '@julien-f/freactal'
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'

import _ from './intl'
import Button from './button'
import Component from './base-component'
import TimezonePicker from './timezone-picker'
import Tooltip from './tooltip'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
import { getXoServerTimezone } from './xo'
import { Range, Toggle } from './form'

// ===================================================================

const CLICKABLE = { cursor: 'pointer' }
const PREVIEW_SLIDER_STYLE = { width: '400px' }

// ===================================================================

const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']

const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20

const MONTHS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]

const DAYS = (() => {
  const days = []

  for (let i = 0; i < 4; i++) {
    days[i] = []

    for (let j = 1; j < 8; j++) {
      days[i].push(7 * i + j)
    }
  }

  days.push([29, 30, 31])

  return days
})()

const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]

const HOURS = (() => {
  const hours = []

  for (let i = 0; i < 4; i++) {
    hours[i] = []

    for (let j = 0; j < 6; j++) {
      hours[i].push(6 * i + j)
    }
  }

  return hours
})()

const MINS = (() => {
  const minutes = []

  for (let i = 0; i < 6; i++) {
    minutes[i] = []

    for (let j = 0; j < 10; j++) {
      minutes[i].push(10 * i + j)
    }
  }

  return minutes
})()

const PICKTIME_TO_ID = {
  minute: 0,
  hour: 1,
  monthDay: 2,
  month: 3,
  weekDay: 4,
}

const TIME_FORMAT = {
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
}

// ===================================================================

// monthNum: [ 0 : 11 ]
const getMonthName = monthNum => (
  <FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
)

// dayNum: [ 0 : 6 ]
const getDayName = dayNum => (
  // January, 1970, 5th => Monday
  <FormattedDate
    value={Date.UTC(1970, 0, 4 + dayNum)}
    weekday='long'
    timeZone='UTC'
  />
)

// ===================================================================

export class SchedulePreview extends Component {
  static propTypes = {
    cronPattern: PropTypes.string.isRequired,
    timezone: PropTypes.string,
  }

  componentDidMount () {
    getXoServerTimezone.then(serverTimezone => {
      this.setState({
        defaultTimezone: serverTimezone,
      })
    })
  }

  render () {
    const { defaultTimezone, value } = this.state
    const { cronPattern, timezone = defaultTimezone } = this.props
    const dates = createSchedule(cronPattern, timezone).next(value)

    return (

          {_('cronPattern')} {cronPattern}

          <Range
            min={MIN_PREVIEWS}
            max={MAX_PREVIEWS}
            onChange={this.linkState('value')}
            value={value && +value}
            required
          />

          {map(dates, (date, id) => (

              <FormattedTime value={date} {...TIME_FORMAT} />

          ))}

...

    )
  }
}

// ===================================================================

class ToggleTd extends Component {
  static propTypes = {
    children: PropTypes.any.isRequired,
    onChange: PropTypes.func.isRequired,
    tdId: PropTypes.number.isRequired,
    value: PropTypes.bool.isRequired,
  }

  _onClick = () => {
    const { props } = this
    props.onChange(props.tdId, !props.value)
  }

  render () {
    const { props } = this
    return (

        {props.children}

    )
  }
}

// ===================================================================

const TableSelect = [
  provideState({
    effects: {
      onChange: (_, tdId, add) => (_, { value, onChange, options }) => {
        let newValue = [...value]
        const index = sortedIndex(newValue, tdId)
        if (add) {
          newValue[index] !== tdId && newValue.splice(index, 0, tdId)
        } else if (newValue[index] === tdId) {
          if (newValue.length > 1) {
            newValue.splice(index, 1)
          } else {
            newValue = [options[0][0]]
          }
        }
        onChange(newValue)
      },
      selectAll: () => ({ optionsValues }, { onChange }) => {
        onChange(optionsValues)
      },
    },
    computed: {
      optionsValues: (_, { options }) => flatten(options),
    },
  }),
  injectState,
  ({ state, effects, labelId, options, optionRenderer = identity, value }) => {
    let k = 0
    return (

            {map(options, (line, i) => (

                {map(line, tdOption => (
                  <ToggleTd
                    children={optionRenderer(tdOption)}
                    tdId={tdOption}
                    key={tdOption}
                    onChange={effects.onChange}
                    value={
                      k < value.length && value[k] === tdOption && (++k, true)
                    }
                  />
                ))}

            ))}

        <Button className='pull-right' onClick={effects.selectAll}>
          {_(`selectTableAll${labelId}`)}
        </Button>

    )
  },
].reduceRight((value, decorator) => decorator(value))

TableSelect.propTypes = {
  labelId: PropTypes.string.isRequired,
  options: PropTypes.array.isRequired,
  optionRenderer: PropTypes.func,
  onChange: PropTypes.func.isRequired,
  value: PropTypes.array.isRequired,
}

// ===================================================================

const TimePicker = [
  provideState({
    effects: {
      onChange: (_, value) => ({ optionsValues }, { onChange }) => {
        if (isArray(value)) {
          value = value.length === optionsValues.length ? '*' : value.join(',')
        } else {
          value = `*/${value}`
        }

        onChange(value)
      },
    },
    computed: {
      maxStep: ({ optionsValues }) => Math.floor(optionsValues.length / 2),
      optionsValues: (_, { options }) => flatten(options),

      // '*' or '*/1' => all values
      // '2,7' => [2,7]
      // '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]
      tableValue: ({ optionsValues, step }, { value }) =>
        step === 1
          ? optionsValues
          : step !== undefined
            ? optionsValues.filter((_, i) => i % step === 0)
            : value.split(',').map(Number),

      // '*' => 1
      // '*/2' => 2
      // otherwise => undefined
      step: (_, { value }) =>
        value === '*'
          ? 1
          : value.indexOf('/') === 1
            ? +value.split('/')[1]
            : undefined,
    },
  }),
  injectState,
  ({ state, effects, ...props }) => (
    <Card>
      <CardHeader>
        {_(`scheduling${props.labelId}`)}
        {props.headerAddon}
      </CardHeader>
      <CardBlock>
        <TableSelect
          labelId={props.labelId}
          onChange={effects.onChange}
          optionRenderer={props.optionRenderer}
          options={props.options}
          value={state.tableValue}
        />
        <Range
          max={state.maxStep}
          min={1}
          onChange={effects.onChange}
          value={state.step}
        />
      </CardBlock>
    </Card>
  ),
].reduceRight((value, decorator) => decorator(value))

TimePicker.propTypes = {
  headerAddon: PropTypes.node,
  labelId: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  optionRenderer: PropTypes.func,
  options: PropTypes.array.isRequired,
  value: PropTypes.string.isRequired,
}

const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
  if (monthDayPattern === '*' && weekDayPattern === '*') {
    return
  }

  return weekDayPattern !== '*'
}

class DayPicker extends Component {
  static propTypes = {
    monthDayPattern: PropTypes.string.isRequired,
    weekDayPattern: PropTypes.string.isRequired,
  }

  state = {
    weekDayMode: isWeekDayMode(this.props),
  }

  componentWillReceiveProps (props) {
    const weekDayMode = isWeekDayMode(props)

    if (weekDayMode !== undefined) {
      this.setState({ weekDayMode })
    }
  }

  _setWeekDayMode = weekDayMode => {
    this.props.onChange(['*', '*'])
    this.setState({ weekDayMode })
  }

  _onChange = cron => {
    this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])
  }

  render () {
    const { monthDayPattern, weekDayPattern } = this.props
    const { weekDayMode } = this.state

    const dayModeToggle = (
      <Tooltip
        content={_(
          weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode'
        )}
      >

          <Toggle
            onChange={this._setWeekDayMode}
            iconSize={1}
            value={weekDayMode}
          />

      </Tooltip>
    )

    return (
      <TimePicker
        headerAddon={dayModeToggle}
        key={weekDayMode ? 'week' : 'month'}
        labelId='Day'
        onChange={this._onChange}
        optionRenderer={weekDayMode ? getDayName : undefined}
        options={weekDayMode ? WEEK_DAYS : DAYS}
        value={weekDayMode ? weekDayPattern : monthDayPattern}
      />
    )
  }
}

// ===================================================================

export default class Scheduler extends Component {
  static propTypes = {
    cronPattern: PropTypes.string,
    onChange: PropTypes.func,
    timezone: PropTypes.string,
    value: PropTypes.shape({
      cronPattern: PropTypes.string.isRequired,
      timezone: PropTypes.string,
    }),
  }

  constructor (props) {
    super(props)

    this._onCronChange = newCrons => {
      const cronPattern = this._getCronPattern().split(' ')
      forEach(newCrons, (cron, unit) => {
        cronPattern[PICKTIME_TO_ID[unit]] = cron
      })

      this.props.onChange({
        cronPattern: cronPattern.join(' '),
        timezone: this._getTimezone(),
      })
    }

    forEach(UNITS, unit => {
      this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
    })
    this._dayChange = ([monthDay, weekDay]) =>
      this._onCronChange({ monthDay, weekDay })
  }

  _onTimezoneChange = timezone => {
    this.props.onChange({
      cronPattern: this._getCronPattern(),
      timezone,
    })
  }

  _getCronPattern = () => {
    const { value, cronPattern = value.cronPattern } = this.props
    return cronPattern
  }

  _getTimezone = () => {
    const { value, timezone = value && value.timezone } = this.props
    return timezone
  }

  render () {
    const cronPatternArr = this._getCronPattern().split(' ')
    const timezone = this._getTimezone()

    return (

        <Row>
          <Col largeSize={6}>
            <TimePicker
              labelId='Month'
              optionRenderer={getMonthName}
              options={MONTHS}
              onChange={this._monthChange}
              value={cronPatternArr[PICKTIME_TO_ID['month']]}
            />
          </Col>
          <Col largeSize={6}>
            <DayPicker
              onChange={this._dayChange}
              monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
              weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
            />
          </Col>
        </Row>
        <Row>
          <Col largeSize={6}>
            <TimePicker
              labelId='Hour'
              options={HOURS}
              onChange={this._hourChange}
              value={cronPatternArr[PICKTIME_TO_ID['hour']]}
            />
          </Col>
          <Col largeSize={6}>
            <TimePicker
              labelId='Minute'
              options={MINS}
              onChange={this._minuteChange}
              value={cronPatternArr[PICKTIME_TO_ID['minute']]}
            />
          </Col>
        </Row>
        <Row>
          <Col>

            <TimezonePicker
              value={timezone}
              onChange={this._onTimezoneChange}
            />
          </Col>
        </Row>

    )
  }
}
<|endoftext|>


<commit_before>var CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency");
var ConcatSource = require("webpack/lib/ConcatSource");
var GlobalizeCompilerHelper = require("./GlobalizeCompilerHelper");
var ModuleFilenameHelpers = require("webpack/lib/ModuleFilenameHelpers");
var MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin");
var NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin");
var SkipAMDPlugin = require("skip-amd-webpack-plugin");
var util = require("./util");

/**
 * Production Mode:
 * - Have Globalize modules replaced with their runtime modules.
 * - Statically extracts formatters and parsers from user code and pre-compile
 *   them into globalize-compiled-data chunks.
 */
function ProductionModePlugin(attributes) {
  this.cldr = attributes.cldr || util.cldr;
  this.developmentLocale = attributes.developmentLocale;
  this.messages = attributes.messages && attributes.supportedLocales.reduce(function(sum, locale) {
    sum[locale] = util.readMessages(attributes.messages, locale) || {};
    return sum;
  }, {});
  this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter);
  this.supportedLocales = attributes.supportedLocales;
  this.output = attributes.output;
  this.tmpdir = util.tmpdir();
}

ProductionModePlugin.prototype.apply = function(compiler) {
  var globalizeSkipAMDPlugin;
  var cldr = this.cldr;
  var developmentLocale = this.developmentLocale;
  var moduleFilter = this.moduleFilter;
  var messages = this.messages;
  var supportedLocales = this.supportedLocales;
  var output = this.output || "i18n-[locale].js";

  var globalizeCompilerHelper = new GlobalizeCompilerHelper({
    cldr: cldr,
    developmentLocale: developmentLocale,
    messages: messages,
    tmpdir: this.tmpdir,
    webpackCompiler: compiler
  });

  compiler.apply(
    // Skip AMD part of Globalize Runtime UMD wrapper.
    globalizeSkipAMDPlugin = new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/),

    // Replaces `require("globalize")` with `require("globalize/dist/globalize-runtime")`.
    new NormalModuleReplacementPlugin(/(^|[\/\\])globalize$/, "globalize/dist/globalize-runtime"),

    // Skip AMD part of Globalize Runtime UMD wrapper.
    new SkipAMDPlugin(/(^|[\/\\])globalize-runtime($|[\/\\])/)
  );

  // Map each AST and its request filepath.
  compiler.parser.plugin("program", function(ast) {
    globalizeCompilerHelper.setAst(this.state.current.request, ast);
  });

  // "Intercepts" all `require("globalize")` by transforming them into a
  // `require` to our custom precompiled formatters/parsers, which in turn
  // requires Globalize, set the default locale and then exports the
  // Globalize object.
  compiler.parser.plugin("call require:commonjs:item", function(expr, param) {
    var request = this.state.current.request;
    if(param.isString() && param.string === "globalize" && moduleFilter(request) &&
          !(globalizeCompilerHelper.isCompiledDataModule(request))) {
      var dep;

      // Statically extract Globalize formatters and parsers from the request
      // file only. Then, create a custom precompiled formatters/parsers module
      // that will be called instead of Globalize, which in turn requires
      // Globalize, set the default locale and then exports the Globalize
      // object.
      var compiledDataFilepath = globalizeCompilerHelper.createCompiledDataModule(request);

      // Skip the AMD part of the custom precompiled formatters/parsers UMD
      // wrapper.
      //
      // Note: We're hacking an already created SkipAMDPlugin instance instead
      // of using a regular code like the below in order to take advantage of
      // its position in the plugins list. Otherwise, it'd be too late to plugin
      // and AMD would no longer be skipped at this point.
      //
      // compiler.apply(new SkipAMDPlugin(new RegExp(compiledDataFilepath));
      //
      // 1: Removes the leading and the trailing `/` from the regexp string.
      globalizeSkipAMDPlugin.requestRegExp = new RegExp([
        globalizeSkipAMDPlugin.requestRegExp.toString().slice(1, -1)/* 1 */,
        util.escapeRegex(compiledDataFilepath)
      ].join("|"));

      // Replace require("globalize") with require(<custom precompiled module>).
      dep = new CommonJsRequireDependency(compiledDataFilepath, param.range);
      dep.loc = expr.loc;
      dep.optional = !!this.scope.inTry;
      this.state.current.addDependency(dep);

      return true;
    }
  });

  // Create globalize-compiled-data chunks for the supportedLocales.
  compiler.plugin("entry-option", function(context) {
    supportedLocales.forEach(function(locale) {
      compiler.apply(new MultiEntryPlugin(context, [], "globalize-compiled-data-" + locale ));
    });
  });

  // Place the Globalize compiled data modules into the globalize-compiled-data
  // chunks.
  //
  // Note that, at this point, all compiled data have been compiled for
  // developmentLocale. All globalize-compiled-data chunks will equally include all
  // precompiled modules for the developmentLocale instead of their respective
  // locales. This will get fixed in the subsquent step.
  compiler.plugin("this-compilation", function(compilation) {
    compilation.plugin("after-optimize-chunks", function(chunks) {
      var hasAnyModuleBeenIncluded;
      var compiledDataChunks = chunks.filter(function(chunk) {
        return /globalize-compiled-data/.test(chunk.name);
      });
      chunks.forEach(function(chunk) {
        chunk.modules.forEach(function(module) {
          if (globalizeCompilerHelper.isCompiledDataModule(module.request)) {
            hasAnyModuleBeenIncluded = true;
            module.removeChunk(chunk);
            compiledDataChunks.forEach(function(compiledDataChunk) {
              compiledDataChunk.addModule(module);
              module.addChunk(compiledDataChunk);
            });
          }
        });
      });
      compiledDataChunks.forEach(function(chunk) {
        var locale = chunk.name.replace("globalize-compiled-data-", "");
        chunk.filenameTemplate = output.replace("[locale]", locale);
      });
      if(!hasAnyModuleBeenIncluded) {
        console.warn("No Globalize compiled data module found");
      }
    });

    // Have each globalize-compiled-data chunks to include precompiled data for
    // each supportedLocales. On each chunk, merge all the precompiled modules
    // into a single one. Finally, allow the chunks to be loaded incrementally
    // (not mutually exclusively). Details below.
    //
    // Up to this step, all globalize-compiled-data chunks include several
    // precompiled modules, which have been mandatory to allow webpack to figure
    // out the Globalize runtime dependencies. But, for the final chunk we need
    // something a little different:
    //
    // a) Instead of including several individual precompiled modules, it's
    //    better (i.e., reduced size due to less boilerplate and due to deduped
    //    formatters and parsers) having one single precompiled module for all
    //    these individual modules.
    //
    // b) globalize-compiled-data chunks shouldn't be mutually exclusive to each
    //    other, but users should be able to load two or more of these chunks
    //    and be able to switch from one locale to another dynamically during
    //    runtime.
    //
    //    Some background: by having each individual precompiled module defining
    //    the formatters and parsers for its individual parents, what happens is
    //    that each parent will load the globalize precompiled data by its id
    //    with __webpack_require__(id). These ids are equally defined by the
    //    globalize-compiled-data chunks (each chunk including data for a
    //    certain locale). When one chunk is loaded, these ids get defined by
    //    webpack. When a second chunk is loaded, these ids would get
    //    overwritten.
    //
    //    Therefore, instead of having each individual precompiled module
    //    defining the formatters and parsers for its individual parents, we
    //    actually simplify them by returning Globalize only. The precompiled
    //    content for the whole set of formatters and parsers are going to be
    //    included in the id numbered 0 of these chunks. The id 0 has a special
    //    meaning in webpack, it means it's going to be executed as soon as it's
    //    loaded. So, we accomplish what we need: have the data loaded as soon
    //    as the chunk is loaded, which means it will be available when each
    //    individual parent code needs it.
    //
    // OBS: `additional-chunk-assets` is used to make globalize-compiled-data
    // changes right before `optimize-chunk-assets` (used by plugins like
    // Uglify).
    //
    // TODO: Can we do this differently than using regexps?
    compilation.plugin("additional-chunk-assets", function(chunks) {
      var globalizeModuleIds = [];
      var globalizeModuleIdsMap = {};

      chunks.forEach(function(chunk) {
        chunk.modules.forEach(function(module) {
          var aux;
          var request = module.request;
          if (request && util.isGlobalizeRuntimeModule(request)) {
            // While request has the full pathname, aux has something like "globalize/dist/globalize-runtime/date".
            aux = request.split(/[\/\\]/);
            aux = aux.slice(aux.lastIndexOf("globalize")).join("/").replace(/\.js$/, "");
            globalizeModuleIds.push(module.id);
            globalizeModuleIdsMap[aux] = module.id;
          }
        });
      });

      chunks.filter(function(chunk) {
        return /globalize-compiled-data/.test(chunk.name);
      }).forEach(function(chunk) {
        var locale = chunk.name.replace("globalize-compiled-data-", "");
        chunk.files.filter(ModuleFilenameHelpers.matchObject).forEach(function(file) {
          var isFirst = true;
          var source = compilation.assets[file].source().replace(/\n\/\*\*\*\/ function\(module, exports(, __webpack_require__)?\) {[\s\S]*?(\n\/\*\*\*\/ })/g, function(garbage1, garbage2, fnTail) {
            var fnContent;

            // Define the initial module 0 as the whole formatters and parsers.
            if (isFirst) {
              isFirst = false;
              fnContent = globalizeCompilerHelper.compile(locale)
                .replace("typeof define === \"function\" && define.amd", "false")
                .replace(/require\("([^)]+)"\)/g, function(garbage, moduleName) {
                  return "__webpack_require__(" + globalizeModuleIdsMap[moduleName] + ")";
                });

            // Define all other individual globalize compiled data as a simple exports to Globalize.
            } else {
              fnContent = "module.exports = __webpack_require__(" + globalizeModuleIds[0] + ");";
            }

            return "\n/***/ function(module, exports, __webpack_require__) {\n" + fnContent + fnTail;
          });
          compilation.assets[file] = new ConcatSource(source);
        });
      });
    });

    // Set the right chunks order. The globalize-compiled-data chunks must
    // appear after globalize runtime modules, but before any app code.
    compilation.plugin("optimize-chunk-order", function(chunks) {
      var cachedChunkScore = {};
      function moduleScore(module) {
        if (module.request && util.isGlobalizeRuntimeModule(module.request)) {
          return 1;
        } else if (module.request && globalizeCompilerHelper.isCompiledDataModule(module.request)) {
          return 0;
        }
        return -1;
      }
      function chunkScore(chunk) {
        if (!cachedChunkScore[chunk.name]) {
          cachedChunkScore[chunk.name] = chunk.modules.reduce(function(sum, module) {
            var score = moduleScore(module);
            return Math.max(sum, score);
          }, -1);
        }
        return cachedChunkScore[chunk.name];
      }
      chunks.sort(function(a, b) {
        return chunkScore(a) - chunkScore(b);
      });
    });
  });
};

module.exports = ProductionModePlugin;
<commit_msg>Fix a production bug where i18n compiled data ends up in app chunk<commit_after>var CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency");
var ConcatSource = require("webpack/lib/ConcatSource");
var GlobalizeCompilerHelper = require("./GlobalizeCompilerHelper");
var ModuleFilenameHelpers = require("webpack/lib/ModuleFilenameHelpers");
var MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin");
var NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin");
var SkipAMDPlugin = require("skip-amd-webpack-plugin");
var util = require("./util");

/**
 * Production Mode:
 * - Have Globalize modules replaced with their runtime modules.
 * - Statically extracts formatters and parsers from user code and pre-compile
 *   them into globalize-compiled-data chunks.
 */
function ProductionModePlugin(attributes) {
  this.cldr = attributes.cldr || util.cldr;
  this.developmentLocale = attributes.developmentLocale;
  this.messages = attributes.messages && attributes.supportedLocales.reduce(function(sum, locale) {
    sum[locale] = util.readMessages(attributes.messages, locale) || {};
    return sum;
  }, {});
  this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter);
  this.supportedLocales = attributes.supportedLocales;
  this.output = attributes.output;
  this.tmpdir = util.tmpdir();
}

ProductionModePlugin.prototype.apply = function(compiler) {
  var globalizeSkipAMDPlugin;
  var cldr = this.cldr;
  var developmentLocale = this.developmentLocale;
  var moduleFilter = this.moduleFilter;
  var messages = this.messages;
  var supportedLocales = this.supportedLocales;
  var output = this.output || "i18n-[locale].js";

  var globalizeCompilerHelper = new GlobalizeCompilerHelper({
    cldr: cldr,
    developmentLocale: developmentLocale,
    messages: messages,
    tmpdir: this.tmpdir,
    webpackCompiler: compiler
  });

  compiler.apply(
    // Skip AMD part of Globalize Runtime UMD wrapper.
    globalizeSkipAMDPlugin = new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/),

    // Replaces `require("globalize")` with `require("globalize/dist/globalize-runtime")`.
    new NormalModuleReplacementPlugin(/(^|[\/\\])globalize$/, "globalize/dist/globalize-runtime"),

    // Skip AMD part of Globalize Runtime UMD wrapper.
    new SkipAMDPlugin(/(^|[\/\\])globalize-runtime($|[\/\\])/)
  );

  // Map each AST and its request filepath.
  compiler.parser.plugin("program", function(ast) {
    globalizeCompilerHelper.setAst(this.state.current.request, ast);
  });

  // "Intercepts" all `require("globalize")` by transforming them into a
  // `require` to our custom precompiled formatters/parsers, which in turn
  // requires Globalize, set the default locale and then exports the
  // Globalize object.
  compiler.parser.plugin("call require:commonjs:item", function(expr, param) {
    var request = this.state.current.request;
    if(param.isString() && param.string === "globalize" && moduleFilter(request) &&
          !(globalizeCompilerHelper.isCompiledDataModule(request))) {
      var dep;

      // Statically extract Globalize formatters and parsers from the request
      // file only. Then, create a custom precompiled formatters/parsers module
      // that will be called instead of Globalize, which in turn requires
      // Globalize, set the default locale and then exports the Globalize
      // object.
      var compiledDataFilepath = globalizeCompilerHelper.createCompiledDataModule(request);

      // Skip the AMD part of the custom precompiled formatters/parsers UMD
      // wrapper.
      //
      // Note: We're hacking an already created SkipAMDPlugin instance instead
      // of using a regular code like the below in order to take advantage of
      // its position in the plugins list. Otherwise, it'd be too late to plugin
      // and AMD would no longer be skipped at this point.
      //
      // compiler.apply(new SkipAMDPlugin(new RegExp(compiledDataFilepath));
      //
      // 1: Removes the leading and the trailing `/` from the regexp string.
      globalizeSkipAMDPlugin.requestRegExp = new RegExp([
        globalizeSkipAMDPlugin.requestRegExp.toString().slice(1, -1)/* 1 */,
        util.escapeRegex(compiledDataFilepath)
      ].join("|"));

      // Replace require("globalize") with require(<custom precompiled module>).
      dep = new CommonJsRequireDependency(compiledDataFilepath, param.range);
      dep.loc = expr.loc;
      dep.optional = !!this.scope.inTry;
      this.state.current.addDependency(dep);

      return true;
    }
  });

  // Create globalize-compiled-data chunks for the supportedLocales.
  compiler.plugin("entry-option", function(context) {
    supportedLocales.forEach(function(locale) {
      compiler.apply(new MultiEntryPlugin(context, [], "globalize-compiled-data-" + locale ));
    });
  });

  // Place the Globalize compiled data modules into the globalize-compiled-data
  // chunks.
  //
  // Note that, at this point, all compiled data have been compiled for
  // developmentLocale. All globalize-compiled-data chunks will equally include all
  // precompiled modules for the developmentLocale instead of their respective
  // locales. This will get fixed in the subsquent step.
  var allModules;
  compiler.plugin("this-compilation", function(compilation) {
    compilation.plugin("optimize-modules", function(modules) {
      allModules = modules;
    });
  });
  compiler.plugin("this-compilation", function(compilation) {
    compilation.plugin("after-optimize-chunks", function(chunks) {
      var hasAnyModuleBeenIncluded;
      var compiledDataChunks = chunks.filter(function(chunk) {
        return /globalize-compiled-data/.test(chunk.name);
      });
      allModules.forEach(function(module) {
        if (globalizeCompilerHelper.isCompiledDataModule(module.request)) {
          hasAnyModuleBeenIncluded = true;
          module.chunks.forEach(function(chunk) {
            module.removeChunk(chunk);
          });
          compiledDataChunks.forEach(function(compiledDataChunk) {
            compiledDataChunk.addModule(module);
            module.addChunk(compiledDataChunk);
          });
        }
      });
      compiledDataChunks.forEach(function(chunk) {
        var locale = chunk.name.replace("globalize-compiled-data-", "");
        chunk.filenameTemplate = output.replace("[locale]", locale);
      });
      if(!hasAnyModuleBeenIncluded) {
        console.warn("No Globalize compiled data module found");
      }
    });

    // Have each globalize-compiled-data chunks to include precompiled data for
    // each supportedLocales. On each chunk, merge all the precompiled modules
    // into a single one. Finally, allow the chunks to be loaded incrementally
    // (not mutually exclusively). Details below.
    //
    // Up to this step, all globalize-compiled-data chunks include several
    // precompiled modules, which have been mandatory to allow webpack to figure
    // out the Globalize runtime dependencies. But, for the final chunk we need
    // something a little different:
    //
    // a) Instead of including several individual precompiled modules, it's
    //    better (i.e., reduced size due to less boilerplate and due to deduped
    //    formatters and parsers) having one single precompiled module for all
    //    these individual modules.
    //
    // b) globalize-compiled-data chunks shouldn't be mutually exclusive to each
    //    other, but users should be able to load two or more of these chunks
    //    and be able to switch from one locale to another dynamically during
    //    runtime.
    //
    //    Some background: by having each individual precompiled module defining
    //    the formatters and parsers for its individual parents, what happens is
    //    that each parent will load the globalize precompiled data by its id
    //    with __webpack_require__(id). These ids are equally defined by the
    //    globalize-compiled-data chunks (each chunk including data for a
    //    certain locale). When one chunk is loaded, these ids get defined by
    //    webpack. When a second chunk is loaded, these ids would get
    //    overwritten.
    //
    //    Therefore, instead of having each individual precompiled module
    //    defining the formatters and parsers for its individual parents, we
    //    actually simplify them by returning Globalize only. The precompiled
    //    content for the whole set of formatters and parsers are going to be
    //    included in the id numbered 0 of these chunks. The id 0 has a special
    //    meaning in webpack, it means it's going to be executed as soon as it's
    //    loaded. So, we accomplish what we need: have the data loaded as soon
    //    as the chunk is loaded, which means it will be available when each
    //    individual parent code needs it.
    //
    // OBS: `additional-chunk-assets` is used to make globalize-compiled-data
    // changes right before `optimize-chunk-assets` (used by plugins like
    // Uglify).
    //
    // TODO: Can we do this differently than using regexps?
    compilation.plugin("additional-chunk-assets", function(chunks) {
      var globalizeModuleIds = [];
      var globalizeModuleIdsMap = {};

      chunks.forEach(function(chunk) {
        chunk.modules.forEach(function(module) {
          var aux;
          var request = module.request;
          if (request && util.isGlobalizeRuntimeModule(request)) {
            // While request has the full pathname, aux has something like "globalize/dist/globalize-runtime/date".
            aux = request.split(/[\/\\]/);
            aux = aux.slice(aux.lastIndexOf("globalize")).join("/").replace(/\.js$/, "");
            globalizeModuleIds.push(module.id);
            globalizeModuleIdsMap[aux] = module.id;
          }
        });
      });

      chunks.filter(function(chunk) {
        return /globalize-compiled-data/.test(chunk.name);
      }).forEach(function(chunk) {
        var locale = chunk.name.replace("globalize-compiled-data-", "");
        chunk.files.filter(ModuleFilenameHelpers.matchObject).forEach(function(file) {
          var isFirst = true;
          var source = compilation.assets[file].source().replace(/\n\/\*\*\*\/ function\(module, exports(, __webpack_require__)?\) {[\s\S]*?(\n\/\*\*\*\/ })/g, function(garbage1, garbage2, fnTail) {
            var fnContent;

            // Define the initial module 0 as the whole formatters and parsers.
            if (isFirst) {
              isFirst = false;
              fnContent = globalizeCompilerHelper.compile(locale)
                .replace("typeof define === \"function\" && define.amd", "false")
                .replace(/require\("([^)]+)"\)/g, function(garbage, moduleName) {
                  return "__webpack_require__(" + globalizeModuleIdsMap[moduleName] + ")";
                });

            // Define all other individual globalize compiled data as a simple exports to Globalize.
            } else {
              fnContent = "module.exports = __webpack_require__(" + globalizeModuleIds[0] + ");";
            }

            return "\n/***/ function(module, exports, __webpack_require__) {\n" + fnContent + fnTail;
          });
          compilation.assets[file] = new ConcatSource(source);
        });
      });
    });

    // Set the right chunks order. The globalize-compiled-data chunks must
    // appear after globalize runtime modules, but before any app code.
    compilation.plugin("optimize-chunk-order", function(chunks) {
      var cachedChunkScore = {};
      function moduleScore(module) {
        if (module.request && util.isGlobalizeRuntimeModule(module.request)) {
          return 1;
        } else if (module.request && globalizeCompilerHelper.isCompiledDataModule(module.request)) {
          return 0;
        }
        return -1;
      }
      function chunkScore(chunk) {
        if (!cachedChunkScore[chunk.name]) {
          cachedChunkScore[chunk.name] = chunk.modules.reduce(function(sum, module) {
            var score = moduleScore(module);
            return Math.max(sum, score);
          }, -1);
        }
        return cachedChunkScore[chunk.name];
      }
      chunks.sort(function(a, b) {
        return chunkScore(a) - chunkScore(b);
      });
    });
  });
};

module.exports = ProductionModePlugin;
<|endoftext|>


<commit_before>process.on('uncaughtException', e => {
    e = e || {}
    const appRoot = new RegExp(path.join(__dirname, '../../'), 'g')

    if (e.message) {
        process.stderr.write('\u001b[131m[Error]\u001b[031m' + e.message + '\n\u001b[m\n')
    }
    if (e.stack) {
        process.stderr.write('\u001b[131m[Stack]\u001b[031m' + (e.stack.replace(appRoot, '')) + '\u001b[m\n')
    }
})

import fs  from 'fs-promise'
import path  from 'path'
import yargs from 'yargs'
import  {BrowserWindow, Menu, app} from 'electron'

const parseCommandLine = () => {
    const parser = yargs
        .boolean('dev').describe('dev', 'Run development mode')
        .boolean('help').describe('help', 'Show command line help').alias('help', 'h')
        .boolean('version').describe('version', 'Show version').alias('version', 'v')

    const args = parser.parse(process.argv.slice(1))

    if (args.help) {
        args.showHelp('error')
        process.exit(0);
    }
    if (args.version) {
        console.log(`Delir ${app.getVersion()}`);
        process.exit(0);
    }

    return {
        devMode: args['dev']
    }
}

const install = async () => {
    const userDir = app.getPath('appData')

    try {
        await fs.mkdir(path.join(userDir, 'delir'))
    } catch (e) {
        if (e.code !== 'EEXIST') {
            throw e
        }
    }

    try {
        await fs.mkdir(path.join(userDir, 'delir/plugins'))
    } catch (e) {
        if (e.code !== 'EEXIST') {
            throw e
        }
    }
}

(async () => {
    const args = parseCommandLine()
    await install()

    app.on('window-all-closed', function() {
        if (process.platform !== 'darwin') {
            app.quit()
        }
    })

    const run = () => {
        const window = new BrowserWindow({
            // frame: false,
            titleBarStyle: 'hidden',
            // transparent: true,
            webPreferences: {
                webgl: true,
                experimentalFeatures: true,
                experimentalCanvasFeatures: true,
            }
        })

        window.loadURL(`file://${path.join(__dirname, '/../renderer/index.html')}`)
        window.show()
    }

    app.isReady() ? run() : app.on('ready', run)
})().catch(e => console.log(e))
<commit_msg>Fix minssing print help(and now open inspector in devMode)<commit_after>process.on('uncaughtException', e => {
    e = e || {}
    const appRoot = new RegExp(path.join(__dirname, '../../'), 'g')

    if (e.message) {
        process.stderr.write('\u001b[131m[Error]\u001b[031m' + e.message + '\n\u001b[m\n')
    }
    if (e.stack) {
        process.stderr.write('\u001b[131m[Stack]\u001b[031m' + (e.stack.replace(appRoot, '')) + '\u001b[m\n')
    }
})

import fs  from 'fs-promise'
import path  from 'path'
import yargs from 'yargs'
import  {BrowserWindow, Menu, app} from 'electron'

const parseCommandLine = () => {
    const parser = yargs
        .boolean('dev').describe('dev', 'Run development mode')
        .boolean('help').describe('help', 'Show command line help').alias('help', 'h')
        .boolean('version').describe('version', 'Show version').alias('version', 'v')

    const args = parser.parse(process.argv.slice(1))

    if (args.help) {
        parser.showHelp('error')
        process.exit(0);
    }
    if (args.version) {
        console.log(`Delir ${app.getVersion()}`);
        process.exit(0);
    }

    return {
        devMode: args['dev']
    }
}

const install = async () => {
    const userDir = app.getPath('appData')

    try {
        await fs.mkdir(path.join(userDir, 'delir'))
    } catch (e) {
        if (e.code !== 'EEXIST') {
            throw e
        }
    }

    try {
        await fs.mkdir(path.join(userDir, 'delir/plugins'))
    } catch (e) {
        if (e.code !== 'EEXIST') {
            throw e
        }
    }
}

(async () => {
    const args = parseCommandLine()
    await install()

    args.devMode && console.log('Run as develop mode')

    app.on('window-all-closed', function() {
        if (process.platform !== 'darwin') {
            app.quit()
        }
    })

    const run = () => {
        const window = new BrowserWindow({
            // frame: false,
            titleBarStyle: 'hidden',
            // transparent: true,
            webPreferences: {
                webgl: true,
                experimentalFeatures: true,
                experimentalCanvasFeatures: true,
            },
        })

        window.loadURL(`file://${path.join(__dirname, '/../renderer/index.html')}`)
        window.show()
        args.devMode && window.webContents.openDevTools()
    }

    app.isReady() ? run() : app.on('ready', run)
})().catch(e => console.log(e))
<|endoftext|>


<commit_before>import * as PhaserObjects from './phaserObjects';

/** Group with some added functionality for text overlays.
 * @private
 * @extends Phaser.Group
 */
class TextGroup extends PhaserObjects.Group {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the sprite will be placed.
     * @param {number} y - The y coordinate on screen where the sprite will be placed.
     */
    constructor(game, x, y) {
        super(game, x, y);

        this.game = game;
        this.x = x;
        this.y = y;

        game.add.existing(this);
    }

    /**
     * @param {string} label - The text to place on top of the sprite.
     * @param {Object} style - The style properties to be set on the Text.
     */
    setText(label, style) {
        if (this.text) {
            this.text.destroy();
        }

        if (this.version === 3) {
            this.text = this.game.add.text(0, 0, label, style);
            this.text.setOrigin(0.5, 0.5);
        } else {
            this.text = this.game.add.text(this.width / 2, this.height / 2, label, style);
            this.text.anchor.set(0.5, 0.5);
        }

        this.add(this.text);

        this.text.wordWrap = true;
        this.text.wordWrapWidth = this.width;

        return this;
    }

    /** When setOrigin is called, align the text as well. */
    setOrigin(x, y) {
        if (this.version === 3) {
            this.sprite.setOrigin(x, y);
        } else {
            this.sprite.anchor.set(x, y);
        }

        this.text.x = this.sprite.width / 2;
        this.text.y = this.sprite.height / 2;

        return this;
    }
}

/** Sprite with text added as a child.
 * @extends Phaser.Group
 */
export class TextSprite extends TextGroup {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the textSprite will be placed.
     * @param {number} y - The y coordinate on screen where the textSprite will be placed.
     * @param {string} key - The image to create a sprite with.
     */
    constructor(game, x, y, key) {
        super(game, x, y);

        this.sprite = new PhaserObjects.Sprite(game, 0, 0, key);
        this.add(this.sprite);

        this.width = this.sprite.width;
        this.height = this.sprite.height;
    }
}

/** Phaser Group containing a button with text anchored in the button's center.
 * @extends Phaser.Group
 */
export class TextButton extends TextGroup {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the textSprite will be placed.
     * @param {number} y - The y coordinate on screen where the textSprite will be placed.
     * @param {string} key - The image to create a sprite with.
     * @param {Object} callback - Callback to use when the button is clicked.
     * @param {Object} callbackContext - The context the callback is called in.
     * @param {number} overKey - The frame to switch to when an over event is triggered.
     * @param {number} outKey - The frame to switch to when an out event is triggered.
     * @param {number} downKey - The frame to switch to when a down event is triggered.
     * @param {number} upKey - The frame to switch to when an up event is triggered.
     */
    constructor(game, x, y, key, callback, callbackContext, overKey, outKey, downKey, upKey) {
        super(game, x, y);

        this.sprite = new PhaserObjects.Button(
            game,
            0,
            0,
            key,
            callback,
            callbackContext,
            overKey,
            outKey,
            downKey,
            upKey,
        );
        this.add(this.sprite);

        this.width = this.sprite.width;
        this.height = this.sprite.height;
    }

    /** Adds an adjustment to the text on down/up events. */
    eventTextYAdjustment(number) {
        const startY = this.text.y;

        this.sprite.addDownEvent(() => { this.text.y += number; });
        this.sprite.addUpEvent(() => { this.text.y = startY; });

        // A pointerout event should reset the text position too.
        this.sprite.addOutEvent(() => { this.text.y = startY; });

        return this;
    }
}
<commit_msg>Phaser 3: Fix button text alignment<commit_after>import * as PhaserObjects from './phaserObjects';

/** Group with some added functionality for text overlays.
 * @private
 * @extends Phaser.Group
 */
class TextGroup extends PhaserObjects.Group {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the sprite will be placed.
     * @param {number} y - The y coordinate on screen where the sprite will be placed.
     */
    constructor(game, x, y) {
        super(game, x, y);

        this.game = game;
        this.x = x;
        this.y = y;

        game.add.existing(this);
    }

    /**
     * @param {string} label - The text to place on top of the sprite.
     * @param {Object} style - The style properties to be set on the Text.
     */
    setText(label, style) {
        if (this.text) {
            this.text.destroy();
        }

        if (this.version === 3) {
            this.text = this.game.add.text(0, 0, label, style);
            this.text.setOrigin(0.5, 0.5);
        } else {
            this.text = this.game.add.text(this.width / 2, this.height / 2, label, style);
            this.text.anchor.set(0.5, 0.5);
        }

        this.add(this.text);

        this.text.wordWrap = true;
        this.text.wordWrapWidth = this.width;

        return this;
    }

    /** When setOrigin is called, align the text as well. */
    setOrigin(x, y) {
        if (this.version === 3) {
            this.sprite.setOrigin(x, y);
        } else {
            this.sprite.anchor.set(x, y);
        }

        this.text.x = this.sprite.width / 2;
        this.text.y = this.sprite.height / 2;
        this.currentTextY = this.sprite.height / 2;

        return this;
    }
}

/** Sprite with text added as a child.
 * @extends Phaser.Group
 */
export class TextSprite extends TextGroup {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the textSprite will be placed.
     * @param {number} y - The y coordinate on screen where the textSprite will be placed.
     * @param {string} key - The image to create a sprite with.
     */
    constructor(game, x, y, key) {
        super(game, x, y);

        this.sprite = new PhaserObjects.Sprite(game, 0, 0, key);
        this.add(this.sprite);

        this.width = this.sprite.width;
        this.height = this.sprite.height;
    }
}

/** Phaser Group containing a button with text anchored in the button's center.
 * @extends Phaser.Group
 */
export class TextButton extends TextGroup {
    /**
     * @param {Object} game - Current game instance.
     * @param {number} x - The x coordinate on screen where the textSprite will be placed.
     * @param {number} y - The y coordinate on screen where the textSprite will be placed.
     * @param {string} key - The image to create a sprite with.
     * @param {Object} callback - Callback to use when the button is clicked.
     * @param {Object} callbackContext - The context the callback is called in.
     * @param {number} overKey - The frame to switch to when an over event is triggered.
     * @param {number} outKey - The frame to switch to when an out event is triggered.
     * @param {number} downKey - The frame to switch to when a down event is triggered.
     * @param {number} upKey - The frame to switch to when an up event is triggered.
     */
    constructor(game, x, y, key, callback, callbackContext, overKey, outKey, downKey, upKey) {
        super(game, x, y);

        this.sprite = new PhaserObjects.Button(
            game,
            0,
            0,
            key,
            callback,
            callbackContext,
            overKey,
            outKey,
            downKey,
            upKey,
        );
        this.add(this.sprite);

        this.width = this.sprite.width;
        this.height = this.sprite.height;

        this.currentTextY = 0;
    }

    /** Adds an adjustment to the text on down/up events. */
    eventTextYAdjustment(number) {
        this.sprite.addDownEvent(() => { this.text.y += number; });
        this.sprite.addUpEvent(() => { this.text.y = this.currentTextY; });

        // A pointerout event should reset the text position too.
        this.sprite.addOutEvent(() => { this.text.y = this.currentTextY; });

        return this;
    }
}
<|endoftext|>


<commit_before>/* Bubble chart
 *
 * Based on Jim Vallandingham's work
 * Organization and style inspired by:
 * https://bost.ocks.org/mike/chart/
 *
 */

function bubbleChart() {
    /* bubbleChart creation function. Returns a function that will
     * instantiate a new bubble chart given a DOM element to display
     * it in and a dataset to visualize.
    */
    // Constants for sizing
    var width = BUBBLE_PARAMETERS.width;
    var height = BUBBLE_PARAMETERS.height;

    // Tooltip object for mouseover functionality
    var tooltip = floatingTooltip('bubble_chart_tooltip', 240);

    // Locations to move bubbles towards, depending
    // on which view mode is selected.
    var center = { x: width / 2, y: height / 2 };

    // SET COLOURS
    var color_groupsKeys = Object.keys(BUBBLE_PARAMETERS.fill_color.color_groups)
    var color_groupsValues = []
    for (var i=0; i<color_groupsKeys.length; i++) {
        var key = color_groupsKeys[i]
        color_groupsValues.push(BUBBLE_PARAMETERS.fill_color.color_groups[key])
    }

    // Nice looking colors - no reason to buck the trend
    var fillColor = d3.scaleOrdinal()
        .domain(color_groupsKeys)
        .range(color_groupsValues);

    // SCALE BUBBLE SIZES
    // Using ^(0.5), size bubbles based on area instead of radius
    var radiusScale = d3.scalePow()
        .exponent(0.5)
        .range([2, 25]);  // Range between 2 and 50 pixels

    // These will be set in createNodes and create_vis
    var svg = null;
    var bubbles = null;
    var forceSim = null;
    var nodes = [];

    function createNodes(rawData) {
        /*
         * This data manipulation function takes the raw data from
         * the CSV file and converts it into an array of node objects.
         * Each node will store data and visualization values to visualize
         * a bubble.
         *
         * rawData is expected to be an array of data objects, read in from
         * one of d3's loading functions like d3.csv.
         *
         * This function returns the new node array, with a node in that
         * array for each element in the rawData input.
         */
        // Use map() to convert raw data into node data.
        // Checkout http://learnjsdata.com/ for more on
        // working with data.
        var myNodes = rawData.map(function (data_row) {
            node = {
                id: data_row.id,
                scaled_radius: radiusScale(+data_row[BUBBLE_PARAMETERS.radius_field]),
                actual_radius: +data_row[BUBBLE_PARAMETERS.radius_field],
                fill_color_group: data_row[BUBBLE_PARAMETERS.fill_color.data_field],
                // Put each node initially in a random location
                x: Math.random() * BUBBLE_PARAMETERS.width,
                y: Math.random() * BUBBLE_PARAMETERS.height
            };
            for(var key in data_row) {
                // Skip loop if the property is from prototype
                if (!data_row.hasOwnProperty(key)) continue;
                node[key] = data_row[key];
            }

            return node;
        });

        // Sort them to prevent occlusion of smaller nodes.
        myNodes.sort(function (a, b) { return b.actual_radius - a.actual_radius; });

        return myNodes;
    }

    function getTargetFunction(mode) {
        // Given a mode, return an anonymous function that maps nodes to target coordinates
        return function (node) {
            // Given a mode and node, return the correct target
            if(mode.size == 1) {
                target = mode.gridCenters[""];
                //console.log("targets size 1, so target is ", target)
            } else {
                // If the grid size is greater than 1, look up the appropriate target
                // coordinate using the relevant node_tag for the mode we are in
                node_tag = node[mode.dataField]
                target = mode.gridCenters[node_tag];
            }
            return target;
        }
    }

    function showLabels(mode) {
        /*
         * Shows labels for each of the positions in the grid.
         */
        console.log("Show bubble group labels");

        var currentLabels = mode.labels;
        var bubble_group_labels = svg.selectAll('.bubble_group_label')
            .data(currentLabels);

        var grid_element_half_height = BUBBLE_PARAMETERS.height / (mode.gridDimensions.rows * 2);

        bubble_group_labels.enter().append('text')
            .attr('class', 'bubble_group_label')
            .attr('x', function (d) { return mode.gridCenters[d].x; })
            .attr('y', function (d) { return mode.gridCenters[d].y - grid_element_half_height; })
            .attr('text-anchor', 'middle')                // centre the text
            .attr('dominant-baseline', 'hanging') // so the text is immediately below the bounding box, rather than above
            .text(function (d) { return d; });

        // GRIDLINES FOR DEBUGGING PURPOSES
        /*
        var grid_element_half_height = BUBBLE_PARAMETERS.height / (mode.gridDimensions.rows * 2);
        var grid_element_half_width = BUBBLE_PARAMETERS.width / (mode.gridDimensions.columns * 2);

        for (var key in currentMode.gridCenters) {
            if (currentMode.gridCenters.hasOwnProperty(key)) {
                var rectangle = svg.append("rect")
                    .attr("class", "mc_debug")
                    .attr("x", currentMode.gridCenters[key].x - grid_element_half_width)
                    .attr("y", currentMode.gridCenters[key].y - grid_element_half_height)
                    .attr("width", grid_element_half_width*2)
                    .attr("height", grid_element_half_height*2)
                    .attr("stroke", "red")
                    .attr("fill", "none");
                var ellipse = svg.append("ellipse")
                    .attr("class", "mc_debug")
                    .attr("cx", currentMode.gridCenters[key].x)
                    .attr("cy", currentMode.gridCenters[key].y)
                    .attr("rx", 15)
                    .attr("ry", 10);
            }
        }
        */
    }

    function tooltipContent(d) {
        /*
         * Helper function to generate the tooltip content
         *
         * Parameters: d, a dict from the node
         * Returns: a string representing the formatted HTML to display
         */
        var content = ''

        // Loop through all lines we want displayed in the tooltip
        for(var i=0; i<BUBBLE_PARAMETERS.tooltip.length; i++) {
            var cur_tooltip = BUBBLE_PARAMETERS.tooltip[i];
            var value_formatted;

            // If a format was specified, use it
            if ("format_string" in cur_tooltip) {
                value_formatted =
                    d3.format(cur_tooltip.format_string)(d[cur_tooltip.data_field]);
            } else {
                value_formatted = d[cur_tooltip.data_field];
            }

            // If there was a previous tooltip line, add a newline separator
            if (i > 0) {
                content += '
';
            }
            content += ''    + cur_tooltip.title + ': ';
            content += '' + value_formatted     + '';
        }

        return content;
    }

    function showTooltip(d) {
        /*
         * Function called on mouseover to display the
         * details of a bubble in the tooltip.
         */
        // Change the circle's outline to indicate hover state.
        d3.select(this).attr('stroke', 'black');

        // Show the tooltip
        tooltip.showTooltip(tooltipContent(d), d3.event);
    }

    function hideTooltip(d) {
        /*
         * Hide tooltip
         */
        // Reset the circle's outline back to its original color.
        var originalColor = d3.rgb(fillColor(d.fill_color_group)).darker()
        d3.select(this).attr('stroke', originalColor);

        // Hide the tooltip
        tooltip.hideTooltip();
    }

    function ticked() {
        bubbles.each(function (node) {})
            .attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; });
    }
    //////////////////////////////////////////////////////////////

    var chart = function chart(selector, rawData) {
        /*
         * Main entry point to the bubble chart. This function is returned
         * by the parent closure. It prepares the rawData for visualization
         * and adds an svg element to the provided selector and starts the
         * visualization creation process.
         *
         * selector is expected to be a DOM element or CSS selector that
         * points to the parent element of the bubble chart. Inside this
         * element, the code will add the SVG continer for the visualization.
         *
         * rawData is expected to be an array of data objects as provided by
         * a d3 loading function like d3.csv.
         */

        // Use the max radius in the data as the max in the scale's domain
        // (Ensure the radius is a number by converting it with `+`)
        var maxAmount = d3.max(rawData, function (d) { return +d[BUBBLE_PARAMETERS.radius_field]; });
        radiusScale.domain([0, maxAmount]);

        nodes = createNodes(rawData);

        // Create a SVG element inside the provided selector with desired size.
        svg = d3.select(selector)
            .append('svg')
            .attr('width', width)
            .attr('height', height);

        console.log("nodes", nodes);

        // Bind nodes data to what will become DOM elements to represent them.
        svg.selectAll('.bubble')
            .data(nodes, function (d) { return d.id; })
            // Create new circle elements each with class `bubble`.
            // There will be one circle.bubble for each object in the nodes array.
            .enter().append('circle')
            .classed('bubble', true)
            // Initially, their radius (r attribute) will be 0.
            .attr('r', 0)
            .attr('fill', function (d) { return fillColor(d.fill_color_group); })
            .attr('stroke', function (d) { return d3.rgb(fillColor(d.fill_color_group)).darker(); })
            .attr('stroke-width', 2)
            .on('mouseover', showTooltip)
            .on('mouseout', hideTooltip);

        bubbles = d3.selectAll('.bubble');

        // Fancy transition to make bubbles appear, ending with the correct radius
        bubbles.transition()
            .duration(2000)
            .on("start", function (d) {console.log("gah");})
            .attr('r', function (d) { return d.scaled_radius; });

        forceSim = d3.forceSimulation(nodes)
            .velocityDecay(0.1)
            .force("collide", d3.forceCollide().radius(function(d) { return d.scaled_radius + 0.5; }).iterations(3))
            .on("tick", ticked);
    };

    chart.switchMode = function (buttonId) {
        /*
         * Externally accessible function (this is attached to the
         * returned chart function). Allows the visualization to toggle
         * between display modes.
         *
         * buttonId is expected to be a string corresponding to one of the modes.
         */
        // Get data on the new mode we have just switched to
        currentMode = new ViewMode(buttonId)

        // Remove current labels
        label_elements = svg.selectAll('.bubble_group_label').remove();
        // Remove current debugging elements
        debug_elements = svg.selectAll('.mc_debug').remove(); // DEBUG

        // Show labels, if we have more than one category to label
        if (currentMode.size > 1) {
            showLabels(currentMode);
        }

        // Move the bubbles to their new locations

        // Given the mode we are in, obtain the node -> target mapping
        var targetFunction = getTargetFunction(currentMode)

        // Specify the target of the force layout for each of the circles
        forceSim
            .force("x", d3.forceX(function(d) {return targetFunction(d).x}).strength(0.02))
            .force("y", d3.forceY(function(d) {return targetFunction(d).y}).strength(0.02));

        // Restart the force layout simulation
        forceSim.alphaTarget(1).restart();
    };

    // Return the chart function from closure.
    return chart;
}

/////////////////////////////////////////////////////////////////////////////////////
// OBJECT
function ViewMode(button_id) {
    /* ViewMode: an object that has useful parameters for each view mode.
     * initialize it with your desired view mode, then use its parameters.
     * Attributes:
     - mode_index (which button was pressed)
     - buttonId     (which button was pressed)
     - gridDimensions    e.g. {"rows": 10, "columns": 20}
     - gridCenters         e.g. {"group1": {"x": 10, "y": 20}, ...}
     - dataField    (string)
     - labels     (an array)
     - size         (number of groups)
     */
    var width = BUBBLE_PARAMETERS.width;
    var height = BUBBLE_PARAMETERS.height;

    // Find which button was pressed
    var mode_index;
    for(mode_index=0; mode_index<BUBBLE_PARAMETERS.modes.length; mode_index++) {
        if(BUBBLE_PARAMETERS.modes[mode_index].button_id == button_id) {
            break;
        }
    }
    if(mode_index>=BUBBLE_PARAMETERS.modes.length) {
        console.log("Error: can't find mode with button_id = ", button_id)
    }

    var curMode = BUBBLE_PARAMETERS.modes[mode_index];
    this.buttonId = curMode.button_id;
    this.gridDimensions = curMode.grid_dimensions;
    this.dataField = curMode.data_field;
    this.labels = curMode.labels;
    if (this.labels == null) { this.labels = [""]; }
    this.size = this.labels.length;

    // Loop through all grid labels and assign the centre coordinates
    this.gridCenters = {};
    for(var i=0; i<this.size; i++) {
        var cur_row = Math.floor(i / this.gridDimensions.columns);    // indexed starting at zero
        var cur_col = i % this.gridDimensions.columns;    // indexed starting at zero
        var xx = {
            x: (2 * cur_col + 1) * (width / (this.gridDimensions.columns * 2)),
            y: (2 * cur_row + 1) * (height / (this.gridDimensions.rows * 2))
        };
        this.gridCenters[this.labels[i]] = xx;
        console.log("gridcentre", i, xx.x, xx.y);
    }
};

/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

// Set title
document.title = BUBBLE_PARAMETERS.report_title;
report_title.innerHTML = BUBBLE_PARAMETERS.report_title;
// Set footer
document.getElementById("footer_text").innerHTML = BUBBLE_PARAMETERS.footer_text;

// Create a new bubble chart instance
var myBubbleChart = bubbleChart();

// Load data
d3.csv("data/" + BUBBLE_PARAMETERS.data_file, function (error, data) {
    // Once the data is loaded...

    if (error) { console.log(error); }

    // Display bubble chart inside the #vis div.
    myBubbleChart('#vis', data);

    // Start the visualization with the first button
    myBubbleChart.switchMode(BUBBLE_PARAMETERS.modes[0].button_id)
});

function setupButtons() {
    // As the data is being loaded: setup buttons
    // Create the buttons
    // TODO: change this to use native d3js selection methods
    for (i = 0; i<BUBBLE_PARAMETERS.modes.length; i++) {
        var button_element = document.createElement("a");
        button_element.href = "#";
        if (i == 0) {
            button_element.className = "button active";
        } else {
            button_element.className = "button";
        }
        button_element.id = BUBBLE_PARAMETERS.modes[i].button_id;
        button_element.innerHTML = BUBBLE_PARAMETERS.modes[i].button_text;
        document.getElementById("toolbar").appendChild(button_element);
    }

    // Handle button click
    // Set up the layout buttons to allow for toggling between view modes.
    d3.select('#toolbar')
        .selectAll('.button')
        .on('click', function () {
            // Remove active class from all buttons
            d3.selectAll('.button').classed('active', false);

            // Set the button just clicked to active
            d3.select(this).classed('active', true);

            // Get the id of the button
            var buttonId = d3.select(this).attr('id');

            // Switch the bubble chart to the mode of
            // the currently clicked button.
            myBubbleChart.switchMode(buttonId);
        });
}

setupButtons();
<commit_msg>Remove console.log debugging<commit_after>/* Bubble chart
 *
 * Based on Jim Vallandingham's work
 * Organization and style inspired by:
 * https://bost.ocks.org/mike/chart/
 *
 */

function bubbleChart() {
    /* bubbleChart creation function. Returns a function that will
     * instantiate a new bubble chart given a DOM element to display
     * it in and a dataset to visualize.
    */
    // Constants for sizing
    var width = BUBBLE_PARAMETERS.width;
    var height = BUBBLE_PARAMETERS.height;

    // Tooltip object for mouseover functionality
    var tooltip = floatingTooltip('bubble_chart_tooltip', 240);

    // Locations to move bubbles towards, depending
    // on which view mode is selected.
    var center = { x: width / 2, y: height / 2 };

    // SET COLOURS
    var color_groupsKeys = Object.keys(BUBBLE_PARAMETERS.fill_color.color_groups)
    var color_groupsValues = []
    for (var i=0; i<color_groupsKeys.length; i++) {
        var key = color_groupsKeys[i]
        color_groupsValues.push(BUBBLE_PARAMETERS.fill_color.color_groups[key])
    }

    // Nice looking colors - no reason to buck the trend
    var fillColor = d3.scaleOrdinal()
        .domain(color_groupsKeys)
        .range(color_groupsValues);

    // SCALE BUBBLE SIZES
    // Using ^(0.5), size bubbles based on area instead of radius
    var radiusScale = d3.scalePow()
        .exponent(0.5)
        .range([2, 25]);  // Range between 2 and 50 pixels

    // These will be set in createNodes and create_vis
    var svg = null;
    var bubbles = null;
    var forceSim = null;
    var nodes = [];

    function createNodes(rawData) {
        /*
         * This data manipulation function takes the raw data from
         * the CSV file and converts it into an array of node objects.
         * Each node will store data and visualization values to visualize
         * a bubble.
         *
         * rawData is expected to be an array of data objects, read in from
         * one of d3's loading functions like d3.csv.
         *
         * This function returns the new node array, with a node in that
         * array for each element in the rawData input.
         */
        // Use map() to convert raw data into node data.
        // Checkout http://learnjsdata.com/ for more on
        // working with data.
        var myNodes = rawData.map(function (data_row) {
            node = {
                id: data_row.id,
                scaled_radius: radiusScale(+data_row[BUBBLE_PARAMETERS.radius_field]),
                actual_radius: +data_row[BUBBLE_PARAMETERS.radius_field],
                fill_color_group: data_row[BUBBLE_PARAMETERS.fill_color.data_field],
                // Put each node initially in a random location
                x: Math.random() * BUBBLE_PARAMETERS.width,
                y: Math.random() * BUBBLE_PARAMETERS.height
            };
            for(var key in data_row) {
                // Skip loop if the property is from prototype
                if (!data_row.hasOwnProperty(key)) continue;
                node[key] = data_row[key];
            }

            return node;
        });

        // Sort them to prevent occlusion of smaller nodes.
        myNodes.sort(function (a, b) { return b.actual_radius - a.actual_radius; });

        return myNodes;
    }

    function getTargetFunction(mode) {
        // Given a mode, return an anonymous function that maps nodes to target coordinates
        return function (node) {
            // Given a mode and node, return the correct target
            if(mode.size == 1) {
                // If there is no grid, our target is the default center
                target = mode.gridCenters[""];
            } else {
                // If the grid size is greater than 1, look up the appropriate target
                // coordinate using the relevant node_tag for the mode we are in
                node_tag = node[mode.dataField]
                target = mode.gridCenters[node_tag];
            }
            return target;
        }
    }

    function showLabels(mode) {
        /*
         * Shows labels for each of the positions in the grid.
         */
        var currentLabels = mode.labels;
        var bubble_group_labels = svg.selectAll('.bubble_group_label')
            .data(currentLabels);

        var grid_element_half_height = BUBBLE_PARAMETERS.height / (mode.gridDimensions.rows * 2);

        bubble_group_labels.enter().append('text')
            .attr('class', 'bubble_group_label')
            .attr('x', function (d) { return mode.gridCenters[d].x; })
            .attr('y', function (d) { return mode.gridCenters[d].y - grid_element_half_height; })
            .attr('text-anchor', 'middle')                // centre the text
            .attr('dominant-baseline', 'hanging') // so the text is immediately below the bounding box, rather than above
            .text(function (d) { return d; });

        // GRIDLINES FOR DEBUGGING PURPOSES
        /*
        var grid_element_half_height = BUBBLE_PARAMETERS.height / (mode.gridDimensions.rows * 2);
        var grid_element_half_width = BUBBLE_PARAMETERS.width / (mode.gridDimensions.columns * 2);

        for (var key in currentMode.gridCenters) {
            if (currentMode.gridCenters.hasOwnProperty(key)) {
                var rectangle = svg.append("rect")
                    .attr("class", "mc_debug")
                    .attr("x", currentMode.gridCenters[key].x - grid_element_half_width)
                    .attr("y", currentMode.gridCenters[key].y - grid_element_half_height)
                    .attr("width", grid_element_half_width*2)
                    .attr("height", grid_element_half_height*2)
                    .attr("stroke", "red")
                    .attr("fill", "none");
                var ellipse = svg.append("ellipse")
                    .attr("class", "mc_debug")
                    .attr("cx", currentMode.gridCenters[key].x)
                    .attr("cy", currentMode.gridCenters[key].y)
                    .attr("rx", 15)
                    .attr("ry", 10);
            }
        }
        */
    }

    function tooltipContent(d) {
        /*
         * Helper function to generate the tooltip content
         *
         * Parameters: d, a dict from the node
         * Returns: a string representing the formatted HTML to display
         */
        var content = ''

        // Loop through all lines we want displayed in the tooltip
        for(var i=0; i<BUBBLE_PARAMETERS.tooltip.length; i++) {
            var cur_tooltip = BUBBLE_PARAMETERS.tooltip[i];
            var value_formatted;

            // If a format was specified, use it
            if ("format_string" in cur_tooltip) {
                value_formatted =
                    d3.format(cur_tooltip.format_string)(d[cur_tooltip.data_field]);
            } else {
                value_formatted = d[cur_tooltip.data_field];
            }

            // If there was a previous tooltip line, add a newline separator
            if (i > 0) {
                content += '
';
            }
            content += ''    + cur_tooltip.title + ': ';
            content += '' + value_formatted     + '';
        }

        return content;
    }

    function showTooltip(d) {
        /*
         * Function called on mouseover to display the
         * details of a bubble in the tooltip.
         */
        // Change the circle's outline to indicate hover state.
        d3.select(this).attr('stroke', 'black');

        // Show the tooltip
        tooltip.showTooltip(tooltipContent(d), d3.event);
    }

    function hideTooltip(d) {
        /*
         * Hide tooltip
         */
        // Reset the circle's outline back to its original color.
        var originalColor = d3.rgb(fillColor(d.fill_color_group)).darker()
        d3.select(this).attr('stroke', originalColor);

        // Hide the tooltip
        tooltip.hideTooltip();
    }

    function ticked() {
        bubbles.each(function (node) {})
            .attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; });
    }
    //////////////////////////////////////////////////////////////

    var chart = function chart(selector, rawData) {
        /*
         * Main entry point to the bubble chart. This function is returned
         * by the parent closure. It prepares the rawData for visualization
         * and adds an svg element to the provided selector and starts the
         * visualization creation process.
         *
         * selector is expected to be a DOM element or CSS selector that
         * points to the parent element of the bubble chart. Inside this
         * element, the code will add the SVG continer for the visualization.
         *
         * rawData is expected to be an array of data objects as provided by
         * a d3 loading function like d3.csv.
         */

        // Use the max radius in the data as the max in the scale's domain
        // (Ensure the radius is a number by converting it with `+`)
        var maxAmount = d3.max(rawData, function (d) { return +d[BUBBLE_PARAMETERS.radius_field]; });
        radiusScale.domain([0, maxAmount]);

        nodes = createNodes(rawData);

        // Create a SVG element inside the provided selector with desired size.
        svg = d3.select(selector)
            .append('svg')
            .attr('width', width)
            .attr('height', height);

        // Bind nodes data to what will become DOM elements to represent them.
        svg.selectAll('.bubble')
            .data(nodes, function (d) { return d.id; })
            // Create new circle elements each with class `bubble`.
            // There will be one circle.bubble for each object in the nodes array.
            .enter().append('circle')
            .classed('bubble', true)
            // Initially, their radius (r attribute) will be 0.
            .attr('r', 0)
            .attr('fill', function (d) { return fillColor(d.fill_color_group); })
            .attr('stroke', function (d) { return d3.rgb(fillColor(d.fill_color_group)).darker(); })
            .attr('stroke-width', 2)
            .on('mouseover', showTooltip)
            .on('mouseout', hideTooltip);

        bubbles = d3.selectAll('.bubble');

        // Fancy transition to make bubbles appear, ending with the correct radius
        bubbles.transition()
            .duration(2000)
            //.on("start", function (d) {console.log("Starting the bubbles growing");})
            .attr('r', function (d) { return d.scaled_radius; });

        forceSim = d3.forceSimulation(nodes)
            .velocityDecay(0.1)
            .force("collide", d3.forceCollide().radius(function(d) { return d.scaled_radius + 0.5; }).iterations(3))
            .on("tick", ticked);
    };

    chart.switchMode = function (buttonId) {
        /*
         * Externally accessible function (this is attached to the
         * returned chart function). Allows the visualization to toggle
         * between display modes.
         *
         * buttonId is expected to be a string corresponding to one of the modes.
         */
        // Get data on the new mode we have just switched to
        currentMode = new ViewMode(buttonId)

        // Remove current labels
        label_elements = svg.selectAll('.bubble_group_label').remove();
        // Remove current debugging elements
        debug_elements = svg.selectAll('.mc_debug').remove(); // DEBUG

        // Show labels, if we have more than one category to label
        if (currentMode.size > 1) {
            showLabels(currentMode);
        }

        // Move the bubbles to their new locations

        // Given the mode we are in, obtain the node -> target mapping
        var targetFunction = getTargetFunction(currentMode)

        // Specify the target of the force layout for each of the circles
        forceSim
            .force("x", d3.forceX(function(d) {return targetFunction(d).x}).strength(0.02))
            .force("y", d3.forceY(function(d) {return targetFunction(d).y}).strength(0.02));

        // Restart the force layout simulation
        forceSim.alphaTarget(1).restart();
    };

    // Return the chart function from closure.
    return chart;
}

/////////////////////////////////////////////////////////////////////////////////////
// OBJECT
function ViewMode(button_id) {
    /* ViewMode: an object that has useful parameters for each view mode.
     * initialize it with your desired view mode, then use its parameters.
     * Attributes:
     - mode_index (which button was pressed)
     - buttonId     (which button was pressed)
     - gridDimensions    e.g. {"rows": 10, "columns": 20}
     - gridCenters         e.g. {"group1": {"x": 10, "y": 20}, ...}
     - dataField    (string)
     - labels     (an array)
     - size         (number of groups)
     */
    var width = BUBBLE_PARAMETERS.width;
    var height = BUBBLE_PARAMETERS.height;

    // Find which button was pressed
    var mode_index;
    for(mode_index=0; mode_index<BUBBLE_PARAMETERS.modes.length; mode_index++) {
        if(BUBBLE_PARAMETERS.modes[mode_index].button_id == button_id) {
            break;
        }
    }
    if(mode_index>=BUBBLE_PARAMETERS.modes.length) {
        console.log("Error: can't find mode with button_id = ", button_id)
    }

    var curMode = BUBBLE_PARAMETERS.modes[mode_index];
    this.buttonId = curMode.button_id;
    this.gridDimensions = curMode.grid_dimensions;
    this.dataField = curMode.data_field;
    this.labels = curMode.labels;
    if (this.labels == null) { this.labels = [""]; }
    this.size = this.labels.length;

    // Loop through all grid labels and assign the centre coordinates
    this.gridCenters = {};
    for(var i=0; i<this.size; i++) {
        var cur_row = Math.floor(i / this.gridDimensions.columns);    // indexed starting at zero
        var cur_col = i % this.gridDimensions.columns;    // indexed starting at zero
        var currentCenter = {
            x: (2 * cur_col + 1) * (width / (this.gridDimensions.columns * 2)),
            y: (2 * cur_row + 1) * (height / (this.gridDimensions.rows * 2))
        };
        this.gridCenters[this.labels[i]] = currentCenter;
    }
};

/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

// Set title
document.title = BUBBLE_PARAMETERS.report_title;
report_title.innerHTML = BUBBLE_PARAMETERS.report_title;
// Set footer
document.getElementById("footer_text").innerHTML = BUBBLE_PARAMETERS.footer_text;

// Create a new bubble chart instance
var myBubbleChart = bubbleChart();

// Load data
d3.csv("data/" + BUBBLE_PARAMETERS.data_file, function (error, data) {
    // Once the data is loaded...

    if (error) { console.log(error); }

    // Display bubble chart inside the #vis div.
    myBubbleChart('#vis', data);

    // Start the visualization with the first button
    myBubbleChart.switchMode(BUBBLE_PARAMETERS.modes[0].button_id)
});

function setupButtons() {
    // As the data is being loaded: setup buttons
    // Create the buttons
    // TODO: change this to use native d3js selection methods
    for (i = 0; i<BUBBLE_PARAMETERS.modes.length; i++) {
        var button_element = document.createElement("a");
        button_element.href = "#";
        if (i == 0) {
            button_element.className = "button active";
        } else {
            button_element.className = "button";
        }
        button_element.id = BUBBLE_PARAMETERS.modes[i].button_id;
        button_element.innerHTML = BUBBLE_PARAMETERS.modes[i].button_text;
        document.getElementById("toolbar").appendChild(button_element);
    }

    // Handle button click
    // Set up the layout buttons to allow for toggling between view modes.
    d3.select('#toolbar')
        .selectAll('.button')
        .on('click', function () {
            // Remove active class from all buttons
            d3.selectAll('.button').classed('active', false);

            // Set the button just clicked to active
            d3.select(this).classed('active', true);

            // Get the id of the button
            var buttonId = d3.select(this).attr('id');

            // Switch the bubble chart to the mode of
            // the currently clicked button.
            myBubbleChart.switchMode(buttonId);
        });
}

setupButtons();
<|endoftext|>